PrismLauncher-11.0.3/0000755000175100017510000000000015224505336013763 5ustar runnerrunnerPrismLauncher-11.0.3/.clang-format0000644000175100017510000000070115224505336016334 0ustar runnerrunner--- BasedOnStyle: Chromium IndentWidth: 4 AllowShortIfStatementsOnASingleLine: false ColumnLimit: 140 --- Language: Cpp AccessModifierOffset: -1 AlignConsecutiveMacros: None AlignConsecutiveAssignments: None BraceWrapping: AfterFunction: true SplitEmptyFunction: false SplitEmptyRecord: false SplitEmptyNamespace: false BreakBeforeBraces: Custom BreakConstructorInitializers: BeforeComma Cpp11BracedListStyle: false QualifierAlignment: Left PrismLauncher-11.0.3/flake.nix0000644000175100017510000001566615224505336015603 0ustar runnerrunner{ description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)"; nixConfig = { extra-substituters = [ "https://prismlauncher.cachix.org" ]; extra-trusted-public-keys = [ "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c=" ]; }; inputs = { nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"; libnbtplusplus = { url = "github:PrismLauncher/libnbtplusplus"; flake = false; }; }; outputs = { self, nixpkgs, libnbtplusplus, }: let inherit (nixpkgs) lib; # While we only officially support aarch and x86_64 on Linux and MacOS, # we expose a reasonable amount of other systems for users who want to # build for most exotic platforms systems = lib.systems.flakeExposed; forAllSystems = lib.genAttrs systems; nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system}); in { checks = forAllSystems ( system: let pkgs = nixpkgsFor.${system}; llvm = pkgs.llvmPackages_22; in { 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_22; python = pkgs.python3; mkShell = pkgs.mkShell.override { inherit (llvm) stdenv; }; packages' = self.packages.${system}; welcomeMessage = '' Welcome to the Prism Launcher repository! 🌈 We just set some things up for you. To get building, you can run: ``` $ cd "$cmakeBuildDir" $ ninjaBuildPhase $ ninjaInstallPhase ``` Feel free to ask any questions in our Discord server or Matrix space: - https://prismlauncher.org/discord - https://matrix.to/#/#prismlauncher:matrix.org And thanks for helping out :) ''; # Re-use our package wrapper to wrap our development environment qt-wrapper-env = packages'.prismlauncher.overrideAttrs (old: { name = "qt-wrapper-env"; # Required to use script-based makeWrapper below strictDeps = true; # We don't need/want the unwrapped Prism 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 = mkShell { name = "prism-launcher"; inputsFrom = [ packages'.prismlauncher-unwrapped ]; packages = [ pkgs.ccache llvm.clang-tools python # NOTE(@getchoo): Required for run-clang-tidy, etc. (pkgs.stdenvNoCC.mkDerivation { pname = "clang-tidy-diff"; inherit (llvm.clang) version; nativeBuildInputs = [ pkgs.installShellFiles python.pkgs.wrapPython ]; dontUnpack = true; dontConfigure = true; dontBuild = true; postInstall = "installBin ${llvm.libclang.python}/share/clang/clang-tidy-diff.py"; postFixup = "wrapPythonPrograms"; }) ]; cmakeBuildType = "Debug"; cmakeFlags = [ "-GNinja" ] ++ packages'.prismlauncher-unwrapped.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 echo ${lib.escapeShellArg welcomeMessage} ''; }; } ); formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style); overlays.default = final: prev: let llvm = final.llvmPackages_22 or prev.llvmPackages_22; in { prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { inherit (llvm) stdenv; inherit libnbtplusplus self ; }; prismlauncher = final.callPackage ./nix/wrapper.nix { }; }; packages = forAllSystems ( system: let pkgs = nixpkgsFor.${system}; # Build a scope from our overlay prismPackages = lib.makeScope pkgs.newScope (final: self.overlays.default final pkgs); # Grab our packages from it and set the default packages = { inherit (prismPackages) prismlauncher-unwrapped prismlauncher; default = prismPackages.prismlauncher; }; in # Only output them if they're available on the current system lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages ); # We put these under legacyPackages as they are meant for CI, not end user consumption legacyPackages = forAllSystems ( system: let packages' = self.packages.${system}; legacyPackages' = self.legacyPackages.${system}; in { prismlauncher-debug = packages'.prismlauncher.override { prismlauncher-unwrapped = legacyPackages'.prismlauncher-unwrapped-debug; }; prismlauncher-unwrapped-debug = packages'.prismlauncher-unwrapped.overrideAttrs { cmakeBuildType = "Debug"; dontStrip = true; }; } ); }; } PrismLauncher-11.0.3/CODE_OF_CONDUCT.md0000644000175100017510000001310415224505336016561 0ustar runnerrunner# Contributor Covenant Code of Conduct This is a modified version of the Contributor Covenant. See commit history to see our changes. ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling (antagonistic, inflammatory, insincere behaviour), insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement via email at [coc@scrumplex.net](mailto:coc@scrumplex.net) (Email address subject to change). All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations PrismLauncher-11.0.3/vcpkg-configuration.json0000644000175100017510000000071215224505336020635 0ustar runnerrunner{ "default-registry": { "kind": "git", "baseline": "2d6a6cf3ac9a7cc93942c3d289a2f9c661a6f4a7", "repository": "https://github.com/microsoft/vcpkg" }, "registries": [ { "kind": "artifact", "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", "name": "microsoft" } ], "overlay-ports": [ "./cmake/vcpkg-ports" ], "overlay-triplets": [ "./cmake/vcpkg-triplets" ] } PrismLauncher-11.0.3/tools/0000755000175100017510000000000015224505336015123 5ustar runnerrunnerPrismLauncher-11.0.3/tools/ninjatracing.py0000644000175100017510000001572315224505336020154 0ustar runnerrunner#!/usr/bin/env python3 # Copyright 2018 Nico Weber # Patched to work with v7 logs # # 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. """Converts one (or several) .ninja_log files into chrome's about:tracing format. If clang -ftime-trace .json files are found adjacent to generated files they are embedded. Usage: ninja -C $BUILDDIR ninjatracing $BUILDDIR/.ninja_log > trace.json """ import json import os import optparse import re import sys class Target: """Represents a single line read for a .ninja_log file. Start and end times are milliseconds.""" def __init__(self, start, end): self.start = int(start) self.end = int(end) self.targets = [] def read_targets(log, show_all): """Reads all targets from .ninja_log file |log_file|, sorted by start time""" header = log.readline() m = re.search(r'^# ninja log v(\d+)\n$', header) assert m, "unrecognized ninja log version %r" % header version = int(m.group(1)) assert 5 <= version <= 7, "unsupported ninja log version %d" % version if version >= 6: # Skip header line next(log) targets = {} last_end_seen = 0 for line in log: if line.startswith('#'): continue start, end, _, name, cmdhash = line.strip().split('\t') # Ignore restat. if not show_all and int(end) < last_end_seen: # An earlier time stamp means that this step is the first in a new # build, possibly an incremental build. Throw away the previous data # so that this new build will be displayed independently. targets = {} last_end_seen = int(end) targets.setdefault(cmdhash, Target(start, end)).targets.append(name) return sorted(targets.values(), key=lambda job: job.end, reverse=True) class Threads: """Tries to reconstruct the parallelism from a .ninja_log""" def __init__(self): self.workers = [] # Maps thread id to time that thread is occupied for. def alloc(self, target): """Places target in an available thread, or adds a new thread.""" for worker in range(len(self.workers)): if self.workers[worker] >= target.end: self.workers[worker] = target.start return worker self.workers.append(target.start) return len(self.workers) - 1 def read_events(trace, options): """Reads all events from time-trace json file |trace|.""" trace_data = json.load(trace) def include_event(event, options): """Only include events if they are complete events, are longer than granularity, and are not totals.""" return ((event['ph'] == 'X') and (event['dur'] >= options['granularity']) and (not event['name'].startswith('Total'))) return [x for x in trace_data['traceEvents'] if include_event(x, options)] def trace_to_dicts(target, trace, options, pid, tid): """Read a file-like object |trace| containing -ftime-trace data and yields about:tracing dict per eligible event in that log.""" ninja_time = (target.end - target.start) * 1000 for event in read_events(trace, options): # Check if any event duration is greater than the duration from ninja. if event['dur'] > ninja_time: print("Inconsistent timing found (clang time > ninja time). Please" " ensure that timings are from consistent builds.") sys.exit(1) # Set tid and pid from ninja log. event['pid'] = pid event['tid'] = tid # Offset trace time stamp by ninja start time. event['ts'] += (target.start * 1000) yield event def embed_time_trace(ninja_log_dir, target, pid, tid, options): """Produce time trace output for the specified ninja target. Expects time-trace file to be in .json file named based on .o file.""" for t in target.targets: o_path = os.path.join(ninja_log_dir, t) json_trace_path = os.path.splitext(o_path)[0] + '.json' try: with open(json_trace_path, 'r') as trace: for time_trace_event in trace_to_dicts(target, trace, options, pid, tid): yield time_trace_event except IOError: pass def log_to_dicts(log, pid, options): """Reads a file-like object |log| containing a .ninja_log, and yields one about:tracing dict per command found in the log.""" threads = Threads() for target in read_targets(log, options['showall']): tid = threads.alloc(target) yield { 'name': '%0s' % ', '.join(target.targets), 'cat': 'targets', 'ph': 'X', 'ts': (target.start * 1000), 'dur': ((target.end - target.start) * 1000), 'pid': pid, 'tid': tid, 'args': {}, } if options.get('embed_time_trace', False): # Add time-trace information into the ninja trace. try: ninja_log_dir = os.path.dirname(log.name) except AttributeError: continue for time_trace in embed_time_trace(ninja_log_dir, target, pid, tid, options): yield time_trace def main(argv): usage = __doc__ parser = optparse.OptionParser(usage) parser.add_option('-a', '--showall', action='store_true', dest='showall', default=False, help='report on last build step for all outputs. Default ' 'is to report just on the last (possibly incremental) ' 'build') parser.add_option('-g', '--granularity', type='int', default=50000, dest='granularity', help='minimum length time-trace event to embed in ' 'microseconds. Default: %default') parser.add_option('-e', '--embed-time-trace', action='store_true', default=False, dest='embed_time_trace', help='embed clang -ftime-trace json file found adjacent ' 'to a target file') (options, args) = parser.parse_args() if len(args) == 0: print('Must specify at least one .ninja_log file') parser.print_help() return 1 entries = [] for pid, log_file in enumerate(args): with open(log_file, 'r') as log: entries += list(log_to_dicts(log, pid, vars(options))) json.dump(entries, sys.stdout) if __name__ == '__main__': sys.exit(main(sys.argv[1:])) PrismLauncher-11.0.3/Containerfile0000644000175100017510000000436515224505336016500 0ustar runnerrunnerARG DEBIAN_VERSION=stable-slim FROM docker.io/library/debian:${DEBIAN_VERSION} ARG QT_ABI=gcc_64 ARG QT_ARCH= ARG QT_HOST=linux ARG QT_TARGET=desktop ARG QT_VERSION=6.10.2 ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update \ && apt-get --assume-yes upgrade \ && apt-get --assume-yes autopurge # Use Adoptium for Java 17 RUN apt-get --assume-yes --no-install-recommends install \ apt-transport-https ca-certificates curl gpg RUN curl -L https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | tee /etc/apt/trusted.gpg.d/adoptium.gpg RUN echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | tee /etc/apt/sources.list.d/adoptium.list RUN apt-get update # Install base dependencies RUN apt-get --assume-yes --no-install-recommends install \ # Compilers clang lld llvm temurin-17-jdk \ # Build system cmake ninja-build extra-cmake-modules pkg-config \ # Dependencies cmark gamemode-dev libarchive-dev libcmark-dev libgamemode0 libgl1-mesa-dev libqrencode-dev libtomlplusplus-dev scdoc zlib1g-dev \ # Tooling clang-format clang-tidy git # Use LLD by default for faster linking ENV CMAKE_LINKER_TYPE=lld # Prepare and install Qt ## Setup UTF-8 locale (required, apparently) RUN apt-get --assume-yes --no-install-recommends install locales RUN echo "C.UTF-8 UTF-8" > /etc/locale.gen RUN locale-gen ENV LC_ALL=C.UTF-8 ## Some libraries are required for the official binaries RUN apt-get --assume-yes --no-install-recommends install \ libglib2.0-0t64 libxkbcommon0 python3-pip RUN pip3 install --break-system-packages aqtinstall RUN aqt install-qt \ ${QT_HOST} ${QT_TARGET} ${QT_VERSION} ${QT_ARCH} \ --outputdir /opt/qt \ --modules qtimageformats qtnetworkauth ENV PATH=/opt/qt/${QT_VERSION}/${QT_ABI}/bin:$PATH ENV QT_PLUGIN_PATH=/opt/qt/${QT_VERSION}/${QT_ABI}/plugins/ ## We don't use these. Nuke them RUN rm -rf \ "$QT_PLUGIN_PATH"/designer \ "$QT_PLUGIN_PATH"/help \ # "$QT_PLUGIN_PATH"/platformthemes/libqgtk3.so \ "$QT_PLUGIN_PATH"/printsupport \ "$QT_PLUGIN_PATH"/qmllint \ "$QT_PLUGIN_PATH"/qmlls \ "$QT_PLUGIN_PATH"/qmltooling \ "$QT_PLUGIN_PATH"/sqldrivers # Setup workspace RUN mkdir /work WORKDIR /work ENTRYPOINT ["bash"] CMD ["-i"] PrismLauncher-11.0.3/buildconfig/0000755000175100017510000000000015224505336016250 5ustar runnerrunnerPrismLauncher-11.0.3/buildconfig/BuildConfig.cpp.in0000644000175100017510000001315715224505336021555 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include "BuildConfig.h" const Config BuildConfig; Config::Config() { // Name and copyright LAUNCHER_NAME = "@Launcher_Name@"; LAUNCHER_APP_BINARY_NAME = "@Launcher_APP_BINARY_NAME@"; LAUNCHER_DISPLAYNAME = "@Launcher_DisplayName@"; LAUNCHER_COPYRIGHT = "@Launcher_Copyright@"; LAUNCHER_DOMAIN = "@Launcher_Domain@"; LAUNCHER_CONFIGFILE = "@Launcher_ConfigFile@"; LAUNCHER_GIT = "@Launcher_Git@"; LAUNCHER_APPID = "@Launcher_AppID@"; LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@"; LAUNCHER_ENVNAME = "@Launcher_ENVName@"; USER_AGENT = "@Launcher_UserAgent@"; // Version information VERSION_MAJOR = @Launcher_VERSION_MAJOR@; VERSION_MINOR = @Launcher_VERSION_MINOR@; VERSION_PATCH = @Launcher_VERSION_PATCH@; BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; BUILD_ARTIFACT = "@Launcher_BUILD_ARTIFACT@"; BUILD_DATE = "@Launcher_BUILD_TIMESTAMP@"; UPDATER_GITHUB_REPO = "@Launcher_UPDATER_GITHUB_REPO@"; COMPILER_NAME = "@Launcher_COMPILER_NAME@"; COMPILER_VERSION = "@Launcher_COMPILER_VERSION@"; COMPILER_TARGET_SYSTEM = "@Launcher_COMPILER_TARGET_SYSTEM@"; COMPILER_TARGET_SYSTEM_VERSION = "@Launcher_COMPILER_TARGET_SYSTEM_VERSION@"; COMPILER_TARGET_SYSTEM_PROCESSOR = "@Launcher_COMPILER_TARGET_PROCESSOR@"; MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@"; MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@"; if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) { UPDATER_ENABLED = true; } else if (!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) { UPDATER_ENABLED = true; } #cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER JAVA_DOWNLOADER_ENABLED = Launcher_ENABLE_JAVA_DOWNLOADER; GIT_COMMIT = "@Launcher_GIT_COMMIT@"; GIT_TAG = "@Launcher_GIT_TAG@"; GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; // Assume that builds outside of Git repos are "stable" if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") || GIT_REFSPEC == QStringLiteral("") || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) { GIT_REFSPEC = "refs/heads/stable"; GIT_TAG = versionString(); GIT_COMMIT = ""; } if (GIT_REFSPEC.startsWith("refs/heads/")) { VERSION_CHANNEL = GIT_REFSPEC; VERSION_CHANNEL.remove("refs/heads/"); } else if (!GIT_COMMIT.isEmpty()) { VERSION_CHANNEL = GIT_COMMIT.mid(0, 8); } else { VERSION_CHANNEL = "unknown"; } NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@"; NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@"; WIKI_URL = "@Launcher_WIKI_URL@"; HELP_URL = "@Launcher_HELP_URL@"; LOGIN_CALLBACK_URL = "@Launcher_LOGIN_CALLBACK_URL@"; IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@"; MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@"; FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@"; META_URL = "@Launcher_META_URL@"; LEGACY_FMLLIBS_BASE_URL = "@Launcher_LEGACY_FMLLIBS_BASE_URL@"; GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@"; OPENAL_LIBRARY_NAME = "@Launcher_OPENAL_LIBRARY_NAME@"; BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@"; TRANSLATIONS_URL = "@Launcher_TRANSLATIONS_URL@"; TRANSLATION_FILES_URL = "@Launcher_TRANSLATION_FILES_URL@"; MATRIX_URL = "@Launcher_MATRIX_URL@"; DISCORD_URL = "@Launcher_DISCORD_URL@"; SUBREDDIT_URL = "@Launcher_SUBREDDIT_URL@"; } QString Config::versionString() const { return QString("%1.%2.%3").arg(VERSION_MAJOR).arg(VERSION_MINOR).arg(VERSION_PATCH); } QString Config::printableVersionString() const { QString vstr = versionString(); // If the build is not a main release, append the channel if (VERSION_CHANNEL != "stable" && GIT_TAG != vstr) { vstr += "-" + VERSION_CHANNEL; } return vstr; } QString Config::compilerID() const { if (COMPILER_VERSION.isEmpty()) return COMPILER_NAME; return QStringLiteral("%1 - %2").arg(COMPILER_NAME).arg(COMPILER_VERSION); } QString Config::systemID() const { return QStringLiteral("%1 %2 %3").arg(COMPILER_TARGET_SYSTEM, COMPILER_TARGET_SYSTEM_VERSION, COMPILER_TARGET_SYSTEM_PROCESSOR); } PrismLauncher-11.0.3/buildconfig/BuildConfig.h0000644000175100017510000001533415224505336020614 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include /** * \brief The Config class holds all the build-time information passed from the build system. */ class Config { public: Config(); QString LAUNCHER_NAME; QString LAUNCHER_APP_BINARY_NAME; QString LAUNCHER_DISPLAYNAME; QString LAUNCHER_COPYRIGHT; QString LAUNCHER_DOMAIN; QString LAUNCHER_CONFIGFILE; QString LAUNCHER_GIT; QString LAUNCHER_APPID; QString LAUNCHER_SVGFILENAME; QString LAUNCHER_ENVNAME; /// The major version number. int VERSION_MAJOR; /// The minor version number. int VERSION_MINOR; /// The patch version number. int VERSION_PATCH; /** * The version channel * This is used by the updater to determine what channel the current version came from. */ QString VERSION_CHANNEL; bool UPDATER_ENABLED = false; bool JAVA_DOWNLOADER_ENABLED = false; /// A short string identifying this build's platform or distribution. QString BUILD_PLATFORM; /// A short string identifying this build's valid artifacts int he updater. For example, "lin64" or "win32". QString BUILD_ARTIFACT; /// A string containing the build timestamp QString BUILD_DATE; /// A string identifying the compiler use to build QString COMPILER_NAME; /// A string identifying the compiler version used to build QString COMPILER_VERSION; /// A string identifying the compiler target system os QString COMPILER_TARGET_SYSTEM; /// A String identifying the compiler target system version QString COMPILER_TARGET_SYSTEM_VERSION; /// A String identifying the compiler target processor QString COMPILER_TARGET_SYSTEM_PROCESSOR; /// URL for the updater's channel QString UPDATER_GITHUB_REPO; /// The public key used to sign releases for the Sparkle updater appcast QString MAC_SPARKLE_PUB_KEY; /// URL for the Sparkle updater's appcast QString MAC_SPARKLE_APPCAST_URL; /// User-Agent to use. QString USER_AGENT; /// The git commit hash of this build QString GIT_COMMIT; /// The git tag of this build QString GIT_TAG; /// The git refspec of this build QString GIT_REFSPEC; /** * This is used to fetch the news RSS feed. * It defaults in CMakeLists.txt to "https://multimc.org/rss.xml" */ QString NEWS_RSS_URL; /** * URL that gets opened when the user clicks "More News" */ QString NEWS_OPEN_URL; /** * URL that gets opened when the user clicks 'Launcher Help' */ QString WIKI_URL; /** * URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help in a dialog window */ QString HELP_URL; /** * URL that gets opened when the user succesfully logins. */ QString LOGIN_CALLBACK_URL; /** * Client ID you can get from Imgur when you register an application */ QString IMGUR_CLIENT_ID; /** * Client ID you can get from Microsoft Identity Platform when you register an application */ QString MSA_CLIENT_ID; /** * Client API key for CurseForge */ QString FLAME_API_KEY; /** * Metadata repository URL prefix */ QString META_URL; QString GLFW_LIBRARY_NAME; QString OPENAL_LIBRARY_NAME; QString BUG_TRACKER_URL; QString TRANSLATIONS_URL; QString MATRIX_URL; QString DISCORD_URL; QString SUBREDDIT_URL; QString DEFAULT_RESOURCE_BASE = "https://resources.download.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; QString LEGACY_FMLLIBS_BASE_URL; QString TRANSLATION_FILES_URL; QString FTB_API_BASE_URL = "https://api.feed-the-beast.com/v1/modpacks/public"; QString LEGACY_FTB_CDN_BASE_URL = "https://dist.creeper.host/FTB2/"; QString ATL_DOWNLOAD_SERVER_URL = "https://download.nodecdn.net/containers/atl/"; QString ATL_API_BASE_URL = "https://api.atlauncher.com/v1/"; QString TECHNIC_API_BASE_URL = "https://api.technicpack.net/"; /** * The build that is reported to the Technic API. */ QString TECHNIC_API_BUILD = "multimc"; QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2"; QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2"; QStringList MODRINTH_MRPACK_HOSTS{ "cdn.modrinth.com", "github.com", "raw.githubusercontent.com", "gitlab.com" }; QString MODRINTH_DOWNLOAD_HOST = "cdn.modrinth.com"; QString FLAME_BASE_URL = "https://api.curseforge.com/v1"; QString FLAME_DOWNLOAD_HOST = "edge.forgecdn.net"; QString versionString() const; /** * \brief Converts the Version to a string. * \return The version number in string format (major.minor.revision.build). */ QString printableVersionString() const; /** * \brief Compiler ID String * \return a string of the form "Name - Version" of just "Name" if the version is empty */ QString compilerID() const; /** * \brief System ID String * \return a string of the form "OS Verison Processor" */ QString systemID() const; }; extern const Config BuildConfig; PrismLauncher-11.0.3/buildconfig/CMakeLists.txt0000644000175100017510000000063415224505336021013 0ustar runnerrunner######## Configure the file with build properties ######## configure_file("${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp") add_library(BuildConfig STATIC BuildConfig.h ${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp ) target_link_libraries(BuildConfig Qt${QT_VERSION_MAJOR}::Core) target_include_directories(BuildConfig PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") PrismLauncher-11.0.3/CMakePresets.json0000644000175100017510000001424615224505336017213 0ustar runnerrunner{ "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", "version": 8, "cmakeMinimumRequired": { "major": 3, "minor": 28 }, "configurePresets": [ { "name": "base", "hidden": true, "binaryDir": "build", "installDir": "install", "generator": "Ninja Multi-Config", "cacheVariables": { "Launcher_BUILD_ARTIFACT": "$penv{ARTIFACT_NAME}", "Launcher_BUILD_PLATFORM": "$penv{BUILD_PLATFORM}", "Launcher_ENABLE_JAVA_DOWNLOADER": "ON", "ENABLE_LTO": "ON" } }, { "name": "linux", "displayName": "Linux", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" } }, { "name": "macos", "displayName": "macOS", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" }, "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "$penv{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" } }, { "name": "macos_universal", "displayName": "macOS (Universal Binary)", "inherits": [ "macos" ], "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "$penv{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64", "VCPKG_TARGET_TRIPLET": "universal-osx" } }, { "name": "windows_mingw", "displayName": "Windows (MinGW)", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" } }, { "name": "windows_msvc", "displayName": "Windows (MSVC)", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "$penv{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" } } ], "buildPresets": [ { "name": "linux", "displayName": "Linux", "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" }, "configurePreset": "linux" }, { "name": "macos", "displayName": "macOS", "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" }, "configurePreset": "macos" }, { "name": "macos_universal", "displayName": "macOS (Universal Binary)", "inherits": [ "macos" ], "configurePreset": "macos_universal" }, { "name": "windows_mingw", "displayName": "Windows (MinGW)", "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "configurePreset": "windows_mingw" }, { "name": "windows_msvc", "displayName": "Windows (MSVC)", "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "configurePreset": "windows_msvc" } ], "testPresets": [ { "name": "base", "hidden": true, "output": { "outputOnFailure": true, "verbosity": "extra" }, "execution": { "noTestsAction": "error" }, "filter": { "exclude": { "name": "^example64|example$" } } }, { "name": "linux", "displayName": "Linux", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" }, "configurePreset": "linux" }, { "name": "macos", "displayName": "macOS", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" }, "configurePreset": "macos" }, { "name": "macos_universal", "displayName": "macOS (Universal Binary)", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" }, "configurePreset": "macos_universal" }, { "name": "windows_mingw", "displayName": "Windows (MinGW)", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "configurePreset": "windows_mingw" }, { "name": "windows_msvc", "displayName": "Windows (MSVC)", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "configurePreset": "windows_msvc" } ] } PrismLauncher-11.0.3/.gitignore0000644000175100017510000000122615224505336015754 0ustar runnerrunnerThumbs.db *.kdev4 .user .directory resources/CMakeFiles *~ *.swp html/ # Project Files *.pro.user CMakeLists.txt.user CMakeLists.txt.user.* CMakeSettings.json /CMakeFiles CMakeCache.txt CMakeUserPresets.json /.project /.settings /.idea /.vscode /.vs cmake-build-*/ Debug compile_commands.json # Build dirs build /build-* # Install dirs install /install-* # Ctags File tags # YouCompleteMe config stuff. .ycm_extra_conf.* #OSX Stuff .DS_Store branding/ secrets/ run/ .cache/ # Nix/NixOS .direnv/ ## Used when manually invoking stdenv phases outputs/ ## Regular artifacts result result-* repl-result-* # Flatpak .flatpak-builder flatbuild # Snap *.snap PrismLauncher-11.0.3/.clang-tidy0000644000175100017510000000271315224505336016022 0ustar runnerrunnerFormatStyle: file Checks: "bugprone-*,clang-analyzer-*,cppcoreguidelines-*,hicpp-*,misc-*,modernize-*,performance-*,portability-*,readability-*, -*-magic-numbers, -*-non-private-member-variables-in-classes, -*-special-member-functions, -bugprone-easily-swappable-parameters, -cppcoreguidelines-owning-memory, -cppcoreguidelines-pro-type-static-cast-downcast, -modernize-use-nodiscard, -modernize-use-trailing-return-type, -portability-avoid-pragma-once, -readability-avoid-unconditional-preprocessor-if, -readability-function-cognitive-complexity, -readability-identifier-length, -readability-redundant-access-specifiers" CheckOptions: misc-include-cleaner.MissingIncludes: false readability-identifier-naming.DefaultCase: "camelBack" readability-identifier-naming.NamespaceCase: "CamelCase" readability-identifier-naming.ClassCase: "CamelCase" readability-identifier-naming.ClassConstantCase: "CamelCase" readability-identifier-naming.EnumCase: "CamelCase" readability-identifier-naming.EnumConstantCase: "CamelCase" readability-identifier-naming.MacroDefinitionCase: "UPPER_CASE" readability-identifier-naming.ClassMemberPrefix: "m_" readability-identifier-naming.StaticConstantPrefix: "s_" readability-identifier-naming.StaticVariablePrefix: "s_" readability-identifier-naming.GlobalConstantPrefix: "g_" readability-implicit-bool-conversion.AllowPointerConditions: true PrismLauncher-11.0.3/tests/0000755000175100017510000000000015224505336015125 5ustar runnerrunnerPrismLauncher-11.0.3/tests/Version_test.cpp0000644000175100017510000001777215224505336020333 0ustar runnerrunner/* 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 #include class VersionTest : public QObject { Q_OBJECT QStringList m_flex_test_names = {}; void addDataColumns() { QTest::addColumn("first"); QTest::addColumn("second"); QTest::addColumn("lessThan"); QTest::addColumn("equal"); } void setupVersions() { addDataColumns(); QTest::newRow("equal, explicit") << "1.2.0" << "1.2.0" << false << true; QTest::newRow("equal, two-digit") << "1.42" << "1.42" << false << true; QTest::newRow("lessThan, explicit 1") << "1.2.0" << "1.2.1" << true << false; QTest::newRow("lessThan, explicit 2") << "1.2.0" << "1.3.0" << true << false; QTest::newRow("lessThan, explicit 3") << "1.2.0" << "2.2.0" << true << false; QTest::newRow("lessThan, implicit 1") << "1.2" << "1.2.0" << true << false; QTest::newRow("lessThan, implicit 2") << "1.2" << "1.2.1" << true << false; QTest::newRow("lessThan, implicit 3") << "1.2" << "1.3.0" << true << false; QTest::newRow("lessThan, implicit 4") << "1.2" << "2.2.0" << true << false; QTest::newRow("lessThan, two-digit") << "1.41" << "1.42" << true << false; QTest::newRow("lessThan, snapshot") << "1.20.0-rc2" << "1.20.1" << true << false; QTest::newRow("greaterThan, explicit 1") << "1.2.1" << "1.2.0" << false << false; QTest::newRow("greaterThan, explicit 2") << "1.3.0" << "1.2.0" << false << false; QTest::newRow("greaterThan, explicit 3") << "2.2.0" << "1.2.0" << false << false; QTest::newRow("greaterThan, implicit 1") << "1.2.0" << "1.2" << false << false; QTest::newRow("greaterThan, implicit 2") << "1.2.1" << "1.2" << false << false; QTest::newRow("greaterThan, implicit 3") << "1.3.0" << "1.2" << false << false; QTest::newRow("greaterThan, implicit 4") << "2.2.0" << "1.2" << false << false; QTest::newRow("greaterThan, two-digit") << "1.42" << "1.41" << false << false; QTest::newRow("greaterThan, snapshot") << "1.20.2-rc2" << "1.20.1" << false << false; } private slots: void test_versionCompare_data() { setupVersions(); } void test_versionCompare() { QFETCH(QString, first); QFETCH(QString, second); QFETCH(bool, lessThan); QFETCH(bool, equal); const auto v1 = Version(first); const auto v2 = Version(second); qDebug() << v1 << "vs" << v2; QCOMPARE(v1 < v2, lessThan); QCOMPARE(v1 > v2, !lessThan && !equal); QCOMPARE(v1 == v2, equal); } void test_flexVerTestVector_data() { addDataColumns(); QDir test_vector_dir(QFINDTESTDATA("testdata/Version")); QFile vector_file{ test_vector_dir.absoluteFilePath("test_vectors.txt") }; if (!vector_file.open(QFile::OpenModeFlag::ReadOnly)) { qCritical() << "Failed to open file" << vector_file.fileName() << "for reading:" << vector_file.errorString(); return; } int test_number = 0; const QString test_name_template{ "FlexVer test #%1 (%2)" }; for (auto line = vector_file.readLine(); !vector_file.atEnd(); line = vector_file.readLine()) { line = line.simplified(); if (line.startsWith('#') || line.isEmpty()) continue; test_number += 1; auto split_line = line.split('<'); if (split_line.size() == 2) { QString first{ split_line.first().simplified() }; QString second{ split_line.last().simplified() }; auto new_test_name = test_name_template.arg(QString::number(test_number), "lessThan"); m_flex_test_names.append(new_test_name); QTest::newRow(m_flex_test_names.last().toLatin1().data()) << first << second << true << false; continue; } split_line = line.split('='); if (split_line.size() == 2) { QString first{ split_line.first().simplified() }; QString second{ split_line.last().simplified() }; auto new_test_name = test_name_template.arg(QString::number(test_number), "equals"); m_flex_test_names.append(new_test_name); QTest::newRow(m_flex_test_names.last().toLatin1().data()) << first << second << false << true; continue; } split_line = line.split('>'); if (split_line.size() == 2) { QString first{ split_line.first().simplified() }; QString second{ split_line.last().simplified() }; auto new_test_name = test_name_template.arg(QString::number(test_number), "greaterThan"); m_flex_test_names.append(new_test_name); QTest::newRow(m_flex_test_names.last().toLatin1().data()) << first << second << false << false; continue; } qCritical() << "Unexpected separator in the test vector:"; qCritical() << line; QVERIFY(0 != 0); } vector_file.close(); } void test_flexVerTestVector() { QFETCH(QString, first); QFETCH(QString, second); QFETCH(bool, lessThan); QFETCH(bool, equal); const auto v1 = Version(first); const auto v2 = Version(second); qDebug() << v1 << "vs" << v2; QCOMPARE(v1 < v2, lessThan); QCOMPARE(v1 > v2, !lessThan && !equal); QCOMPARE(v1 == v2, equal); } static void test_strict_weak_order() { // this tests the strict_weak_order // https://en.cppreference.com/w/cpp/concepts/strict_weak_order.html const Version a("1.10 Pre-Release 1"); // this is a pre-relese is before b because ' ' is lower than '-' const Version b("1.10-pre1"); // this is a pre-release is before c that is an actual release const Version c("1.10"); auto r = [](const Version& a, const Version& b) { return a < b; }; auto e = [&r](const Version& a, const Version& b) { return !r(a, b) && !r(b, a); }; qCritical() << a << b << c; // irreflexive QCOMPARE(r(a, a), false); QCOMPARE(r(b, b), false); QCOMPARE(r(c, c), false); // transitive QCOMPARE(r(a, b), true); QCOMPARE(r(b, c), true); QCOMPARE(r(a, c), true); // transitive equivalence QCOMPARE(e(a, b) && e(b, c), e(a, c)); } }; QTEST_GUILESS_MAIN(VersionTest) #include "Version_test.moc"PrismLauncher-11.0.3/tests/Index_test.cpp0000644000175100017510000000327615224505336017747 0ustar runnerrunner#include #include #include class IndexTest : public QObject { Q_OBJECT private slots: void test_hasUid_and_getList() { Meta::Index windex({ std::make_shared("list1"), std::make_shared("list2"), std::make_shared("list3") }); QVERIFY(windex.hasUid("list1")); QVERIFY(!windex.hasUid("asdf")); QVERIFY(windex.get("list2") != nullptr); QCOMPARE(windex.get("list2")->uid(), QString("list2")); QVERIFY(windex.get("adsf") != nullptr); } void test_merge() { Meta::Index windex({ std::make_shared("list1"), std::make_shared("list2"), std::make_shared("list3") }); QCOMPARE(windex.lists().size(), 3); windex.merge(std::shared_ptr( new Meta::Index({ std::make_shared("list1"), std::make_shared("list2"), std::make_shared("list3") }))); QCOMPARE(windex.lists().size(), 3); windex.merge(std::shared_ptr( new Meta::Index({ std::make_shared("list4"), std::make_shared("list2"), std::make_shared("list5") }))); QCOMPARE(windex.lists().size(), 5); windex.merge(std::shared_ptr(new Meta::Index({ std::make_shared("list6") }))); QCOMPARE(windex.lists().size(), 6); } }; QTEST_GUILESS_MAIN(IndexTest) #include "Index_test.moc" PrismLauncher-11.0.3/tests/TexturePackParse_test.cpp0000644000175100017510000000453115224505336022125 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include "FileSystem.h" #include "minecraft/mod/TexturePack.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" class TexturePackParseTest : public QObject { Q_OBJECT private slots: void test_parseZIP() { QString source = QFINDTESTDATA("testdata/TexturePackParse"); QString zip_rp = FS::PathCombine(source, "test_texture_pack_idk.zip"); TexturePack pack{ QFileInfo(zip_rp) }; bool valid = TexturePackUtils::processZIP(pack); QVERIFY(pack.description() == "joe biden, wake up"); QVERIFY(valid == true); } void test_parseFolder() { QString source = QFINDTESTDATA("testdata/TexturePackParse"); QString folder_rp = FS::PathCombine(source, "test_texturefolder"); TexturePack pack{ QFileInfo(folder_rp) }; bool valid = TexturePackUtils::processFolder(pack, TexturePackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.description() == "Some texture pack surely"); QVERIFY(valid == true); } void test_parseFolder2() { QString source = QFINDTESTDATA("testdata/TexturePackParse"); QString folder_rp = FS::PathCombine(source, "another_test_texturefolder"); TexturePack pack{ QFileInfo(folder_rp) }; bool valid = TexturePackUtils::process(pack, TexturePackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.description() == "quieres\nfor real"); QVERIFY(valid == true); } }; QTEST_GUILESS_MAIN(TexturePackParseTest) #include "TexturePackParse_test.moc" PrismLauncher-11.0.3/tests/CatPack_test.cpp0000644000175100017510000000353215224505336020201 0ustar runnerrunner#include #include #include #include #include #include "FileSystem.h" #include "ui/themes/CatPack.h" class CatPackTest : public QObject { Q_OBJECT private slots: void test_catPack() { auto dataDir = QDir(QFINDTESTDATA("testdata/CatPacks")).absolutePath(); auto fileName = FS::PathCombine(dataDir, "index.json"); auto fileinfo = QFileInfo(fileName); try { auto cat = JsonCatPack(fileinfo); QCOMPARE(cat.path(QDate(2023, 4, 12)), FS::PathCombine(fileinfo.path(), "oneDay.png")); QCOMPARE(cat.path(QDate(2023, 4, 11)), FS::PathCombine(fileinfo.path(), "maxwell.png")); QCOMPARE(cat.path(QDate(2023, 4, 13)), FS::PathCombine(fileinfo.path(), "maxwell.png")); QCOMPARE(cat.path(QDate(2023, 12, 21)), FS::PathCombine(fileinfo.path(), "christmas.png")); QCOMPARE(cat.path(QDate(2023, 12, 28)), FS::PathCombine(fileinfo.path(), "christmas.png")); QCOMPARE(cat.path(QDate(2023, 12, 29)), FS::PathCombine(fileinfo.path(), "newyear.png")); QCOMPARE(cat.path(QDate(2023, 12, 30)), FS::PathCombine(fileinfo.path(), "newyear2.png")); QCOMPARE(cat.path(QDate(2023, 12, 31)), FS::PathCombine(fileinfo.path(), "newyear2.png")); QCOMPARE(cat.path(QDate(2024, 1, 1)), FS::PathCombine(fileinfo.path(), "newyear2.png")); QCOMPARE(cat.path(QDate(2024, 1, 2)), FS::PathCombine(fileinfo.path(), "newyear.png")); QCOMPARE(cat.path(QDate(2024, 1, 3)), FS::PathCombine(fileinfo.path(), "newyear.png")); QCOMPARE(cat.path(QDate(2024, 1, 4)), FS::PathCombine(fileinfo.path(), "maxwell.png")); } catch (const Exception& e) { QFAIL(e.cause().toLatin1()); } } }; QTEST_GUILESS_MAIN(CatPackTest) #include "CatPack_test.moc" PrismLauncher-11.0.3/tests/Packwiz_test.cpp0000644000175100017510000000642515224505336020307 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include "modplatform/ModIndex.h" #include class PackwizTest : public QObject { Q_OBJECT private slots: // Files taken from https://github.com/packwiz/packwiz-example-pack void loadFromFile_Modrinth() { QString source = QFINDTESTDATA("testdata/Packwiz"); QDir index_dir(source); QString slug_mod("borderless-mining"); QString file_name = slug_mod + ".pw.toml"; QVERIFY(index_dir.entryList().contains(file_name)); auto metadata = Packwiz::V1::getIndexForMod(index_dir, slug_mod); QVERIFY(metadata.isValid()); QCOMPARE(metadata.name, "Borderless Mining"); QCOMPARE(metadata.filename, "borderless-mining-1.1.1+1.18.jar"); QCOMPARE(metadata.side, ModPlatform::Side::ClientSide); QCOMPARE(metadata.url, QUrl("https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar")); QCOMPARE(metadata.hash_format, "sha512"); QCOMPARE(metadata.hash, "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba6362306449" "9b3188d"); QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::MODRINTH); QCOMPARE(metadata.version(), "ug2qKTPR"); QCOMPARE(metadata.mod_id(), "kYq5qkSL"); } void loadFromFile_Curseforge() { QString source = QFINDTESTDATA("testdata/Packwiz"); QDir index_dir(source); QString name_mod("screenshot-to-clipboard-fabric.pw.toml"); QVERIFY(index_dir.entryList().contains(name_mod)); // Try without the .pw.toml at the end name_mod.chop(8); auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod); QVERIFY(metadata.isValid()); QCOMPARE(metadata.name, "Screenshot to Clipboard (Fabric)"); QCOMPARE(metadata.filename, "screenshot-to-clipboard-1.0.7-fabric.jar"); QCOMPARE(metadata.side, ModPlatform::Side::UniversalSide); QCOMPARE(metadata.url, QUrl("https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar")); QCOMPARE(metadata.hash_format, "murmur2"); QCOMPARE(metadata.hash, "1781245820"); QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::FLAME); QCOMPARE(metadata.file_id, 3509043); QCOMPARE(metadata.project_id, 327154); } }; QTEST_GUILESS_MAIN(PackwizTest) #include "Packwiz_test.moc" PrismLauncher-11.0.3/tests/GradleSpecifier_test.cpp0000644000175100017510000000416615224505336021727 0ustar runnerrunner#include #include class GradleSpecifierTest : public QObject { Q_OBJECT private slots: void initTestCase() {} void cleanupTestCase() {} void test_Positive_data() { QTest::addColumn("through"); QTest::newRow("3 parter") << "org.gradle.test.classifiers:service:1.0"; QTest::newRow("classifier") << "org.gradle.test.classifiers:service:1.0:jdk15"; QTest::newRow("jarextension") << "org.gradle.test.classifiers:service:1.0@jar"; QTest::newRow("jarboth") << "org.gradle.test.classifiers:service:1.0:jdk15@jar"; QTest::newRow("packxz") << "org.gradle.test.classifiers:service:1.0:jdk15@jar.pack.xz"; } void test_Positive() { QFETCH(QString, through); QString converted = GradleSpecifier(through).serialize(); QCOMPARE(converted, through); } void test_Path_data() { QTest::addColumn("spec"); QTest::addColumn("expected"); QTest::newRow("3 parter") << "group.id:artifact:1.0" << "group/id/artifact/1.0/artifact-1.0.jar"; QTest::newRow("doom") << "id.software:doom:1.666:demons@wad" << "id/software/doom/1.666/doom-1.666-demons.wad"; } void test_Path() { QFETCH(QString, spec); QFETCH(QString, expected); QString converted = GradleSpecifier(spec).toPath(); QCOMPARE(converted, expected); } void test_Negative_data() { QTest::addColumn("input"); QTest::newRow("too many :") << "org:gradle.test:class:::ifiers:service:1.0::"; QTest::newRow("nonsense") << "I like turtles"; QTest::newRow("empty string") << ""; QTest::newRow("missing version") << "herp.derp:artifact"; } void test_Negative() { QFETCH(QString, input); GradleSpecifier spec(input); QVERIFY(!spec.valid()); QCOMPARE(spec.serialize(), input); QCOMPARE(spec.toPath(), QString()); } }; QTEST_GUILESS_MAIN(GradleSpecifierTest) #include "GradleSpecifier_test.moc" PrismLauncher-11.0.3/tests/MojangVersionFormat_test.cpp0000644000175100017510000000356415224505336022632 0ustar runnerrunner#include #include #include class MojangVersionFormatTest : public QObject { Q_OBJECT static QJsonDocument readJson(const QString path) { QFile jsonFile(path); if (!jsonFile.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file" << jsonFile.fileName() << "for reading:" << jsonFile.errorString(); return QJsonDocument(); } auto data = jsonFile.readAll(); jsonFile.close(); return QJsonDocument::fromJson(data); } static void writeJson(const char* file, QJsonDocument doc) { QFile jsonFile(file); if (!jsonFile.open(QIODevice::WriteOnly | QIODevice::Text)) { qCritical() << "Failed to open file" << jsonFile.fileName() << "for writing:" << jsonFile.errorString(); return; } auto data = doc.toJson(QJsonDocument::Indented); qDebug() << QString::fromUtf8(data); jsonFile.write(data); jsonFile.close(); } private slots: void test_Through_Simple() { QJsonDocument doc = readJson(QFINDTESTDATA("testdata/Libraries/1.9-simple.json")); auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9-simple.json"); auto doc2 = MojangVersionFormat::versionFileToJson(vfile); writeJson("1.9-simple-passthorugh.json", doc2); QCOMPARE(doc.toJson(), doc2.toJson()); } void test_Through() { QJsonDocument doc = readJson(QFINDTESTDATA("testdata/Libraries/1.9.json")); auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9.json"); auto doc2 = MojangVersionFormat::versionFileToJson(vfile); writeJson("1.9-passthorugh.json", doc2); QCOMPARE(doc.toJson(), doc2.toJson()); } }; QTEST_GUILESS_MAIN(MojangVersionFormatTest) #include "MojangVersionFormat_test.moc" PrismLauncher-11.0.3/tests/ParseUtils_test.cpp0000644000175100017510000000177715224505336020777 0ustar runnerrunner#include #include class ParseUtilsTest : public QObject { Q_OBJECT private slots: void test_Through_data() { QTest::addColumn("timestamp"); const char* timestamps[] = { "2016-02-29T13:49:54+01:00", "2016-02-26T15:21:11+00:01", "2016-02-24T15:52:36+01:13", "2016-02-18T17:41:00+00:00", "2016-02-17T15:23:19+00:00", "2016-02-16T15:22:39+09:22", "2016-02-10T15:06:41+00:00", "2016-02-04T15:28:02-05:33" }; for (unsigned i = 0; i < (sizeof(timestamps) / sizeof(const char*)); i++) { QTest::newRow(timestamps[i]) << QString(timestamps[i]); } } void test_Through() { QFETCH(QString, timestamp); auto time_parsed = timeFromS3Time(timestamp); auto time_serialized = timeToS3Time(time_parsed); QCOMPARE(time_serialized, timestamp); } }; QTEST_GUILESS_MAIN(ParseUtilsTest) #include "ParseUtils_test.moc" PrismLauncher-11.0.3/tests/DataPackParse_test.cpp0000644000175100017510000000461615224505336021342 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include class DataPackParseTest : public QObject { Q_OBJECT private slots: void test_parseZIP() { QString source = QFINDTESTDATA("testdata/DataPackParse"); QString zip_dp = FS::PathCombine(source, "test_data_pack_boogaloo.zip"); DataPack pack{ QFileInfo(zip_dp) }; bool valid = DataPackUtils::processZIP(&pack); QVERIFY(pack.packFormat() == 4); QVERIFY(pack.description() == "Some data pack 2 boobgaloo"); QVERIFY(valid == true); } void test_parseFolder() { QString source = QFINDTESTDATA("testdata/DataPackParse"); QString folder_dp = FS::PathCombine(source, "test_folder"); DataPack pack{ QFileInfo(folder_dp) }; bool valid = DataPackUtils::processFolder(&pack); QVERIFY(pack.packFormat() == 10); QVERIFY(pack.description() == "Some data pack, maybe"); QVERIFY(valid == true); } void test_parseFolder2() { QString source = QFINDTESTDATA("testdata/DataPackParse"); QString folder_dp = FS::PathCombine(source, "another_test_folder"); DataPack pack{ QFileInfo(folder_dp) }; bool valid = DataPackUtils::process(&pack); QVERIFY(pack.packFormat() == 6); QVERIFY(pack.description() == "Some data pack three, leaves on the tree"); QVERIFY(valid == true); } }; QTEST_GUILESS_MAIN(DataPackParseTest) #include "DataPackParse_test.moc" PrismLauncher-11.0.3/tests/CMakeLists.txt0000644000175100017510000000475215224505336017675 0ustar runnerrunnerproject(tests) ecm_add_test(FileSystem_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME FileSystem) ecm_add_test(GZip_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME GZip) ecm_add_test(GradleSpecifier_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME GradleSpecifier) ecm_add_test(MojangVersionFormat_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME MojangVersionFormat) ecm_add_test(Library_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Library) ecm_add_test(ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ResourceFolderModel) ecm_add_test(ResourcePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ResourcePackParse) ecm_add_test(TexturePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME TexturePackParse) ecm_add_test(DataPackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME DataPackParse) ecm_add_test(ShaderPackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ShaderPackParse) ecm_add_test(WorldSaveParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME WorldSaveParse) ecm_add_test(ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ParseUtils) ecm_add_test(Task_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Task) ecm_add_test(INIFile_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME INIFile) ecm_add_test(JavaVersion_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME JavaVersion) ecm_add_test(Packwiz_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Packwiz) ecm_add_test(Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Index) ecm_add_test(Version_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Version) ecm_add_test(MetaComponentParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME MetaComponentParse) ecm_add_test(CatPack_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME CatPack) ecm_add_test(XmlLogs_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME XmlLogs) PrismLauncher-11.0.3/tests/ResourceFolderModel_test.cpp0000644000175100017510000002137515224505336022604 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include "BaseInstance.h" #include #include #include #define EXEC_UPDATE_TASK(EXEC, VERIFY) \ QEventLoop loop; \ \ connect(&model, &ResourceFolderModel::updateFinished, &loop, &QEventLoop::quit); \ \ QTimer expire_timer; \ expire_timer.callOnTimeout(&loop, &QEventLoop::quit); \ expire_timer.setSingleShot(true); \ expire_timer.start(4000); \ \ VERIFY(EXEC); \ loop.exec(); \ \ QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); \ expire_timer.stop(); \ \ disconnect(&model, nullptr, &loop, nullptr); class ResourceFolderModelTest : public QObject { Q_OBJECT private slots: // test for GH-1178 - install a folder with files to a mod list void test_1178() { // source QString source = QFINDTESTDATA("testdata/Resources/test_folder"); // sanity check QVERIFY(!source.endsWith('/')); auto verify = [](QString path) { QDir target_dir(FS::PathCombine(path, "test_folder")); QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // 1. test with no trailing / { QString folder = source; QTemporaryDir tempDir; QEventLoop loop; ModFolderModel m(tempDir.path(), nullptr, true, true); connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); QTimer expire_timer; expire_timer.callOnTimeout(&loop, &QEventLoop::quit); expire_timer.setSingleShot(true); expire_timer.start(4000); m.installResource(folder); loop.exec(); QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); expire_timer.stop(); verify(tempDir.path()); } // 2. test with trailing / { QString folder = source + '/'; QTemporaryDir tempDir; QEventLoop loop; ModFolderModel m(tempDir.path(), nullptr, true, true); connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); QTimer expire_timer; expire_timer.callOnTimeout(&loop, &QEventLoop::quit); expire_timer.setSingleShot(true); expire_timer.start(4000); m.installResource(folder); loop.exec(); QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); expire_timer.stop(); verify(tempDir.path()); } } void test_addFromWatch() { QString source = QFINDTESTDATA("testdata/Resources"); ModFolderModel model(source, nullptr, false, true); QCOMPARE(model.size(), 0); EXEC_UPDATE_TASK(model.startWatching(), ) for (auto mod : model.allMods()) qDebug() << mod->name(); QCOMPARE(model.size(), 4); model.stopWatching(); } void test_removeResource() { QString folder_resource = QFINDTESTDATA("testdata/Resources/test_folder"); QString file_mod = QFINDTESTDATA("testdata/Resources/supercoolmod.jar"); QTemporaryDir tmp; ResourceFolderModel model(QDir(tmp.path()), nullptr, false, false); QCOMPARE(model.size(), 0); { EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY) } QCOMPARE(model.size(), 1); qDebug() << "Added first mod."; { EXEC_UPDATE_TASK(model.startWatching(), ) } QCOMPARE(model.size(), 1); qDebug() << "Started watching the temp folder."; { EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY) } QCOMPARE(model.size(), 2); qDebug() << "Added second mod."; { EXEC_UPDATE_TASK(model.uninstallResource("supercoolmod.jar"), QVERIFY); } QCOMPARE(model.size(), 1); qDebug() << "Removed first mod."; QString mod_file_name{ model.at(0).fileinfo().fileName() }; QVERIFY(!mod_file_name.isEmpty()); { EXEC_UPDATE_TASK(model.uninstallResource(mod_file_name), QVERIFY); } QCOMPARE(model.size(), 0); qDebug() << "Removed second mod."; model.stopWatching(); } void test_enable_disable() { QString folder_resource = QFINDTESTDATA("testdata/Resources/test_folder"); QString file_mod = QFINDTESTDATA("testdata/Resources/supercoolmod.jar"); QTemporaryDir tmp; ResourceFolderModel model(tmp.path(), nullptr, false, false); QCOMPARE(model.size(), 0); { EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY) } { EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY) } for (auto res : model.allResources()) qDebug() << res->name(); QCOMPARE(model.size(), 2); auto& res_1 = model.at(0).type() != ResourceType::FOLDER ? model.at(0) : model.at(1); auto& res_2 = model.at(0).type() == ResourceType::FOLDER ? model.at(0) : model.at(1); auto id_1 = res_1.internal_id(); auto id_2 = res_2.internal_id(); bool initial_enabled_res_2 = res_2.enabled(); bool initial_enabled_res_1 = res_1.enabled(); QVERIFY(res_1.type() != ResourceType::FOLDER && res_1.type() != ResourceType::UNKNOWN); qDebug() << "res_1 is of the correct type."; QVERIFY(res_1.enabled()); qDebug() << "res_1 is initially enabled."; QVERIFY(res_1.enable(EnableAction::TOGGLE)); QVERIFY(res_1.enabled() == !initial_enabled_res_1); qDebug() << "res_1 got successfully toggled."; QVERIFY(res_1.enable(EnableAction::TOGGLE)); qDebug() << "res_1 got successfully toggled again."; QVERIFY(res_1.enabled() == initial_enabled_res_1); QVERIFY(res_1.internal_id() == id_1); qDebug() << "res_1 got back to its initial state."; QVERIFY(!res_2.enable(initial_enabled_res_2 ? EnableAction::ENABLE : EnableAction::DISABLE)); QVERIFY(res_2.enabled() == initial_enabled_res_2); QVERIFY(res_2.internal_id() == id_2); } }; QTEST_GUILESS_MAIN(ResourceFolderModelTest) #include "ResourceFolderModel_test.moc" PrismLauncher-11.0.3/tests/XmlLogs_test.cpp0000644000175100017510000001344115224505336020260 0ustar runnerrunner// SPDX-FileCopyrightText: 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include class XmlLogParseTest : public QObject { Q_OBJECT private slots: void parseXml_data() { QString source = QFINDTESTDATA("testdata/TestLogs"); QString shortXml = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5.xml.log"))); QString shortText = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5.text.log"))); QStringList shortTextLevels_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5-levels.txt"))) .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); QList shortTextLevels; shortTextLevels.reserve(24); std::transform(shortTextLevels_s.cbegin(), shortTextLevels_s.cend(), std::back_inserter(shortTextLevels), [](const QString& line) { return MessageLevel::fromName(line.trimmed()); }); QString longXml = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-forge.xml.log"))); QString longText = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-forge.text.log"))); QStringList longTextLevels_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-levels.txt"))) .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); QStringList longTextLevelsXml_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-xml-levels.txt"))) .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); QList longTextLevelsPlain; longTextLevelsPlain.reserve(974); std::transform(longTextLevels_s.cbegin(), longTextLevels_s.cend(), std::back_inserter(longTextLevelsPlain), [](const QString& line) { return MessageLevel::fromName(line.trimmed()); }); QList longTextLevelsXml; longTextLevelsXml.reserve(896); std::transform(longTextLevelsXml_s.cbegin(), longTextLevelsXml_s.cend(), std::back_inserter(longTextLevelsXml), [](const QString& line) { return MessageLevel::fromName(line.trimmed()); }); QTest::addColumn("log"); QTest::addColumn("num_entries"); QTest::addColumn>("entry_levels"); QTest::newRow("short-vanilla-plain") << shortText << 25 << shortTextLevels; QTest::newRow("short-vanilla-xml") << shortXml << 25 << shortTextLevels; QTest::newRow("long-forge-plain") << longText << 945 << longTextLevelsPlain; QTest::newRow("long-forge-xml") << longXml << 869 << longTextLevelsXml; } void parseXml() { QFETCH(QString, log); QFETCH(int, num_entries); QFETCH(QList, entry_levels); QList> entries = {}; QBENCHMARK { entries = parseLines(log.split(QRegularExpression("\n|\r\n|\r"))); } QCOMPARE(entries.length(), num_entries); QList levels = {}; std::transform(entries.cbegin(), entries.cend(), std::back_inserter(levels), [](std::pair entry) { return entry.first; }); QCOMPARE(levels, entry_levels); } private: LogParser m_parser; QList> parseLines(const QStringList& lines) { QList> out; MessageLevel last = MessageLevel::Unknown; for (const auto& line : lines) { m_parser.appendLine(line); auto items = m_parser.parseAvailable(); for (const auto& item : items) { if (std::holds_alternative(item)) { auto entry = std::get(item); auto msg = QString("[%1] [%2/%3] [%4]: %5") .arg(entry.timestamp.toString("HH:mm:ss")) .arg(entry.thread) .arg(entry.levelText) .arg(entry.logger) .arg(entry.message); out.append(std::make_pair(entry.level, msg)); last = entry.level; } else if (std::holds_alternative(item)) { auto msg = std::get(item).message; auto level = LogParser::guessLevel(msg, last); out.append(std::make_pair(level, msg)); last = level; } } } return out; } }; QTEST_GUILESS_MAIN(XmlLogParseTest) #include "XmlLogs_test.moc" PrismLauncher-11.0.3/tests/testdata/0000755000175100017510000000000015224505336016736 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/Libraries/0000755000175100017510000000000015224505336020652 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/Libraries/lib-simple.json0000644000175100017510000000057615224505336023612 0ustar runnerrunner{ "downloads": { "artifact": { "path": "com/paulscode/codecwav/20101023/codecwav-20101023.jar", "sha1": "12f031cfe88fef5c1dd36c563c0a3a69bd7261da", "size": 5618, "url": "https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar" } }, "name": "com.paulscode:codecwav:20101023" } PrismLauncher-11.0.3/tests/testdata/Libraries/lib-native-arch.json0000644000175100017510000000316015224505336024512 0ustar runnerrunner{ "downloads": { "classifiers": { "natives-osx": { "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-osx.jar", "sha1": "62503ee712766cf77f97252e5902786fd834b8c5", "size": 418331, "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-osx.jar" }, "natives-windows-32": { "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar", "sha1": "7c6affe439099806a4f552da14c42f9d643d8b23", "size": 386792, "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar" }, "natives-windows-64": { "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar", "sha1": "39d0c3d363735b4785598e0e7fbf8297c706a9f9", "size": 463390, "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar" } } }, "extract": { "exclude": [ "META-INF/" ] }, "name": "tv.twitch:twitch-platform:5.16", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows-${arch}" }, "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "linux" } } ] } PrismLauncher-11.0.3/tests/testdata/Libraries/codecwav-20101023.jar0000644000175100017510000000002115224505336024022 0ustar runnerrunnerdummy test file. PrismLauncher-11.0.3/tests/testdata/Libraries/lib-native.json0000644000175100017510000000444615224505336023607 0ustar runnerrunner{ "downloads": { "artifact": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar", "sha1": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33", "size": 22, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar" }, "classifiers": { "natives-linux": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar", "sha1": "931074f46c795d2f7b30ed6395df5715cfd7675b", "size": 578680, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar" }, "natives-osx": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar", "sha1": "bcab850f8f487c3f4c4dbabde778bb82bd1a40ed", "size": 426822, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar" }, "natives-windows": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar", "sha1": "b84d5102b9dbfabfeb5e43c7e2828d98a7fc80e0", "size": 613748, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar" } } }, "extract": { "exclude": [ "META-INF/" ] }, "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" }, "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] } PrismLauncher-11.0.3/tests/testdata/Libraries/1.9.json0000644000175100017510000005674115224505336022071 0ustar runnerrunner{ "assetIndex": { "id": "1.9", "sha1": "cde65b47a43f638653ab1da3848b53f8a7477b16", "size": 136916, "totalSize": 119917473, "url": "https://launchermeta.mojang.com/mc-staging/assets/1.9/cde65b47a43f638653ab1da3848b53f8a7477b16/1.9.json" }, "assets": "1.9", "downloads": { "client": { "sha1": "2f67dfe8953299440d1902f9124f0f2c3a2c940f", "size": 8697592, "url": "https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar" }, "server": { "sha1": "b4d449cf2918e0f3bd8aa18954b916a4d1880f0d", "size": 8848015, "url": "https://launcher.mojang.com/mc/game/1.9/server/b4d449cf2918e0f3bd8aa18954b916a4d1880f0d/server.jar" } }, "id": "1.9", "libraries": [ { "downloads": { "artifact": { "path": "oshi-project/oshi-core/1.1/oshi-core-1.1.jar", "sha1": "9ddf7b048a8d701be231c0f4f95fd986198fd2d8", "size": 30973, "url": "https://libraries.minecraft.net/oshi-project/oshi-core/1.1/oshi-core-1.1.jar" } }, "name": "oshi-project:oshi-core:1.1" }, { "downloads": { "artifact": { "path": "net/java/dev/jna/jna/3.4.0/jna-3.4.0.jar", "sha1": "803ff252fedbd395baffd43b37341dc4a150a554", "size": 1008730, "url": "https://libraries.minecraft.net/net/java/dev/jna/jna/3.4.0/jna-3.4.0.jar" } }, "name": "net.java.dev.jna:jna:3.4.0" }, { "downloads": { "artifact": { "path": "net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar", "sha1": "e3f70017be8100d3d6923f50b3d2ee17714e9c13", "size": 913436, "url": "https://libraries.minecraft.net/net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar" } }, "name": "net.java.dev.jna:platform:3.4.0" }, { "downloads": { "artifact": { "path": "com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar", "sha1": "63d216a9311cca6be337c1e458e587f99d382b84", "size": 1634692, "url": "https://libraries.minecraft.net/com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar" } }, "name": "com.ibm.icu:icu4j-core-mojang:51.2" }, { "downloads": { "artifact": { "path": "net/sf/jopt-simple/jopt-simple/4.6/jopt-simple-4.6.jar", "sha1": "306816fb57cf94f108a43c95731b08934dcae15c", "size": 62477, "url": "https://libraries.minecraft.net/net/sf/jopt-simple/jopt-simple/4.6/jopt-simple-4.6.jar" } }, "name": "net.sf.jopt-simple:jopt-simple:4.6" }, { "downloads": { "artifact": { "path": "com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar", "sha1": "c73b5636faf089d9f00e8732a829577de25237ee", "size": 103871, "url": "https://libraries.minecraft.net/com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar" } }, "name": "com.paulscode:codecjorbis:20101023" }, { "downloads": { "artifact": { "path": "com/paulscode/codecwav/20101023/codecwav-20101023.jar", "sha1": "12f031cfe88fef5c1dd36c563c0a3a69bd7261da", "size": 5618, "url": "https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar" } }, "name": "com.paulscode:codecwav:20101023" }, { "downloads": { "artifact": { "path": "com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar", "sha1": "5c5e304366f75f9eaa2e8cca546a1fb6109348b3", "size": 21679, "url": "https://libraries.minecraft.net/com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar" } }, "name": "com.paulscode:libraryjavasound:20101123" }, { "downloads": { "artifact": { "path": "com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar", "sha1": "73e80d0794c39665aec3f62eee88ca91676674ef", "size": 18981, "url": "https://libraries.minecraft.net/com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar" } }, "name": "com.paulscode:librarylwjglopenal:20100824" }, { "downloads": { "artifact": { "path": "com/paulscode/soundsystem/20120107/soundsystem-20120107.jar", "sha1": "419c05fe9be71f792b2d76cfc9b67f1ed0fec7f6", "size": 65020, "url": "https://libraries.minecraft.net/com/paulscode/soundsystem/20120107/soundsystem-20120107.jar" } }, "name": "com.paulscode:soundsystem:20120107" }, { "downloads": { "artifact": { "path": "io/netty/netty-all/4.0.23.Final/netty-all-4.0.23.Final.jar", "sha1": "0294104aaf1781d6a56a07d561e792c5d0c95f45", "size": 1779991, "url": "https://libraries.minecraft.net/io/netty/netty-all/4.0.23.Final/netty-all-4.0.23.Final.jar" } }, "name": "io.netty:netty-all:4.0.23.Final" }, { "downloads": { "artifact": { "path": "com/google/guava/guava/17.0/guava-17.0.jar", "sha1": "9c6ef172e8de35fd8d4d8783e4821e57cdef7445", "size": 2243036, "url": "https://libraries.minecraft.net/com/google/guava/guava/17.0/guava-17.0.jar" } }, "name": "com.google.guava:guava:17.0" }, { "downloads": { "artifact": { "path": "org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar", "sha1": "90a3822c38ec8c996e84c16a3477ef632cbc87a3", "size": 412739, "url": "https://libraries.minecraft.net/org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar" } }, "name": "org.apache.commons:commons-lang3:3.3.2" }, { "downloads": { "artifact": { "path": "commons-io/commons-io/2.4/commons-io-2.4.jar", "sha1": "b1b6ea3b7e4aa4f492509a4952029cd8e48019ad", "size": 185140, "url": "https://libraries.minecraft.net/commons-io/commons-io/2.4/commons-io-2.4.jar" } }, "name": "commons-io:commons-io:2.4" }, { "downloads": { "artifact": { "path": "commons-codec/commons-codec/1.9/commons-codec-1.9.jar", "sha1": "9ce04e34240f674bc72680f8b843b1457383161a", "size": 263965, "url": "https://libraries.minecraft.net/commons-codec/commons-codec/1.9/commons-codec-1.9.jar" } }, "name": "commons-codec:commons-codec:1.9" }, { "downloads": { "artifact": { "path": "net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar", "sha1": "39c7796b469a600f72380316f6b1f11db6c2c7c4", "size": 208338, "url": "https://libraries.minecraft.net/net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar" } }, "name": "net.java.jinput:jinput:2.0.5" }, { "downloads": { "artifact": { "path": "net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar", "sha1": "e12fe1fda814bd348c1579329c86943d2cd3c6a6", "size": 7508, "url": "https://libraries.minecraft.net/net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar" } }, "name": "net.java.jutils:jutils:1.0.0" }, { "downloads": { "artifact": { "path": "com/google/code/gson/gson/2.2.4/gson-2.2.4.jar", "sha1": "a60a5e993c98c864010053cb901b7eab25306568", "size": 190432, "url": "https://libraries.minecraft.net/com/google/code/gson/gson/2.2.4/gson-2.2.4.jar" } }, "name": "com.google.code.gson:gson:2.2.4" }, { "downloads": { "artifact": { "path": "com/mojang/authlib/1.5.22/authlib-1.5.22.jar", "sha1": "afaa8f6df976fcb5520e76ef1d5798c9e6b5c0b2", "size": 64539, "url": "https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar" } }, "name": "com.mojang:authlib:1.5.22" }, { "downloads": { "artifact": { "path": "com/mojang/realms/1.8.4/realms-1.8.4.jar", "sha1": "15f8dc326c97a96dee6e65392e145ad6d1cb46cb", "size": 1131574, "url": "https://libraries.minecraft.net/com/mojang/realms/1.8.4/realms-1.8.4.jar" } }, "name": "com.mojang:realms:1.8.4" }, { "downloads": { "artifact": { "path": "org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar", "sha1": "a698750c16740fd5b3871425f4cb3bbaa87f529d", "size": 365552, "url": "https://libraries.minecraft.net/org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar" } }, "name": "org.apache.commons:commons-compress:1.8.1" }, { "downloads": { "artifact": { "path": "org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar", "sha1": "18f4247ff4572a074444572cee34647c43e7c9c7", "size": 589512, "url": "https://libraries.minecraft.net/org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar" } }, "name": "org.apache.httpcomponents:httpclient:4.3.3" }, { "downloads": { "artifact": { "path": "commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar", "sha1": "f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f", "size": 62050, "url": "https://libraries.minecraft.net/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar" } }, "name": "commons-logging:commons-logging:1.1.3" }, { "downloads": { "artifact": { "path": "org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar", "sha1": "31fbbff1ddbf98f3aa7377c94d33b0447c646b6e", "size": 282269, "url": "https://libraries.minecraft.net/org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar" } }, "name": "org.apache.httpcomponents:httpcore:4.3.2" }, { "downloads": { "artifact": { "path": "org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar", "sha1": "1dd66e68cccd907880229f9e2de1314bd13ff785", "size": 108161, "url": "https://libraries.minecraft.net/org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar" } }, "name": "org.apache.logging.log4j:log4j-api:2.0-beta9" }, { "downloads": { "artifact": { "path": "org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar", "sha1": "678861ba1b2e1fccb594bb0ca03114bb05da9695", "size": 681134, "url": "https://libraries.minecraft.net/org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar" } }, "name": "org.apache.logging.log4j:log4j-core:2.0-beta9" }, { "downloads": { "artifact": { "path": "org/lwjgl/lwjgl/lwjgl/2.9.4-nightly-20150209/lwjgl-2.9.4-nightly-20150209.jar", "sha1": "697517568c68e78ae0b4544145af031c81082dfe", "size": 1047168, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl/2.9.4-nightly-20150209/lwjgl-2.9.4-nightly-20150209.jar" } }, "name": "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209", "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] }, { "downloads": { "artifact": { "path": "org/lwjgl/lwjgl/lwjgl_util/2.9.4-nightly-20150209/lwjgl_util-2.9.4-nightly-20150209.jar", "sha1": "d51a7c040a721d13efdfbd34f8b257b2df882ad0", "size": 173887, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl_util/2.9.4-nightly-20150209/lwjgl_util-2.9.4-nightly-20150209.jar" } }, "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209", "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] }, { "downloads": { "artifact": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar", "sha1": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33", "size": 22, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar" }, "classifiers": { "natives-linux": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar", "sha1": "931074f46c795d2f7b30ed6395df5715cfd7675b", "size": 578680, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar" }, "natives-osx": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar", "sha1": "bcab850f8f487c3f4c4dbabde778bb82bd1a40ed", "size": 426822, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar" }, "natives-windows": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar", "sha1": "b84d5102b9dbfabfeb5e43c7e2828d98a7fc80e0", "size": 613748, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar" } } }, "extract": { "exclude": [ "META-INF/" ] }, "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" }, "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] }, { "downloads": { "artifact": { "path": "org/lwjgl/lwjgl/lwjgl/2.9.2-nightly-20140822/lwjgl-2.9.2-nightly-20140822.jar", "sha1": "7707204c9ffa5d91662de95f0a224e2f721b22af", "size": 1045632, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl/2.9.2-nightly-20140822/lwjgl-2.9.2-nightly-20140822.jar" } }, "name": "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822", "rules": [ { "action": "allow", "os": { "name": "osx" } } ] }, { "downloads": { "artifact": { "path": "org/lwjgl/lwjgl/lwjgl_util/2.9.2-nightly-20140822/lwjgl_util-2.9.2-nightly-20140822.jar", "sha1": "f0e612c840a7639c1f77f68d72a28dae2f0c8490", "size": 173887, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl_util/2.9.2-nightly-20140822/lwjgl_util-2.9.2-nightly-20140822.jar" } }, "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822", "rules": [ { "action": "allow", "os": { "name": "osx" } } ] }, { "downloads": { "classifiers": { "natives-linux": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-linux.jar", "sha1": "d898a33b5d0a6ef3fed3a4ead506566dce6720a5", "size": 578539, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-linux.jar" }, "natives-osx": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-osx.jar", "sha1": "79f5ce2fea02e77fe47a3c745219167a542121d7", "size": 468116, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-osx.jar" }, "natives-windows": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-windows.jar", "sha1": "78b2a55ce4dc29c6b3ec4df8ca165eba05f9b341", "size": 613680, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-windows.jar" } } }, "extract": { "exclude": [ "META-INF/" ] }, "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" }, "rules": [ { "action": "allow", "os": { "name": "osx" } } ] }, { "downloads": { "classifiers": { "natives-linux": { "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar", "sha1": "7ff832a6eb9ab6a767f1ade2b548092d0fa64795", "size": 10362, "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar" }, "natives-osx": { "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-osx.jar", "sha1": "53f9c919f34d2ca9de8c51fc4e1e8282029a9232", "size": 12186, "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-osx.jar" }, "natives-windows": { "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-windows.jar", "sha1": "385ee093e01f587f30ee1c8a2ee7d408fd732e16", "size": 155179, "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-windows.jar" } } }, "extract": { "exclude": [ "META-INF/" ] }, "name": "net.java.jinput:jinput-platform:2.0.5", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" } } ], "mainClass": "net.minecraft.client.main.Main", "minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}", "minimumLauncherVersion": 18, "releaseTime": "2016-02-29T13:49:54+00:00", "time": "2016-03-01T13:14:53+00:00", "type": "release" } PrismLauncher-11.0.3/tests/testdata/Libraries/1.9-simple.json0000644000175100017510000001256615224505336023355 0ustar runnerrunner{ "assets": "1.9", "id": "1.9", "libraries": [ { "name": "oshi-project:oshi-core:1.1" }, { "name": "net.java.dev.jna:jna:3.4.0" }, { "name": "net.java.dev.jna:platform:3.4.0" }, { "name": "com.ibm.icu:icu4j-core-mojang:51.2" }, { "name": "net.sf.jopt-simple:jopt-simple:4.6" }, { "name": "com.paulscode:codecjorbis:20101023" }, { "name": "com.paulscode:codecwav:20101023" }, { "name": "com.paulscode:libraryjavasound:20101123" }, { "name": "com.paulscode:librarylwjglopenal:20100824" }, { "name": "com.paulscode:soundsystem:20120107" }, { "name": "io.netty:netty-all:4.0.23.Final" }, { "name": "com.google.guava:guava:17.0" }, { "name": "org.apache.commons:commons-lang3:3.3.2" }, { "name": "commons-io:commons-io:2.4" }, { "name": "commons-codec:commons-codec:1.9" }, { "name": "net.java.jinput:jinput:2.0.5" }, { "name": "net.java.jutils:jutils:1.0.0" }, { "name": "com.google.code.gson:gson:2.2.4" }, { "name": "com.mojang:authlib:1.5.22" }, { "name": "com.mojang:realms:1.8.4" }, { "name": "org.apache.commons:commons-compress:1.8.1" }, { "name": "org.apache.httpcomponents:httpclient:4.3.3" }, { "name": "commons-logging:commons-logging:1.1.3" }, { "name": "org.apache.httpcomponents:httpcore:4.3.2" }, { "name": "org.apache.logging.log4j:log4j-api:2.0-beta9" }, { "name": "org.apache.logging.log4j:log4j-core:2.0-beta9" }, { "name": "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209", "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] }, { "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209", "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] }, { "extract": { "exclude": [ "META-INF/" ] }, "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" }, "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] }, { "name": "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822", "rules": [ { "action": "allow", "os": { "name": "osx" } } ] }, { "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822", "rules": [ { "action": "allow", "os": { "name": "osx" } } ] }, { "extract": { "exclude": [ "META-INF/" ] }, "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" }, "rules": [ { "action": "allow", "os": { "name": "osx" } } ] }, { "extract": { "exclude": [ "META-INF/" ] }, "name": "net.java.jinput:jinput-platform:2.0.5", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" } } ], "mainClass": "net.minecraft.client.main.Main", "minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}", "minimumLauncherVersion": 18, "releaseTime": "2016-02-29T13:49:54+00:00", "time": "2016-03-01T13:14:53+00:00", "type": "release" } PrismLauncher-11.0.3/tests/testdata/Libraries/testname-testversion-linux-32.jar0000644000175100017510000000002115224505336027123 0ustar runnerrunnerdummy test file. PrismLauncher-11.0.3/tests/testdata/Packwiz/0000755000175100017510000000000015224505336020346 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/Packwiz/screenshot-to-clipboard-fabric.pw.toml0000644000175100017510000000050315224505336027644 0ustar runnerrunnername = "Screenshot to Clipboard (Fabric)" filename = "screenshot-to-clipboard-1.0.7-fabric.jar" side = "both" [download] url = "https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar" hash-format = "murmur2" hash = "1781245820" [update] [update.curseforge] file-id = 3509043 project-id = 327154 PrismLauncher-11.0.3/tests/testdata/Packwiz/borderless-mining.pw.toml0000644000175100017510000000065715224505336025323 0ustar runnerrunnername = "Borderless Mining" filename = "borderless-mining-1.1.1+1.18.jar" side = "client" [download] url = "https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar" hash-format = "sha512" hash = "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d" [update] [update.modrinth] mod-id = "kYq5qkSL" version = "ug2qKTPR" PrismLauncher-11.0.3/tests/testdata/TestLogs/0000755000175100017510000000000015224505336020502 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/TestLogs/TerraFirmaGreg-Modern-levels.txt0000644000175100017510000001144315224505336026621 0ustar runnerrunnerUNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN INFO INFO INFO INFO INFO INFO INFO WARN WARN WARN WARN WARN INFO ERROR INFO ERROR ERROR ERROR INFO INFO INFO WARN INFO INFO INFO INFO WARN INFO INFO INFO WARN WARN INFO WARN WARN WARN WARN WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN INFO INFO WARN WARN WARN INFO WARN INFO INFO WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN INFO INFO WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN INFO INFO WARN INFO WARN WARN INFO INFO INFO INFO INFO WARN INFO INFO INFO WARN INFO WARN WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO WARN INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN INFO UNKNOWN UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN INFO INFO WARN WARN WARN INFO INFO INFO INFO INFO INFO UNKNOWN INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR INFO ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO WARN ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR INFO INFO INFO WARN ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR WARN ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO ERROR INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN WARN WARN WARN WARN WARN WARN UNKNOWN INFO UNKNOWN INFO ERROR INFO INFO INFO INFO UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO WARN WARN WARN WARN WARN WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO ERROR ERROR ERROR ERROR INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN PrismLauncher-11.0.3/tests/testdata/TestLogs/TerraFirmaGreg-Modern-xml-levels.txt0000644000175100017510000001045315224505336027417 0ustar runnerrunnerUNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN WARN WARN INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO ERROR INFO ERROR ERROR ERROR INFO INFO INFO WARN INFO INFO INFO INFO WARN INFO INFO INFO WARN WARN INFO WARN WARN WARN WARN WARN INFO WARN WARN INFO INFO WARN WARN WARN INFO WARN INFO INFO WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN INFO INFO WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN INFO INFO WARN INFO WARN WARN INFO INFO INFO INFO INFO WARN INFO INFO INFO WARN INFO WARN WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO WARN INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO WARN WARN INFO INFO INFO INFO UNKNOWN UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN INFO INFO WARN WARN INFO INFO INFO INFO INFO INFO UNKNOWN INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO ERROR INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO WARN WARN INFO INFO INFO INFO ERROR INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN WARN WARN UNKNOWN INFO UNKNOWN INFO ERROR INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO WARN WARN WARN WARN WARN WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO ERROR ERROR ERROR ERROR INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO PrismLauncher-11.0.3/tests/testdata/TestLogs/vanilla-1.21.5-levels.txt0000644000175100017510000000017515224505336024706 0ustar runnerrunnerINFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO PrismLauncher-11.0.3/tests/testdata/TestLogs/vanilla-1.21.5.xml.log0000644000175100017510000001273415224505336024163 0ustar runnerrunner PrismLauncher-11.0.3/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.xml.log0000644000175100017510000077213615224505336027207 0ustar runnerrunnerChecking: MC_SLIM Checking: MERGED_MAPPINGS Checking: MAPPINGS Checking: MC_EXTRA Checking: MOJMAPS Checking: PATCHED Checking: MC_SRG , --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --launchTarget, forgeclient, --fml.forgeVersion, 47.2.6, --fml.mcVersion, 1.20.1, --fml.forgeGroup, net.minecraftforge, --fml.mcpVersion, 20230612.114412, --width, 854, --height, 480]]]> , --username, Ryexandrite, --assetIndex, 5, --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --width, 854, --height, 480]]]> Outdated (Target version: "v2.5.4")]]> [Mouse Tweaks] Main.initialize() [Mouse Tweaks] Initialized. Outdated (Target version: "v1.0.5")]]> [ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) (ModelBakery.java:150) at TRANSFORMER/minecraft@1.20.1/net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) at java.base/java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) at java.base/java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ]]> (ModelBakery.java:159) at TRANSFORMER/minecraft@1.20.1/net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) at java.base/java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) at java.base/java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ]]> Reloading Dynamic Lights Create Crafts & Additions Initialized! [ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) PrismLauncher-11.0.3/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.text.log0000644000175100017510000031134215224505336027357 0ustar runnerrunnerChecking: MC_SLIM Checking: MERGED_MAPPINGS Checking: MAPPINGS Checking: MC_EXTRA Checking: MOJMAPS Checking: PATCHED Checking: MC_SRG 2025-04-18 12:47:23,932 main WARN Advanced terminal features are not available in this environment [12:47:24] [main/INFO] [cp.mo.mo.Launcher/MODLAUNCHER]: ModLauncher running: args [--username, Ryexandrite, --version, 1.20.1, --gameDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft, --assetsDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/assets, --assetIndex, 5, --uuid, , --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --launchTarget, forgeclient, --fml.forgeVersion, 47.2.6, --fml.mcVersion, 1.20.1, --fml.forgeGroup, net.minecraftforge, --fml.mcpVersion, 20230612.114412, --width, 854, --height, 480] [12:47:24] [main/INFO] [cp.mo.mo.Launcher/MODLAUNCHER]: ModLauncher 10.0.9+10.0.9+main.dcd20f30 starting: java version 17.0.8 by Microsoft; OS Linux arch amd64 version 6.6.85 [12:47:24] [main/INFO] [ne.mi.fm.lo.ImmediateWindowHandler/]: Loading ImmediateWindowProvider fmlearlywindow [12:47:24] [main/INFO] [EARLYDISPLAY/]: Trying GL version 4.6 [12:47:24] [main/INFO] [EARLYDISPLAY/]: Requested GL version 4.6 got version 4.6 [12:47:24] [main/INFO] [mixin/]: SpongePowered MIXIN Subsystem Version=0.8.5 Source=union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/org/spongepowered/mixin/0.8.5/mixin-0.8.5.jar%23140!/ Service=ModLauncher Env=CLIENT [12:47:24] [pool-2-thread-1/INFO] [EARLYDISPLAY/]: GL info: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) GL version 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f), AMD [12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/fmlcore/1.20.1-47.2.6/fmlcore-1.20.1-47.2.6.jar is missing mods.toml file [12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/javafmllanguage/1.20.1-47.2.6/javafmllanguage-1.20.1-47.2.6.jar is missing mods.toml file [12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/lowcodelanguage/1.20.1-47.2.6/lowcodelanguage-1.20.1-47.2.6.jar is missing mods.toml file [12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/mclanguage/1.20.1-47.2.6/mclanguage-1.20.1-47.2.6.jar is missing mods.toml file [12:47:25] [main/WARN] [ne.mi.ja.se.JarSelector/]: Attempted to select two dependency jars from JarJar which have the same identification: Mod File: and Mod File: . Using Mod File: [12:47:25] [main/INFO] [ne.mi.fm.lo.mo.JarInJarDependencyLocator/]: Found 28 dependencies adding them to mods collection [12:47:28] [main/ERROR] [mixin/]: Mixin config dynamiclightsreforged.mixins.json does not specify "minVersion" property [12:47:28] [main/INFO] [mixin/]: Compatibility level set to JAVA_17 [12:47:28] [main/ERROR] [mixin/]: Mixin config mixins.satin.client.json does not specify "minVersion" property [12:47:28] [main/ERROR] [mixin/]: Mixin config firstperson.mixins.json does not specify "minVersion" property [12:47:28] [main/ERROR] [mixin/]: Mixin config yacl.mixins.json does not specify "minVersion" property [12:47:28] [main/INFO] [cp.mo.mo.LaunchServiceHandler/MODLAUNCHER]: Launching target 'forgeclient' with arguments [--version, 1.20.1, --gameDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft, --assetsDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/assets, --uuid, , --username, Ryexandrite, --assetIndex, 5, --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --width, 854, --height, 480] [12:47:28] [main/INFO] [co.ab.sa.co.Saturn/]: Loaded Saturn config file with 4 configurable options [12:47:28] [main/INFO] [ModernFix/]: Loaded configuration file for ModernFix 5.18.1+mc1.20.1: 83 options available, 1 override(s) found [12:47:28] [main/WARN] [ModernFix/]: Option 'mixin.perf.thread_priorities' overriden (by mods [smoothboot]) to 'false' [12:47:28] [main/INFO] [ModernFix/]: Applying Nashorn fix [12:47:28] [main/INFO] [ModernFix/]: Applied Forge config corruption patch [12:47:28] [main/INFO] [fpsreducer/]: OptiFine was NOT detected. [12:47:28] [main/INFO] [fpsreducer/]: OptiFabric was NOT detected. [12:47:28] [main/WARN] [EmbeddiumConfig/]: Mod 'tfc' attempted to override option 'mixin.features.fast_biome_colors', which doesn't exist, ignoring [12:47:28] [main/INFO] [Embeddium/]: Loaded configuration file for Embeddium: 205 options available, 3 override(s) found [12:47:28] [main/INFO] [Embeddium-GraphicsAdapterProbe/]: Searching for graphics cards... [12:47:28] [main/INFO] [Embeddium-GraphicsAdapterProbe/]: Found graphics card: GraphicsAdapterInfo[vendor=AMD, name=Navi 10 [Radeon RX 5600 OEM/5600 XT / 5700/5700 XT], version=unknown] [12:47:28] [main/WARN] [Embeddium-Workarounds/]: Sodium has applied one or more workarounds to prevent crashes or other issues on your system: [NO_ERROR_CONTEXT_UNSUPPORTED] [12:47:28] [main/WARN] [Embeddium-Workarounds/]: This is not necessarily an issue, but it may result in certain features or optimizations being disabled. You can sometimes fix these issues by upgrading your graphics driver. [12:47:28] [main/INFO] [Radium Config/]: Loaded configuration file for Radium: 125 options available, 7 override(s) found [12:47:28] [main/WARN] [mixin/]: Reference map 'carpeted-common-refmap.json' for carpeted-common.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:28] [main/WARN] [mixin/]: Reference map 'carpeted-forge-refmap.json' for carpeted.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:28] [main/WARN] [mixin/]: Reference map 'emi-forge-refmap.json' for emi-forge.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:28] [main/WARN] [mixin/]: Reference map 'ftb-filter-system-common-refmap.json' for ftbfiltersystem-common.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:28] [main/WARN] [mixin/]: Reference map 'ftb-filter-system-forge-refmap.json' for ftbfiltersystem.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:28] [main/INFO] [Puzzles Lib/]: Loading 160 mods: - additionalplacements 1.8.0 - ae2 15.2.13 - ae2insertexportcard 1.20.1-1.3.0 - ae2netanalyser 1.20-1.0.6-forge - ae2wtlib 15.2.3-forge - aiimprovements 0.5.2 - ambientsounds 6.1.1 - architectury 9.2.14 - astikorcarts 1.1.8 - attributefix 21.0.4 - balm 7.3.9 \-- kuma_api 20.1.8 - barrels_2012 2.1 - betterf3 7.0.2 - betterfoliage 5.0.2 - betterpingdisplay 1.1 - betterthirdperson 1.9.0 - blur 3.1.1 \-- satin 1.20.1+1.15.0-SNAPSHOT - carpeted 1.20-1.4 - carryon 2.1.2.7 \-- mixinextras 0.2.0-beta.6 - catalogue 1.8.0 - chat_heads 0.13.9 - cherishedworlds 6.1.7+1.20.1 - clienttweaks 11.1.0 - cloth_config 11.1.136 - clumps 12.0.0.4 - computercraft 1.113.1 - controlling 12.0.2 - coralstfc 1.0.0 - corpse 1.20.1-1.0.19 - cosmeticarmorreworked 1.20.1-v1a - craftingtweaks 18.2.5 - craftpresence 2.5.0 - create 0.5.1.f \-- flywheel 0.6.10-7 - create_connected 0.8.2-mc1.20.1 - createaddition 1.20.1-1.2.4c - creativecore 2.12.15 - cucumber 7.0.12 - cupboard 1.20.1-2.7 - curios 5.10.0+1.20.1 - defaultoptions 18.0.1 - do_a_barrel_roll 3.5.6+1.20.1 - drippyloadingscreen 3.0.1 - dynamiclightsreforged 1.20.1_v1.6.0 - embeddium 0.3.19+mc1.20.1 \-- rubidium 0.7.1 - embeddiumplus 1.2.12 - emi 1.1.7+1.20.1+forge - enhancedvisuals 1.8.1 - etched 3.0.2 - everycomp 1.20-2.7.12 - expatternprovider 1.20-1.1.14-forge - exposure 1.7.7 - fallingtrees 0.12.7 - fancymenu 3.2.3 - ferritecore 6.0.1 - firmaciv 0.2.10-alpha-1.20.1 - firmalife 2.1.15 - firstperson 2.4.5 - flickerfix 4.0.1 - forge 47.2.6 - fpsreducer 1.20-2.5 - framedblocks 9.3.1 - ftbbackups2 1.0.23 - ftbessentials 2001.2.2 - ftbfiltersystem 1.0.2 - ftblibrary 2001.2.4 - ftbquests 2001.4.8 - ftbranks 2001.1.3 - ftbteams 2001.3.0 - ftbxmodcompat 2.1.1 - gcyr 0.1.8 - getittogetherdrops 1.3 - glodium 1.20-1.5-forge - gtceu 1.2.3.a |-- configuration 2.2.0 \-- ldlib 1.0.25.j - hangglider 8.0.1 - immediatelyfast 1.2.18+1.20.4 - inventoryhud 3.4.26 - invtweaks 1.1.0 - itemphysiclite 1.6.5 - jade 11.9.4+forge - jadeaddons 5.2.2 - jei 15.3.0.8 - konkrete 1.8.0 - ksyxis 1.3.2 - kubejs 2001.6.5-build.14 - kubejs_create 2001.2.5-build.2 - kubejs_tfc 1.20.1-1.1.3 - letmedespawn 1.3.2b - lootjs 1.20.1-2.12.0 - megacells 2.4.4-1.20.1 - melody 1.0.2 - memoryleakfix 1.1.5 - merequester 1.20.1-1.1.5 - minecraft 1.20.1 - modelfix 1.15 - modernfix 5.18.1+mc1.20.1 - moonlight 1.20-2.13.51 - morered 4.0.0.4 |-- jumbofurnace 4.0.0.5 \-- useitemonblockevent 1.0.0.2 - mousetweaks 2.25.1 - myserveriscompatible 1.0 - nanhealthfixer 1.20.1-0.0.1 - nerb 0.4.1 - noisium 2.3.0+mc1.20-1.20.1 - noreportbutton 1.5.0 - notenoughanimations 1.7.6 - octolib 0.4.2 - oculus 1.7.0 - openpartiesandclaims 0.23.2 - packetfixer 1.4.2 - pandalib 0.4.2 - patchouli 1.20.1-84-FORGE - pickupnotifier 8.0.0 - placebo 8.6.2 - playerrevive 2.0.27 - polylib 2000.0.3-build.143 - puzzleslib 8.1.23 \-- puzzlesaccessapi 8.0.7 - radium 0.12.3+git.50c5c33 - railways 1.6.4+forge-mc1.20.1 - recipeessentials 1.20.1-3.6 - rhino 2001.2.2-build.18 - saturn 0.1.3 - searchables 1.0.3 - shimmer 1.20.1-0.2.4 - showcaseitem 1.20.1-1.2 - simplylight 1.20.1-1.4.6-build.50 - smoothboot 0.0.4 - sophisticatedbackpacks 3.20.5.1044 - sophisticatedcore 0.6.22.611 - supermartijn642configlib 1.1.8 - supermartijn642corelib 1.1.17 - tfc 3.2.12 - tfc_tumbleweed 1.2.2 - tfcagedalcohol 2.1 - tfcambiental 1.20.1-3.3.0 - tfcastikorcarts 1.1.8.2 - tfcchannelcasting 0.2.3-beta - tfcea 0.0.2 - tfcgroomer 1.20.1-0.1.2 - tfchotornot 1.0.4 - tfcvesseltooltip 1.1 - tfg 0.5.9 - toofast 0.4.3.5 - toolbelt 1.20.01 - treetap 1.20.1-0.4.0 - tumbleweed 0.5.5 - unilib 1.0.2 - uteamcore 5.1.4.312 - waterflasks 3.0.3 - xaerominimap 24.4.0 - xaeroworldmap 1.39.0 - yeetusexperimentus 2.3.1-build.6+mc1.20.1 - yet_another_config_lib_v3 3.5.0+1.20.1-forge [12:47:28] [main/WARN] [mixin/]: Reference map 'packetfixer-forge-forge-refmap.json' for packetfixer-forge.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:28] [main/WARN] [mixin/]: Reference map 'tfchotornot.refmap.json' for tfchotornot.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:29] [main/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel [12:47:29] [main/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel [12:47:29] [main/WARN] [mixin/]: Error loading class: mezz/modnametooltip/TooltipEventHandler (java.lang.ClassNotFoundException: mezz.modnametooltip.TooltipEventHandler) [12:47:29] [main/WARN] [mixin/]: Error loading class: me/shedaniel/rei/impl/client/ClientHelperImpl (java.lang.ClassNotFoundException: me.shedaniel.rei.impl.client.ClientHelperImpl) [12:47:29] [main/WARN] [mixin/]: Error loading class: me/shedaniel/rei/impl/client/gui/ScreenOverlayImpl (java.lang.ClassNotFoundException: me.shedaniel.rei.impl.client.gui.ScreenOverlayImpl) [12:47:29] [main/INFO] [co.cu.Cupboard/]: Loaded config for: recipeessentials.json [12:47:30] [main/WARN] [mixin/]: Error loading class: loaderCommon/forge/com/seibel/distanthorizons/common/wrappers/worldGeneration/mimicObject/ChunkLoader (java.lang.ClassNotFoundException: loaderCommon.forge.com.seibel.distanthorizons.common.wrappers.worldGeneration.mimicObject.ChunkLoader) [12:47:30] [main/INFO] [fpsreducer/]: bre2el.fpsreducer.mixin.RenderSystemMixin will be applied. [12:47:30] [main/INFO] [fpsreducer/]: bre2el.fpsreducer.mixin.WindowMixin will NOT be applied because OptiFine was NOT detected. [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatComponentMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatComponentMixin2 false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatListenerMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ClientPacketListenerMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.CommandSuggestionSuggestionsListMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ConnectionMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.DownloadedPackSourceMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.FontStringRenderOutputMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.GuiMessageLineMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.GuiMessageMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.HttpTextureMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.PlayerChatMessageMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.SkinManagerMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.compat.EmojifulMixin true false [12:47:30] [main/WARN] [mixin/]: Error loading class: dan200/computercraft/shared/integration/jei/JEIComputerCraft (java.lang.ClassNotFoundException: dan200.computercraft.shared.integration.jei.JEIComputerCraft) [12:47:30] [main/WARN] [mixin/]: @Mixin target dan200.computercraft.shared.integration.jei.JEIComputerCraft was not found tfg.mixins.json:common.cc.JEIComputerCraftMixin [12:47:30] [main/WARN] [mixin/]: Error loading class: com/copycatsplus/copycats/content/copycat/slab/CopycatSlabBlock (java.lang.ClassNotFoundException: com.copycatsplus.copycats.content.copycat.slab.CopycatSlabBlock) [12:47:30] [main/WARN] [mixin/]: @Mixin target com.copycatsplus.copycats.content.copycat.slab.CopycatSlabBlock was not found create_connected.mixins.json:compat.CopycatBlockMixin [12:47:30] [main/WARN] [mixin/]: Error loading class: com/copycatsplus/copycats/content/copycat/board/CopycatBoardBlock (java.lang.ClassNotFoundException: com.copycatsplus.copycats.content.copycat.board.CopycatBoardBlock) [12:47:30] [main/WARN] [mixin/]: @Mixin target com.copycatsplus.copycats.content.copycat.board.CopycatBoardBlock was not found create_connected.mixins.json:compat.CopycatBlockMixin [12:47:30] [main/WARN] [mixin/]: Error loading class: me/jellysquid/mods/lithium/common/ai/pathing/PathNodeDefaults (java.lang.ClassNotFoundException: me.jellysquid.mods.lithium.common.ai.pathing.PathNodeDefaults) [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'alloc.blockstate.StateMixin' as option 'mixin.alloc.blockstate' (added by mods [ferritecore]) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.fluid.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.intersection.WorldMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.movement.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.AbstractMinecartEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.BoatEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityPredicatesMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityTrackingSectionMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.LivingEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'world.player_chunk_tick.ThreadedAnvilChunkStorageMixin' as option 'mixin.world.player_chunk_tick' (added by user configuration) disables it and children [12:47:30] [main/WARN] [mixin/]: Error loading class: org/cyclops/integrateddynamics/block/BlockCable (java.lang.ClassNotFoundException: org.cyclops.integrateddynamics.block.BlockCable) [12:47:30] [main/WARN] [mixin/]: @Mixin target org.cyclops.integrateddynamics.block.BlockCable was not found mixins.epp.json:MixinBlockCable [12:47:30] [main/WARN] [mixin/]: Error loading class: blusunrize/immersiveengineering/api/wires/GlobalWireNetwork (java.lang.ClassNotFoundException: blusunrize.immersiveengineering.api.wires.GlobalWireNetwork) [12:47:30] [main/WARN] [mixin/]: @Mixin target blusunrize.immersiveengineering.api.wires.GlobalWireNetwork was not found mixins.epp.json:MixinGlobalWireNetwork [12:47:30] [main/WARN] [mixin/]: Error loading class: weather2/weathersystem/storm/TornadoHelper (java.lang.ClassNotFoundException: weather2.weathersystem.storm.TornadoHelper) [12:47:30] [main/WARN] [mixin/]: @Mixin target weather2.weathersystem.storm.TornadoHelper was not found tfc_tumbleweed.mixins.json:TornadoHelperMixin [12:47:30] [main/WARN] [mixin/]: Error loading class: weather2/weathersystem/storm/TornadoHelper (java.lang.ClassNotFoundException: weather2.weathersystem.storm.TornadoHelper) [12:47:30] [main/WARN] [mixin/]: @Mixin target weather2.weathersystem.storm.TornadoHelper was not found tfc_tumbleweed.mixins.json:client.TornadoHelperMixin [12:47:30] [main/INFO] [memoryleakfix/]: [MemoryLeakFix] Will be applying 3 memory leak fixes! [12:47:30] [main/INFO] [memoryleakfix/]: [MemoryLeakFix] Currently enabled memory leak fixes: [targetEntityLeak, biomeTemperatureLeak, hugeScreenshotLeak] [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.WorldRendererMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.ClientWorldMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.BackgroundRendererMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.gui.font.GlyphRendererMixin' as rule 'mixin.features.render.gui.font' (added by mods [oculus]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.gui.font.FontSetMixin' as rule 'mixin.features.render.gui.font' (added by mods [oculus]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.shadows.EntityRenderDispatcherMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.fast_render.ModelPartMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.fast_render.CuboidMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.cull.EntityRendererMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children [12:47:31] [main/WARN] [mixin/]: Error loading class: org/jetbrains/annotations/ApiStatus$Internal (java.lang.ClassNotFoundException: org.jetbrains.annotations.ApiStatus$Internal) [12:47:31] [main/INFO] [MixinExtras|Service/]: Initializing MixinExtras via com.llamalad7.mixinextras.service.MixinExtrasServiceImpl(version=0.4.1). [12:47:31] [main/INFO] [Smooth Boot (Reloaded)/]: Smooth Boot (Reloaded) config initialized [12:47:31] [main/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_216202_ in modernfix-forge.mixins.json:perf.tag_id_caching.TagOrElementLocationMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. [12:47:32] [pool-4-thread-1/INFO] [minecraft/Bootstrap]: ModernFix reached bootstrap stage (9.773 s after launch) [12:47:32] [pool-4-thread-1/WARN] [mixin/]: @Final field delegatesByName:Ljava/util/Map; in modernfix-forge.mixins.json:perf.forge_registry_alloc.ForgeRegistryMixin should be final [12:47:32] [pool-4-thread-1/WARN] [mixin/]: @Final field delegatesByValue:Ljava/util/Map; in modernfix-forge.mixins.json:perf.forge_registry_alloc.ForgeRegistryMixin should be final [12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel [12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel [12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getNeighborPathNodeType from me.jellysquid.mods.lithium.mixin.ai.pathing.AbstractBlockStateMixin [12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getPathNodeType from me.jellysquid.mods.lithium.mixin.ai.pathing.AbstractBlockStateMixin [12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getAllFlags from me.jellysquid.mods.lithium.mixin.util.block_tracking.AbstractBlockStateMixin [12:47:32] [pool-4-thread-1/WARN] [mixin/]: Method overwrite conflict for m_6104_ in embeddium.mixins.json:features.options.render_layers.LeavesBlockMixin, previously written by me.srrapero720.embeddiumplus.mixins.impl.leaves_culling.LeavesBlockMixin. Skipping method. [12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel [12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel [12:47:33] [pool-4-thread-1/INFO] [minecraft/Bootstrap]: Vanilla bootstrap took 779 milliseconds [12:47:34] [pool-4-thread-1/WARN] [mixin/]: Method overwrite conflict for m_47505_ in lithium.mixins.json:world.temperature_cache.BiomeMixin, previously written by org.embeddedt.modernfix.common.mixin.perf.remove_biome_temperature_cache.BiomeMixin. Skipping method. [12:47:34] [pool-4-thread-1/INFO] [co.al.me.MERequester/]: Registering content [12:47:35] [Render thread/WARN] [minecraft/VanillaPackResourcesBuilder]: Assets URL 'union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraft/client/1.20.1-20230612.114412/client-1.20.1-20230612.114412-srg.jar%23444!/assets/.mcassetsroot' uses unexpected schema [12:47:35] [Render thread/WARN] [minecraft/VanillaPackResourcesBuilder]: Assets URL 'union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraft/client/1.20.1-20230612.114412/client-1.20.1-20230612.114412-srg.jar%23444!/data/.mcassetsroot' uses unexpected schema [12:47:35] [Render thread/INFO] [mojang/YggdrasilAuthenticationService]: Environment: authHost='https://authserver.mojang.com', accountsHost='https://api.mojang.com', sessionHost='https://sessionserver.mojang.com', servicesHost='https://api.minecraftservices.com', name='PROD' [12:47:35] [Render thread/INFO] [minecraft/Minecraft]: Setting user: Ryexandrite [12:47:35] [Render thread/INFO] [ModernFix/]: Bypassed Mojang DFU [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant is searching for constants in method with descriptor (Lnet/minecraft/network/chat/Component;Lnet/minecraft/client/GuiMessageTag;)V [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = , stringValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 0 [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = \\r, stringValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 1 [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn \\r [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = , stringValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 2 [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = \\n, stringValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 3 [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn \\n [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found CLASS constant: value = Ljava/lang/String;, typeValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = [{}] [CHAT] {}, stringValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 4 [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [{}] [CHAT] {} [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = [CHAT] {}, stringValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 5 [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [CHAT] {} [12:47:35] [Render thread/INFO] [defaultoptions/]: Loaded default options for extra-folder [12:47:35] [Render thread/INFO] [ModernFix/]: Instantiating Mojang DFU [12:47:36] [Render thread/INFO] [minecraft/Minecraft]: Backend library: LWJGL version 3.3.1 build 7 [12:47:36] [Render thread/INFO] [KubeJS/]: Loaded client.properties [12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Vendor: AMD [12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Renderer: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) [12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Version: 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f) [12:47:36] [Render thread/WARN] [Embeddium++/Config]: Loading Embeddium++Config [12:47:36] [Render thread/INFO] [Embeddium++/Config]: Updating config cache [12:47:36] [Render thread/INFO] [Embeddium++/Config]: Cache updated successfully [12:47:36] [Render thread/INFO] [ImmediatelyFast/]: Initializing ImmediatelyFast 1.2.18+1.20.4 on AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) (AMD) with OpenGL 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f) [12:47:36] [Render thread/INFO] [ImmediatelyFast/]: AMD GPU detected. Enabling coherent buffer mapping [12:47:36] [Datafixer Bootstrap/INFO] [mojang/DataFixerBuilder]: 188 Datafixer optimizations took 85 milliseconds [12:47:36] [Render thread/INFO] [ImmediatelyFast/]: Found Iris/Oculus 1.7.0. Enabling compatibility. [12:47:36] [Render thread/INFO] [Oculus/]: Debug functionality is disabled. [12:47:36] [Render thread/INFO] [Oculus/]: OpenGL 4.5 detected, enabling DSA. [12:47:36] [Render thread/INFO] [Oculus/]: Shaders are disabled because no valid shaderpack is selected [12:47:36] [Render thread/INFO] [Oculus/]: Shaders are disabled [12:47:36] [modloading-worker-0/INFO] [dynamiclightsreforged/]: [LambDynLights] Initializing Dynamic Lights Reforged... [12:47:36] [modloading-worker-0/INFO] [LowDragLib/]: LowDragLib is initializing on platform: Forge [12:47:36] [modloading-worker-0/INFO] [in.u_.u_.ut.ve.JarSignVerifier/]: Mod uteamcore is signed with a valid certificate. [12:47:36] [modloading-worker-0/INFO] [de.ke.me.Melody/]: [MELODY] Loading Melody background audio library.. [12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for pickupnotifier:main [12:47:36] [modloading-worker-0/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_109501_ in embeddium.mixins.json:core.render.world.WorldRendererMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. [12:47:36] [modloading-worker-0/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Successfully initialized! [12:47:36] [modloading-worker-0/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Server-side libs ready to use! [12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for pickupnotifier:main [12:47:36] [modloading-worker-0/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_215924_ in modernfix-forge.mixins.json:perf.tag_id_caching.TagEntryMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. [12:47:36] [modloading-worker-0/INFO] [Additional Placements/]: Attempting to manually load Additional Placements config early. [12:47:36] [modloading-worker-0/INFO] [Additional Placements/]: manual config load successful. [12:47:36] [modloading-worker-0/WARN] [Additional Placements/]: During block registration you may recieve several reports of "Potentially Dangerous alternative prefix `additionalplacements`". Ignore these, they are intended. [12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for hangglider:main [12:47:36] [modloading-worker-0/INFO] [noisium/]: Loading Noisium. [12:47:36] [modloading-worker-0/INFO] [co.cu.Cupboard/]: Loaded config for: cupboard.json [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id architectury:sync_ids [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id architectury:sync_ids [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id pandalib:config_sync [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id pandalib:config_sync [12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully loaded config 'fallingtrees_client' [12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully saved config 'fallingtrees_client' [12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for hangglider:main [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id polylib:container_to_client [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id polylib:tile_to_client [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:container_packet_server [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:tile_data_server [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:tile_packet_server [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:edit_nbt [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftblibrary:edit_nbt_response [12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully loaded config 'fallingtrees_common' [12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully saved config 'fallingtrees_common' [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:sync_known_server_registries [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:edit_config [12:47:37] [UniLib/INFO] [unilib/]: Starting version check for "craftpresence" (MC 1.20.1) at "https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/CraftPresence/update.json" [12:47:37] [UniLib/INFO] [unilib/]: Starting version check for "unilib" (MC 1.20.1) at "https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/UniLib/update.json" [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbessentials:update_tab_name [12:47:37] [modloading-worker-0/INFO] [invtweaks/]: Registered 2 network packets [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Loaded common.properties [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Loaded dev.properties [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Looking for KubeJS plugins... [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:sync_teams [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:sync_message_history [12:47:37] [modloading-worker-0/INFO] [GregTechCEu/]: GregTechCEu is initializing on platform: Forge [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:open_gui [12:47:37] [CraftPresence/INFO] [craftpresence/]: Configuration settings have been saved and reloaded successfully! [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:open_my_team_gui [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:update_settings [12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin dev.latvian.mods.kubejs.integration.forge.gamestages.GameStagesIntegration does not have required mod gamestages loaded, skipping [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source ldlib [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source exposure [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source tfg [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:update_settings_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:send_message [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source ftbxmodcompat [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:send_message_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:update_presence [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:create_party [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:player_gui_operation [12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin dev.ftb.mods.ftbxmodcompat.ftbchunks.kubejs.FTBChunksKubeJSPlugin does not have required mod ftbchunks loaded, skipping [12:47:37] [modloading-worker-0/INFO] [de.ke.dr.DrippyLoadingScreen/]: [DRIPPY LOADING SCREEN] Loading v3.0.1 in client-side mode on FORGE! [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source lootjs [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source cucumber [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source gtceu [12:47:37] [modloading-worker-0/INFO] [ne.dr.tf.TerraFirmaCraft/]: Initializing TerraFirmaCraft [12:47:37] [modloading-worker-0/INFO] [ne.dr.tf.TerraFirmaCraft/]: Options: Assertions Enabled = false, Boostrap = false, Test = false, Debug Logging = true [12:47:37] [CraftPresence/INFO] [craftpresence/]: Checking Discord for available assets with Client Id: 1182610212121743470 [12:47:37] [CraftPresence/INFO] [craftpresence/]: Originally coded by paulhobbel - https://github.com/paulhobbel [12:47:37] [modloading-worker-0/INFO] [ne.mi.co.ForgeMod/FORGEMOD]: Forge mod loading, version 47.2.6, for MC 1.20.1 with MCP 20230612.114412 [12:47:37] [modloading-worker-0/INFO] [ne.mi.co.MinecraftForge/FORGE]: MinecraftForge v47.2.6 Initialized [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source gcyr [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs_tfc [12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin com.notenoughmail.kubejs_tfc.addons.precpros.PrecProsPlugin does not have required mod precisionprospecting loaded, skipping [12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin com.notenoughmail.kubejs_tfc.addons.afc.AFCPlugin does not have required mod afc loaded, skipping [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs_create [Mouse Tweaks] Main.initialize() [Mouse Tweaks] Initialized. [12:47:37] [modloading-worker-0/INFO] [Every Compat/]: Loaded EveryCompat Create Module [12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gtceu config for auto-sync function [12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gtceu config for auto-sync function [12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gcyr config for auto-sync function [12:47:37] [modloading-worker-0/INFO] [GregTechCEu/]: High-Tier is Disabled. [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Using default implementation for ThreadExecutor [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Done in 309.0 ms [12:47:37] [CraftPresence/INFO] [craftpresence/]: 3 total assets detected! [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_quests [12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Booting... (platform: Forge, manual: false) [12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Found Mixin library. (version: 0.8.5) [12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Ready. As always, this mod will speed up your world loading and might or might not break it. [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_team_data [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.SchedulerSignalerImpl/]: Initialized Scheduler Signaller of type: class net.creeperhost.ftbbackups.repack.org.quartz.core.SchedulerSignalerImpl [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Quartz Scheduler v.2.0.2 created. [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:update_task_progress [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.si.RAMJobStore/]: RAMJobStore initialized. [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:submit_task [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Scheduler meta-data: Quartz Scheduler (v2.0.2) 'ftbbackups2' with instanceId 'NON_CLUSTERED' Scheduler class: 'net.creeperhost.ftbbackups.repack.org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'net.creeperhost.ftbbackups.repack.org.quartz.simpl.SimpleThreadPool' - with 1 threads. Using job-store 'net.creeperhost.ftbbackups.repack.org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Quartz scheduler 'ftbbackups2' initialized from an externally provided properties instance. [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Quartz scheduler version: 2.0.2 [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_reward [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:claim_reward_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_editing_mode [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:get_emergency_items [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:create_other_team_data [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_all_rewards [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_choice_reward [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_completion_toast [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Scheduler ftbbackups2_$_NON_CLUSTERED started. [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_reward_toast [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_item_reward_toast [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_pinned [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:toggle_pinned_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_chapter_pinned [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:toggle_chapter_pinned_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_editing_mode [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:force_save [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:update_team_data [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:set_custom_image [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_started [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_completed [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_started_reset [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_completed_reset [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_lock [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:reset_reward [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:team_data_changed [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:task_screen_config_req [12:47:37] [modloading-worker-0/INFO] [co.jo.fl.ba.Backend/]: Oculus detected. [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:task_screen_config_resp [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:change_progress [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:create_object [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:create_object_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:create_task_at [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:delete_object [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:delete_object_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:edit_object [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:edit_object_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_chapter [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_chapter_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_quest [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_quest_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:change_chapter_group [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:change_chapter_group_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_chapter_group [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_chapter_group_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_reward_blocking [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:copy_quest [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:copy_chapter_image [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:sync_structures_request [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_structures_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:request_team_data [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_editor_permission [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:open_quest_book [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:clear_display_cache [12:47:37] [modloading-worker-0/INFO] [me.je.li.lo.PluginCaller/]: Sending ConfigManager... [12:47:37] [modloading-worker-0/INFO] [me.je.li.lo.PluginCaller/]: Sending ConfigManager took 11.32 ms [12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised items. [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbfiltersystem:sync_filter [12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised blocks. [12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised block entities. [12:47:37] [modloading-worker-0/INFO] [de.ke.fa.FancyMenu/]: [FANCYMENU] Loading v3.2.3 in client-side mode on FORGE! [12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Loading config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml [12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Built config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml [12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Loaded config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml [12:47:37] [modloading-worker-0/INFO] [FTB XMod Compat/]: [FTB Quests] Enabled KubeJS integration [12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] Starting... [12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] Loading... [12:47:37] [modloading-worker-0/INFO] [de.to.pa.PacketFixer/]: Packet Fixer has been initialized successfully [12:47:37] [modloading-worker-0/INFO] [YetAnotherConfigLib/]: Deserializing YACLConfig from '/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/yacl.json5' [12:47:37] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for puzzleslib:main [12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] All done! [12:47:37] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for puzzleslib:main [12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Loading Rhino Minecraft remapper... [12:47:37] [modloading-worker-0/INFO] [de.la.mo.rh.mo.ut.RhinoProperties/]: Rhino properties loaded. [12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Loading mappings for 1.20.1 [12:47:37] [modloading-worker-0/WARN] [mixin/]: @Inject(@At("INVOKE")) Shift.BY=2 on create_connected.mixins.json:sequencedgearshift.SequencedGearshiftScreenMixin::handler$cfa000$updateParamsOfRow exceeds the maximum allowed value: 0. Increase the value of maxShiftBy to suppress this warning. [12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Done in 0.090 s [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registered bogey styles from railways [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering data fixers [12:47:38] [modloading-worker-0/WARN] [Railways/]: Skipping Datafixer Registration due to it being disabled in the config. [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Hex Casting [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Oh The Biomes You'll Go [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Blue Skies [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Twilight Forest [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Biomes O' Plenty [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Nature's Spirit [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Dreams and Desires [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Quark [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for TerraFirmaCraft [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:main_startup_script.js in 0.058 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfc/constants.js in 0.024 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:horornot/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:minecraft/constants.js in 0.004 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:railways/constants.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/machines.js in 0.008 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/material_info.js in 0.002 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/recipe_types.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/blocks.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/items.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:framedblocks/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmalife/constants.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:sophisticated_backpacks/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:more_red/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:mega_cells/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:create/constants.js in 0.002 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:ae2/constants.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:create_additions/constants.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:chisel_and_bits/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:extended_ae2/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:asticor_carts/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmaciv/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmaciv/blocks.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:ftb_quests/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/fluids.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/materials.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/blocks.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/items.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:computer_craft/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded 30/30 KubeJS startup scripts in 0.721 s with 0 errors and 0 warnings [12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: example.js#3: TerraFirmaGreg the best modpack in the world :) [12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded script client_scripts:example.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded script client_scripts:tooltips.js in 0.003 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded 2/2 KubeJS client scripts in 0.022 s with 0 errors and 0 warnings [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#48: Loaded Java class 'net.minecraft.world.level.block.AmethystClusterBlock' [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#49: Loaded Java class 'net.minecraft.world.level.block.Blocks' [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#50: Loaded Java class 'net.minecraft.world.level.block.state.BlockBehaviour$Properties' [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id kubejs:send_data_from_client [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:send_data_from_server [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:paint [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:add_stage [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:remove_stage [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:sync_stages [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id kubejs:first_click [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:toast [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:reload_startup_scripts [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:display_server_errors [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:display_client_errors [12:47:38] [Render thread/INFO] [GregTechCEu/]: GTCEu common proxy init! [12:47:38] [Render thread/INFO] [GregTechCEu/]: Registering material registries [12:47:38] [Render thread/INFO] [GregTechCEu/]: Registering GTCEu Materials [12:47:38] [CraftPresence/INFO] [craftpresence/]: Attempting to connect to Discord (1/10)... [12:47:39] [Render thread/INFO] [GregTechCEu/]: Registering addon Materials [12:47:39] [Render thread/WARN] [GregTechCEu/]: FluidStorageKey{gtceu:liquid} already has an associated fluid for material gtceu:water [12:47:39] [Render thread/WARN] [GregTechCEu/]: FluidStorageKey{gtceu:liquid} already has an associated fluid for material gtceu:lava [12:47:39] [CraftPresence/INFO] [craftpresence/]: Loaded display data with Client Id: 1182610212121743470 (Logged in as RyRy) [12:47:39] [Render thread/INFO] [GregTechCEu/]: Registering KeyBinds [12:47:39] [Render thread/WARN] [ne.mi.fm.DeferredWorkQueue/LOADING]: Mod 'gtceu' took 1.043 s to run a deferred task. [12:47:42] [Render thread/WARN] [ne.mi.re.ForgeRegistry/REGISTRIES]: Registry minecraft:menu: The object net.minecraft.world.inventory.MenuType@67141ef8 has been registered twice for the same name ae2:export_card. [12:47:42] [Render thread/WARN] [ne.mi.re.ForgeRegistry/REGISTRIES]: Registry minecraft:menu: The object net.minecraft.world.inventory.MenuType@f4864e9 has been registered twice for the same name ae2:insert_card. [12:47:42] [Render thread/INFO] [Moonlight/]: Initialized block sets in 21ms [12:47:42] [Render thread/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ae2wtlib:cycle_terminal [12:47:43] [Render thread/INFO] [Every Compat/]: Registering Compat WoodType Blocks [12:47:43] [Render thread/INFO] [Every Compat/]: EveryCompat Create Module: registered 42 WoodType blocks [12:47:43] [Render thread/INFO] [tf.TFCTumbleweed/]: Injecting TFC Tumbleweed override pack [12:47:43] [Render thread/INFO] [co.ee.fi.FirmaLife/]: Injecting firmalife override pack [ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) [12:47:43] [Render thread/INFO] [Oculus/]: Hardware information: [12:47:43] [Render thread/INFO] [Oculus/]: CPU: 16x AMD Ryzen 7 3700X 8-Core Processor [12:47:43] [Render thread/INFO] [Oculus/]: GPU: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) (Supports OpenGL 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f)) [12:47:43] [Render thread/INFO] [Oculus/]: OS: Linux (6.6.85) [12:47:44] [Render thread/WARN] [mixin/]: Method overwrite conflict for isHidden in mixins.oculus.compat.sodium.json:copyEntity.ModelPartMixin, previously written by dev.tr7zw.firstperson.mixins.ModelPartMixin. Skipping method. [12:47:44] [Render thread/INFO] [minecraft/Minecraft]: [FANCYMENU] Registering resource reload listener.. [12:47:44] [Render thread/INFO] [de.ke.fa.cu.ScreenCustomization/]: [FANCYMENU] Initializing screen customization engine! Addons should NOT REGISTER TO REGISTRIES anymore now! [12:47:44] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Minecraft resource reload: STARTING [12:47:44] [Render thread/INFO] [ModernFix/]: Invalidating pack caches [12:47:44] [Render thread/INFO] [minecraft/ReloadableResourceManager]: Reloading ResourceManager: Additional Placements blockstate redirection pack, vanilla, mod_resources, gtceu:dynamic_assets, Moonlight Mods Dynamic Assets, Firmalife-1.20.1-2.1.15.jar:overload, TFCTumbleweed-1.20.1-1.2.2.jar:overload, KubeJS Resource Pack [assets], ldlib [12:47:44] [Finalizer/WARN] [ModernFix/]: One or more BufferBuilders have been leaked, ModernFix will attempt to correct this. [12:47:45] [Render thread/INFO] [Every Compat/]: Generated runtime CLIENT_RESOURCES for pack Moonlight Mods Dynamic Assets (everycomp) in: 597 ms [12:47:45] [Render thread/INFO] [Moonlight/]: Generated runtime CLIENT_RESOURCES for pack Moonlight Mods Dynamic Assets (moonlight) in: 0 ms [12:47:45] [modloading-worker-0/INFO] [Puzzles Lib/]: Loading client config for pickupnotifier [12:47:45] [modloading-worker-0/INFO] [Puzzles Lib/]: Loading client config for hangglider [12:47:45] [Worker-ResourceReload-4/INFO] [minecraft/UnihexProvider]: Found unifont_all_no_pua-15.0.06.hex, loading [12:47:45] [Worker-ResourceReload-3/INFO] [xa.pa.OpenPartiesAndClaims/]: Loading Open Parties and Claims! [12:47:45] [Worker-ResourceReload-1/INFO] [co.re.RecipeEssentials/]: recipeessentials mod initialized [12:47:45] [Worker-ResourceReload-10/INFO] [ne.dr.tf.TerraFirmaCraft/]: TFC Common Setup [12:47:45] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [saturn] Starting version check at https://github.com/AbdElAziz333/Saturn/raw/mc1.20.1/dev/updates.json [12:47:45] [Worker-ResourceReload-1/INFO] [FTB Library/]: Setting game stages provider implementation to: KubeJS Stages [12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: Chose [KubeJS Stages] as the active game stages implementation [12:47:45] [Worker-ResourceReload-1/INFO] [FTB Library/]: Setting permissions provider implementation to: FTB Ranks [12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: Chose [FTB Ranks] as the active permissions implementation [12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: [FTB Quests] recipe helper provider is [JEI] [12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: [FTB Quests] Enabled FTB Filter System integration [12:47:45] [Render thread/INFO] [GregTechCEu/]: GregTech Model loading took 520ms [12:47:46] [Render thread/INFO] [minecraft/LoadingOverlay]: [DRIPPY LOADING SCREEN] Initializing fonts for text rendering.. [12:47:46] [Render thread/INFO] [minecraft/LoadingOverlay]: [DRIPPY LOADING SCREEN] Calculating animation sizes for FancyMenu.. [12:47:46] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] ScreenCustomizationLayer registered: drippy_loading_overlay [12:47:46] [Render thread/INFO] [de.ke.fa.cu.an.AnimationHandler/]: [FANCYMENU] Preloading animations! This could cause the loading screen to freeze for a while.. [12:47:46] [Render thread/INFO] [de.ke.fa.cu.an.AnimationHandler/]: [FANCYMENU] Finished preloading animations! [12:47:46] [Render thread/INFO] [de.ke.fa.FancyMenu/]: [FANCYMENU] Starting late client initialization phase.. [12:47:46] [Forge Version Check/WARN] [ne.mi.fm.VersionChecker/]: Failed to process update information com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 9 column 1 path $ at com.google.gson.Gson.fromJson(Gson.java:1226) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:1124) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:1034) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:969) ~[gson-2.10.jar%2388!/:?] {} at net.minecraftforge.fml.VersionChecker$1.process(VersionChecker.java:183) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} at java.lang.Iterable.forEach(Iterable.java:75) ~[?:?] {re:mixin} at net.minecraftforge.fml.VersionChecker$1.run(VersionChecker.java:114) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 9 column 1 path $ at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:393) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:182) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:144) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:1214) ~[gson-2.10.jar%2388!/:?] {} ... 6 more [12:47:46] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [controlling] Starting version check at https://updates.blamejared.com/get?n=controlling&gv=1.20.1 [12:47:46] [Worker-ResourceReload-6/ERROR] [minecraft/SimpleJsonResourceReloadListener]: Couldn't parse data file tfc:field_guide/ru_ru/entries/tfg_ores/surface_copper from tfc:patchouli_books/field_guide/ru_ru/entries/tfg_ores/surface_copper.json com.google.gson.JsonParseException: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 55 column 6 path $.pages[5] at net.minecraft.util.GsonHelper.m_13780_(GsonHelper.java:526) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} at net.minecraft.util.GsonHelper.m_263475_(GsonHelper.java:531) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} at net.minecraft.util.GsonHelper.m_13776_(GsonHelper.java:581) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_278771_(SimpleJsonResourceReloadListener.java:41) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_5944_(SimpleJsonResourceReloadListener.java:29) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_5944_(SimpleJsonResourceReloadListener.java:17) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} at net.minecraft.server.packs.resources.SimplePreparableReloadListener.m_10786_(SimplePreparableReloadListener.java:11) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,re:classloading,pl:accesstransformer:B,pl:mixin:APP:moonlight.mixins.json:ConditionHackMixin,pl:mixin:A} at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768) ~[?:?] {} at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1760) ~[?:?] {} at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} Caused by: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 55 column 6 path $.pages[5] at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1657) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.stream.JsonReader.checkLenient(JsonReader.java:1463) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:569) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.stream.JsonReader.hasNext(JsonReader.java:422) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:779) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:725) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.TypeAdapters$34$1.read(TypeAdapters.java:1007) ~[gson-2.10.jar%2388!/:?] {} at net.minecraft.util.GsonHelper.m_13780_(GsonHelper.java:524) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} ... 13 more [12:47:46] [Render thread/INFO] [de.ke.fa.ut.wi.WindowHandler/]: [FANCYMENU] Custom window icon successfully updated! [12:47:46] [Render thread/INFO] [KubeJS Client/]: Client resource reload complete! [12:47:46] [Render thread/INFO] [defaultoptions/]: Loaded default options for keymappings [12:47:46] [Render thread/INFO] [de.ke.fa.ut.wi.WindowHandler/]: [FANCYMENU] Custom window icon successfully updated! [12:47:46] [Worker-Main-6/INFO] [minecraft/UnihexProvider]: Found unifont_all_no_pua-15.0.06.hex, loading [12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [controlling] Found status: BETA Current: 12.0.2 Target: 12.0.2 [12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [uteamcore] Starting version check at https://api.u-team.info/update/uteamcore.json [12:47:47] [FTB Backups Config Watcher 0/INFO] [ne.cr.ft.FTBBackups/]: Config at /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/ftbbackups2.json has changed, reloaded! [12:47:47] [Worker-ResourceReload-14/WARN] [minecraft/SpriteLoader]: Texture create_connected:block/fluid_container_window_debug with size 40x32 limits mip level from 4 to 3 [12:47:47] [UniLib/INFO] [unilib/]: Received update status for "unilib" -> Outdated (Target version: "v1.0.5") [12:47:47] [UniLib/INFO] [unilib/]: Received update status for "craftpresence" -> Outdated (Target version: "v2.5.4") [12:47:47] [Render thread/INFO] [Every Compat/]: Registered 42 compat blocks making up 0.31% of total blocks registered [12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [uteamcore] Found status: OUTDATED Current: 5.1.4.312 Target: 5.1.4.346 [12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [pickupnotifier] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/pickupnotifier.json [12:47:47] [Render thread/INFO] [Moonlight/]: Initialized color sets in 104ms [12:47:47] [Render thread/INFO] [co.no.ku.KubeJSTFC/]: KubeJS TFC configuration: [12:47:47] [Render thread/INFO] [co.no.ku.KubeJSTFC/]: Debug mode enabled: false [12:47:47] [Render thread/INFO] [MEGA Cells/]: Initialised AE2WT integration. [12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [pickupnotifier] Found status: UP_TO_DATE Current: 8.0.0 Target: null [12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [corpse] Starting version check at https://update.maxhenkel.de/forge/corpse [12:47:47] [Worker-ResourceReload-0/INFO] [FirstPersonModel/]: Loading FirstPerson Mod [12:47:47] [Worker-ResourceReload-4/INFO] [xa.ma.WorldMap/]: Loading Xaero's World Map - Stage 1/2 [12:47:47] [Placebo Patreon Trail Loader/INFO] [placebo/]: Loading patreon trails data... [12:47:47] [Placebo Patreon Wing Loader/INFO] [placebo/]: Loading patreon wing data... [12:47:47] [Worker-ResourceReload-13/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Client-side libs ready to use! [12:47:47] [Placebo Patreon Trail Loader/INFO] [placebo/]: Loaded 45 patreon trails. [12:47:47] [Placebo Patreon Wing Loader/INFO] [placebo/]: Loaded 21 patreon wings. [12:47:47] [Worker-ResourceReload-7/INFO] [EMI/]: [EMI] Discovered Sodium [12:47:47] [Worker-ResourceReload-14/INFO] [xa.mi.XaeroMinimap/]: Loading Xaero's Minimap - Stage 1/2 [12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:update_wut [12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:update_restock [12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:restock_amounts [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [corpse] Found status: OUTDATED Current: 1.20.1-1.0.19 Target: 1.20.1-1.0.20 [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [create_connected] Starting version check at https://raw.githubusercontent.com/hlysine/create_connected/main/update.json [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [create_connected] Found status: OUTDATED Current: 0.8.2-mc1.20.1 Target: 1.0.1-mc1.20.1 [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [blur] Starting version check at https://api.modrinth.com/updates/rubidium-extra/forge_updates.json [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [blur] Found status: AHEAD Current: 3.1.1 Target: null [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [hangglider] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/hangglider.json [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [hangglider] Found status: UP_TO_DATE Current: 8.0.1 Target: null [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [searchables] Starting version check at https://updates.blamejared.com/get?n=searchables&gv=1.20.1 [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [searchables] Found status: BETA Current: 1.0.3 Target: 1.0.3 [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [computercraft] Starting version check at https://api.modrinth.com/updates/cc-tweaked/forge_updates.json [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [computercraft] Found status: OUTDATED Current: 1.113.1 Target: 1.115.1 [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [unilib] Starting version check at https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/UniLib/update.json [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [unilib] Found status: AHEAD Current: 1.0.2 Target: null [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [craftpresence] Starting version check at https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/CraftPresence/update.json [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [craftpresence] Found status: AHEAD Current: 2.5.0 Target: null [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [radium] Starting version check at https://api.modrinth.com/updates/radium/forge_updates.json [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [radium] Found status: OUTDATED Current: 0.12.3+git.50c5c33 Target: 0.12.4 [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [attributefix] Starting version check at https://updates.blamejared.com/get?n=attributefix&gv=1.20.1 [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [attributefix] Found status: BETA Current: 21.0.4 Target: 21.0.4 [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [clumps] Starting version check at https://updates.blamejared.com/get?n=clumps&gv=1.20.1 [12:47:49] [Worker-ResourceReload-4/WARN] [xa.hu.mi.MinimapLogs/]: io exception while checking patreon: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [clumps] Found status: BETA Current: 12.0.0.4 Target: 12.0.0.4 [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [catalogue] Starting version check at https://mrcrayfish.com/modupdatejson?id=catalogue [12:47:50] [Forge Version Check/WARN] [ne.mi.fm.VersionChecker/]: Failed to process update information com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $ at com.google.gson.Gson.fromJson(Gson.java:1226) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:1124) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:1034) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:969) ~[gson-2.10.jar%2388!/:?] {} at net.minecraftforge.fml.VersionChecker$1.process(VersionChecker.java:183) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} at java.lang.Iterable.forEach(Iterable.java:75) ~[?:?] {re:mixin} at net.minecraftforge.fml.VersionChecker$1.run(VersionChecker.java:114) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $ at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:393) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:182) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:144) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:1214) ~[gson-2.10.jar%2388!/:?] {} ... 6 more [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzlesaccessapi] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/puzzlesaccessapi.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzlesaccessapi] Found status: BETA Current: 8.0.7 Target: null [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [forge] Starting version check at https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json [12:47:50] [Worker-ResourceReload-5/WARN] [minecraft/ModelBakery]: tfcambiental:snowshoes#inventory java.io.FileNotFoundException: tfcambiental:models/item/snowshoes.json at net.minecraft.client.resources.model.ModelBakery.m_119364_(ModelBakery.java:417) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.m_119362_(ModelBakery.java:266) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.m_119341_(ModelBakery.java:243) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.m_119306_(ModelBakery.java:384) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.(ModelBakery.java:150) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) ~[?:?] {re:mixin} at java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) ~[?:?] {} at java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) ~[?:?] {} at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} [12:47:50] [Worker-ResourceReload-5/WARN] [minecraft/ModelBakery]: carpeted:block/label java.io.FileNotFoundException: carpeted:models/block/label.json at net.minecraft.client.resources.model.ModelBakery.m_119364_(ModelBakery.java:417) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.m_119362_(ModelBakery.java:262) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.m_119341_(ModelBakery.java:243) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.(ModelBakery.java:159) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) ~[?:?] {re:mixin} at java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) ~[?:?] {} at java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) ~[?:?] {} at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [forge] Found status: OUTDATED Current: 47.2.6 Target: 47.4.0 [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [moonlight] Starting version check at https://raw.githubusercontent.com/MehVahdJukaar/Moonlight/multi-loader/forge/update.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [moonlight] Found status: BETA Current: 1.20-2.13.51 Target: null [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [configuration] Starting version check at https://raw.githubusercontent.com/Toma1O6/UpdateSchemas/master/configuration-forge.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [configuration] Found status: OUTDATED Current: 2.2.0 Target: 2.2.1 [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [smoothboot] Starting version check at https://github.com/AbdElAziz333/SmoothBoot-Reloaded/raw/mc1.20.1/dev/updates.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [smoothboot] Found status: UP_TO_DATE Current: 0.0.4 Target: null [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [ksyxis] Starting version check at https://raw.githubusercontent.com/VidTu/Ksyxis/main/updater_ksyxis_forge.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [ksyxis] Found status: OUTDATED Current: 1.3.2 Target: 1.3.3 [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [flywheel] Starting version check at https://api.modrinth.com/updates/flywheel/forge_updates.json [12:47:50] [Worker-ResourceReload-4/ERROR] [xa.ma.WorldMap/]: io exception while checking versions: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [flywheel] Found status: BETA Current: 0.6.10-7 Target: null [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [inventoryhud] Starting version check at https://raw.githubusercontent.com/DmitryLovin/pluginUpdate/master/invupdate.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [inventoryhud] Found status: UP_TO_DATE Current: 3.4.26 Target: null [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzleslib] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/puzzleslib.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzleslib] Found status: OUTDATED Current: 8.1.23 Target: 8.1.32 [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [betterf3] Starting version check at https://api.modrinth.com/updates/betterf3/forge_updates.json [12:47:50] [Render thread/INFO] [xa.mi.XaeroMinimap/]: Loading Xaero's Minimap - Stage 2/2 [12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [betterf3] Found status: UP_TO_DATE Current: 7.0.2 Target: null [12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [packetfixer] Starting version check at https://api.modrinth.com/updates/packet-fixer/forge_updates.json [12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [packetfixer] Found status: OUTDATED Current: 1.4.2 Target: 2.0.0 [12:47:51] [Render thread/WARN] [xa.hu.mi.MinimapLogs/]: io exception while checking versions: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Registered player tracker system: minimap_synced [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: World Map found! [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Registered player tracker system: openpartiesandclaims [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Open Parties And Claims found! [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: No Optifine! [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: No Vivecraft! [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Framed Blocks found! [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Iris found! [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Loading Xaero's World Map - Stage 2/2 [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: New world map region cache hash code: -815523079 [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: map_synced [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's WorldMap Mod: Xaero's minimap found! [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: minimap_synced [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: openpartiesandclaims [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's WorldMap Mod: Open Parties And Claims found! [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: No Optifine! [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: No Vivecraft! [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: Framed Blocks found! [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: Iris found! [12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model firmaciv:firmaciv_compass#inventory: minecraft:textures/atlas/blocks.png:minecraft:item/compass [12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model gtceu:tin_double_ingot#inventory: minecraft:textures/atlas/blocks.png:gtceu:item/material_sets/dull/ingot_double_overlay [12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model createaddition:small_light_connector#facing=west,mode=push,powered=true,rotation=x_clockwise_180,variant=default: minecraft:textures/atlas/blocks.png:create:block/chute_block [12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model gtceu:copper_double_ingot#inventory: minecraft:textures/atlas/blocks.png:gtceu:item/material_sets/dull/ingot_double_overlay Reloading Dynamic Lights [12:47:57] [Render thread/INFO] [co.jo.fl.ba.Backend/]: Loaded all shader sources. Create Crafts & Additions Initialized! [12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Loaded values for 19 compatible attributes. [12:47:57] [Worker-ResourceReload-2/ERROR] [AttributeFix/]: Attribute ID 'minecolonies:mc_mob_damage' does not belong to a known attribute. This entry will be ignored. [12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Loaded 20 values from config. [12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Saving config file. 20 entries. [12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Applying changes for 20 attributes. [12:47:57] [Worker-ResourceReload-11/INFO] [de.me.as.AstikorCarts/]: Automatic pull animal configuration: pull_animals = [ "minecraft:camel", "minecraft:donkey", "minecraft:horse", "minecraft:mule", "minecraft:skeleton_horse", "minecraft:zombie_horse", "minecraft:player", "tfc:donkey", "tfc:mule", "tfc:horse" ] [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at io.github.mortuusars.exposure.integration.jade.ExposureJadePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at de.maxhenkel.corpse.integration.waila.PluginCorpse [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at xfacthd.framedblocks.common.compat.jade.FramedJadePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.general.GeneralPlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.create.CreatePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at me.pandamods.fallingtrees.compat.JadePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.gregtechceu.gtceu.integration.jade.GTJadePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at net.dries007.tfc.compat.jade.JadeIntegration [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.vanilla.VanillaPlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.universal.UniversalPlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.core.CorePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at appeng.integration.modules.jade.JadeModule [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.glodblock.github.extendedae.xmod.jade.JadePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at cy.jdkdigital.treetap.compat.jade.JadePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.eerussianguy.firmalife.compat.tooltip.JadeIntegration [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.ljuangbminecraft.tfcchannelcasting.compat.JadeIntegration [12:47:59] [Render thread/WARN] [ne.mi.fm.DeferredWorkQueue/LOADING]: Mod 'create_connected' took 1.342 s to run a deferred task. [12:47:59] [Render thread/INFO] [defaultoptions/]: Loaded default options for keymappings [ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) [12:47:59] [Render thread/INFO] [mojang/Library]: OpenAL initialized on device Starship/Matisse HD Audio Controller Analog Stereo [12:47:59] [Render thread/INFO] [minecraft/SoundEngine]: Sound engine started [12:47:59] [Render thread/INFO] [minecraft/SoundEngine]: [FANCYMENU] Reloading AudioResourceHandler after Minecraft SoundEngine reload.. [12:47:59] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 4096x2048x4 minecraft:textures/atlas/blocks.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 1024x512x4 minecraft:textures/atlas/signs.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 2048x1024x4 minecraft:textures/atlas/armor_trims.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 1024x1024x4 minecraft:textures/atlas/chest.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh particle. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_solid. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_solid. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_cutout_mipped. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_cutout_mipped. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_cutout. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_cutout. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_translucent. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_armor_cutout_no_cull. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_solid. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout_no_cull. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout_no_cull_z_offset. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_translucent_cull. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_translucent. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader rendertype_entity_translucent_emissive could not find sampler named Sampler2 in the specified shader program. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_smooth_cutout. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_decal. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_no_outline. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find sampler named Sampler2 in the specified shader program. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find uniform named IViewRotMat in the specified shader program. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find uniform named FogShape in the specified shader program. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find sampler named Sampler2 in the specified shader program. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find uniform named Light0_Direction in the specified shader program. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find uniform named Light1_Direction in the specified shader program. [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 2048x1024x0 minecraft:textures/atlas/particles.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x256x0 minecraft:textures/atlas/paintings.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x128x0 minecraft:textures/atlas/mob_effects.png-atlas [12:48:00] [Render thread/INFO] [xa.ma.WorldMap/]: Successfully reloaded the world map shaders! [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Loading exposure filters: [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:light_blue_pane, exposure:shaders/light_blue_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:orange_pane, exposure:shaders/orange_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:red_pane, exposure:shaders/red_filter.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:purple_pane, exposure:shaders/purple_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:blue_pane, exposure:shaders/blue_filter.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:light_gray_pane, exposure:shaders/light_gray_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:magenta_pane, exposure:shaders/magenta_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:gray_pane, exposure:shaders/gray_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:lime_pane, exposure:shaders/lime_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:green_pane, exposure:shaders/green_filter.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:pink_pane, exposure:shaders/pink_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:yellow_pane, exposure:shaders/yellow_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:white_pane, exposure:shaders/white_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:brown_pane, exposure:shaders/brown_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:glass_pane, exposure:shaders/crisp.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:interplanar_projector, exposure:shaders/invert.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:black_pane, exposure:shaders/black_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:cyan_pane, exposure:shaders/cyan_tint.json] added. [12:48:00] [Render thread/INFO] [patchouli/]: BookContentResourceListenerLoader preloaded 1073 jsons [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 128x128x0 computercraft:textures/atlas/gui.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x0 polylib:textures/atlas/gui.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x128x0 jei:textures/atlas/gui.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x256x0 moonlight:textures/atlas/map_markers.png-atlas [12:48:00] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Successfully reloaded the minimap shaders! [12:48:00] [Render thread/INFO] [Shimmer/]: buildIn shimmer configuration is enabled, this can be disabled by config file [12:48:00] [Render thread/INFO] [Shimmer/]: mod jar and resource pack discovery: file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] [12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] [12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_iron_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] [12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] [12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_iron_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] [12:48:00] [Render thread/INFO] [de.ke.fa.ut.re.ResourceHandlers/]: [FANCYMENU] Reloading resources.. [12:48:00] [Render thread/INFO] [de.ke.fa.ut.re.pr.ResourcePreLoader/]: [FANCYMENU] Pre-loading resources.. [12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Updating animation sizes.. [12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Minecraft resource reload: FINISHED [12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] ScreenCustomizationLayer registered: title_screen [12:48:00] [Render thread/INFO] [Oculus/]: Creating pipeline for dimension NamespacedId{namespace='minecraft', name='overworld'} [12:48:01] [Render thread/INFO] [ambientsounds/]: Loaded AmbientEngine 'basic' v3.1.0. 11 dimension(s), 11 features, 11 blockgroups, 2 sound collections, 37 regions, 58 sounds, 11 sound categories, 5 solids and 2 biome types [12:48:01] [Render thread/INFO] [FirstPersonModel/]: PlayerAnimator not found! [12:48:01] [Render thread/INFO] [FirstPersonModel/]: Loaded Vanilla Hands items: [] [12:48:01] [Render thread/INFO] [FirstPersonModel/]: Loaded Auto Disable items: [camera] [12:48:02] [Render thread/WARN] [ModernFix/]: Game took 40.304 seconds to start PrismLauncher-11.0.3/tests/testdata/TestLogs/vanilla-1.21.5.text.log0000644000175100017510000000462515224505336024347 0ustar runnerrunner[12:50:56] [Datafixer Bootstrap/INFO]: 263 Datafixer optimizations took 908 milliseconds [12:50:58] [Render thread/INFO]: Environment: Environment[sessionHost=https://sessionserver.mojang.com, servicesHost=https://api.minecraftservices.com, name=PROD] [12:50:58] [Render thread/INFO]: Setting user: Ryexandrite [12:50:58] [Render thread/INFO]: Backend library: LWJGL version 3.3.3+5 [12:50:58] [Render thread/INFO]: Using optional rendering extensions: GL_KHR_debug, GL_ARB_vertex_attrib_binding, GL_ARB_direct_state_access [12:50:58] [Render thread/INFO]: Reloading ResourceManager: vanilla [12:50:59] [Worker-Main-6/INFO]: Found unifont_all_no_pua-16.0.01.hex, loading [12:50:59] [Worker-Main-7/INFO]: Found unifont_jp_patch-16.0.01.hex, loading [12:50:59] [Render thread/WARN]: minecraft:pipeline/entity_translucent_emissive shader program does not use sampler Sampler2 defined in the pipeline. This might be a bug. [12:50:59] [Render thread/INFO]: OpenAL initialized on device Starship/Matisse HD Audio Controller Analog Stereo [12:50:59] [Render thread/INFO]: Sound engine started [12:50:59] [Render thread/INFO]: Created: 1024x512x4 minecraft:textures/atlas/blocks.png-atlas [12:50:59] [Render thread/INFO]: Created: 256x256x4 minecraft:textures/atlas/signs.png-atlas [12:50:59] [Render thread/INFO]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas [12:50:59] [Render thread/INFO]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas [12:50:59] [Render thread/INFO]: Created: 2048x1024x4 minecraft:textures/atlas/armor_trims.png-atlas [12:50:59] [Render thread/INFO]: Created: 256x256x4 minecraft:textures/atlas/chest.png-atlas [12:50:59] [Render thread/INFO]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas [12:50:59] [Render thread/INFO]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas [12:50:59] [Render thread/INFO]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas [12:50:59] [Render thread/INFO]: Created: 64x64x0 minecraft:textures/atlas/map_decorations.png-atlas [12:50:59] [Render thread/INFO]: Created: 512x256x0 minecraft:textures/atlas/particles.png-atlas [12:51:00] [Render thread/INFO]: Created: 512x256x0 minecraft:textures/atlas/paintings.png-atlas [12:51:00] [Render thread/INFO]: Created: 256x128x0 minecraft:textures/atlas/mob_effects.png-atlas [12:51:00] [Render thread/INFO]: Created: 1024x512x0 minecraft:textures/atlas/gui.png-atlas PrismLauncher-11.0.3/tests/testdata/FileSystem/0000755000175100017510000000000015224505336021022 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/FileSystem/FileSystem-test_createShortcut-unix0000755000175100017510000000013615224505336030051 0ustar runnerrunner[Desktop Entry] Type=Application TryExec=asdfDest Exec=asdfDest 'arg1' 'arg2' Name=asdf Icon= PrismLauncher-11.0.3/tests/testdata/FileSystem/test_folder/0000755000175100017510000000000015224505336023334 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/FileSystem/test_folder/pack.mcmeta0000644000175100017510000000013015224505336025434 0ustar runnerrunner{ "pack": { "pack_format": 1, "description": "Some resource pack maybe" } } PrismLauncher-11.0.3/tests/testdata/FileSystem/test_folder/pack.nfo0000644000175100017510000000000215224505336024746 0ustar runnerrunner PrismLauncher-11.0.3/tests/testdata/FileSystem/test_folder/.secret_folder/0000755000175100017510000000000015224505336026232 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/FileSystem/test_folder/.secret_folder/.secret_file.txt0000644000175100017510000000003515224505336031333 0ustar runnerrunneroooooo spooky easter egg :oo PrismLauncher-11.0.3/tests/testdata/FileSystem/test_folder/assets/0000755000175100017510000000000015224505336024636 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/FileSystem/test_folder/assets/minecraft/0000755000175100017510000000000015224505336026606 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/FileSystem/test_folder/assets/minecraft/textures/0000755000175100017510000000000015224505336030471 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/FileSystem/test_folder/assets/minecraft/textures/blah.txt0000644000175100017510000000000215224505336032130 0ustar runnerrunner PrismLauncher-11.0.3/tests/testdata/Resources/0000755000175100017510000000000015224505336020710 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/Resources/test_folder/0000755000175100017510000000000015224505336023222 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/Resources/test_folder/pack.mcmeta0000644000175100017510000000013015224505336025322 0ustar runnerrunner{ "pack": { "pack_format": 1, "description": "Some resource pack maybe" } } PrismLauncher-11.0.3/tests/testdata/Resources/test_folder/pack.nfo0000644000175100017510000000000215224505336024634 0ustar runnerrunner PrismLauncher-11.0.3/tests/testdata/Resources/test_folder/assets/0000755000175100017510000000000015224505336024524 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/Resources/test_folder/assets/minecraft/0000755000175100017510000000000015224505336026474 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/Resources/test_folder/assets/minecraft/textures/0000755000175100017510000000000015224505336030357 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/Resources/test_folder/assets/minecraft/textures/blah.txt0000644000175100017510000000000215224505336032016 0ustar runnerrunner PrismLauncher-11.0.3/tests/testdata/Resources/another_test_folder/0000755000175100017510000000000015224505336024742 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/Resources/another_test_folder/pack.mcmeta0000644000175100017510000000022715224505336027051 0ustar runnerrunner{ "pack": { "pack_format": 6, "description": "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional" } } PrismLauncher-11.0.3/tests/testdata/Resources/test_resource_pack_idk.zip0000644000175100017510000000144415224505336026152 0ustar runnerrunnerPK3vU G pack.mcmetaUT McMcux E10 Eg*nll\4E;ehi5r1LHello, Component!" } PrismLauncher-11.0.3/tests/testdata/MetaComponentParse/component_with_mixed.json0000644000175100017510000000315415224505336027623 0ustar runnerrunner{ "description": [ { "text": "The quick ", "color": "blue", "italic": true }, { "text": "brown fox ", "color": "#873600", "bold": true, "underlined": true, "extra": [ { "text": "jumped over ", "color": "blue", "bold": false, "underlined": false, "italic": true, "strikethrough": true } ] }, { "text": "the lazy dog's back. ", "color": "green", "bold": true, "italic": true, "underlined": true, "strikethrough": true, "extra": [ { "text": "1234567890 ", "color": "black", "strikethrough": false, "extra": [ "How vexingly quick daft zebras jump!" ] } ] } ], "expected_output": "The quick brown fox jumped over the lazy dog's back. 1234567890 How vexingly quick daft zebras jump!" } PrismLauncher-11.0.3/tests/testdata/MetaComponentParse/component_basic.json0000644000175100017510000000021715224505336026540 0ustar runnerrunner{ "description": [ { "text": "Hello, Component!" } ], "expected_output": "Hello, Component!" } PrismLauncher-11.0.3/tests/testdata/MetaComponentParse/component_with_extra.json0000644000175100017510000000114015224505336027631 0ustar runnerrunner{ "description": [ { "text": "Hello, ", "color": "red", "bold": true, "italic": true, "extra": [ { "extra": [ "Component!" ], "bold": false, "italic": false } ] } ], "expected_output": "Hello, Component!" }PrismLauncher-11.0.3/tests/testdata/MetaComponentParse/component_with_format.json0000644000175100017510000000060115224505336027777 0ustar runnerrunner{ "description": [ { "text": "Hello, Component!", "color": "blue", "bold": true, "italic": true, "underlined": true, "strikethrough": true } ], "expected_output": "Hello, Component!" }PrismLauncher-11.0.3/tests/testdata/WorldSaveParse/0000755000175100017510000000000015224505336021637 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/WorldSaveParse/minecraft_save_3/0000755000175100017510000000000015224505336025047 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/WorldSaveParse/minecraft_save_3/world_3/0000755000175100017510000000000015224505336026420 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/WorldSaveParse/minecraft_save_3/world_3/level.dat0000644000175100017510000000000015224505336030207 0ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/WorldSaveParse/minecraft_save_4/0000755000175100017510000000000015224505336025050 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/WorldSaveParse/minecraft_save_4/saves/0000755000175100017510000000000015224505336026171 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/WorldSaveParse/minecraft_save_4/saves/world_4/0000755000175100017510000000000015224505336027543 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/WorldSaveParse/minecraft_save_4/saves/world_4/level.dat0000644000175100017510000000000015224505336031332 0ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/WorldSaveParse/minecraft_save_2.zip0000644000175100017510000000054015224505336025571 0ustar runnerrunnerPK €Usaves/world_2/UT ܅c鉧cux PK €Usaves/world_2/level.datUT ܅c܅cux PK €UAsaves/world_2/UT܅cux PK €UHsaves/world_2/level.datUT܅cux PKPrismLauncher-11.0.3/tests/testdata/WorldSaveParse/minecraft_save_1.zip0000644000175100017510000000027015224505336025570 0ustar runnerrunnerPK Uworld_1/level.datUT ccux PK Uworld_1/level.datUTcux PKWKPrismLauncher-11.0.3/tests/testdata/CatPacks/0000755000175100017510000000000015224505336020427 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/CatPacks/index.json0000644000175100017510000000205415224505336022432 0ustar runnerrunner{ "name": "My Cute Cat", "default": "maxwell.png", "variants": [ { "startTime": { "day": 12, "month": 4 }, "endTime": { "day": 12, "month": 4 }, "path": "oneDay.png" }, { "startTime": { "day": 20, "month": 12 }, "endTime": { "day": 28, "month": 12 }, "path": "christmas.png" }, { "startTime": { "day": 30, "month": 12 }, "endTime": { "day": 1, "month": 1 }, "path": "newyear2.png" }, { "startTime": { "day": 28, "month": 12 }, "endTime": { "day": 3, "month": 1 }, "path": "newyear.png" } ] } PrismLauncher-11.0.3/tests/testdata/TexturePackParse/0000755000175100017510000000000015224505336022170 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/TexturePackParse/test_texturefolder/0000755000175100017510000000000015224505336026123 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/TexturePackParse/test_texturefolder/pack.txt0000644000175100017510000000003015224505336027573 0ustar runnerrunnerSome texture pack surelyPrismLauncher-11.0.3/tests/testdata/TexturePackParse/test_texturefolder/assets/0000755000175100017510000000000015224505336027425 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/TexturePackParse/test_texturefolder/assets/minecraft/0000755000175100017510000000000015224505336031375 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/TexturePackParse/test_texturefolder/assets/minecraft/textures/0000755000175100017510000000000015224505336033260 5ustar runnerrunner././@LongLink0000644000000000000000000000015300000000000011602 Lustar rootrootPrismLauncher-11.0.3/tests/testdata/TexturePackParse/test_texturefolder/assets/minecraft/textures/blah.txtPrismLauncher-11.0.3/tests/testdata/TexturePackParse/test_texturefolder/assets/minecraft/textures/bl0000644000175100017510000000000215224505336033570 0ustar runnerrunner PrismLauncher-11.0.3/tests/testdata/TexturePackParse/another_test_texturefolder/0000755000175100017510000000000015224505336027643 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/TexturePackParse/another_test_texturefolder/pack.txt0000644000175100017510000000002015224505336031312 0ustar runnerrunnerquieres for realPrismLauncher-11.0.3/tests/testdata/TexturePackParse/test_texture_pack_idk.zip0000644000175100017510000000027015224505336027277 0ustar runnerrunnerPK t$U bpack.txtUT c!cux joe biden, wake upPK t$U bpack.txtUTcux PKNTPrismLauncher-11.0.3/tests/testdata/PackageManifest/0000755000175100017510000000000015224505336021760 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/PackageManifest/inspect/0000755000175100017510000000000015224505336023425 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/PackageManifest/inspect/a/0000755000175100017510000000000015224505336023645 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/PackageManifest/inspect/a/b.txt0000755000175100017510000000000015224505336024620 0ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/PackageManifest/inspect/a/b/0000755000175100017510000000000015224505336024066 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/PackageManifest/inspect/a/b/b.txt0000777000175100017510000000000015224505336026242 2../b.txtustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/PackageManifest/1.8.0_202-x64.json0000644000175100017510000060172315224505336024332 0ustar runnerrunner{ "files": { "COPYRIGHT": { "downloads": { "lzma": { "sha1": "dd860e040807f7e53ae89da5f28dd73d57ac605d", "size": 1431, "url": "https://launcher.mojang.com/v1/objects/dd860e040807f7e53ae89da5f28dd73d57ac605d/COPYRIGHT" }, "raw": { "sha1": "c725183c757011e7ba96c83c1e86ee7e8b516a2b", "size": 3244, "url": "https://launcher.mojang.com/v1/objects/c725183c757011e7ba96c83c1e86ee7e8b516a2b/COPYRIGHT" } }, "executable": false, "type": "file" }, "LICENSE": { "downloads": { "raw": { "sha1": "3e86865deec0814c958bcf7fb87f790bccc0e8bd", "size": 40, "url": "https://launcher.mojang.com/v1/objects/3e86865deec0814c958bcf7fb87f790bccc0e8bd/LICENSE" } }, "executable": false, "type": "file" }, "README": { "downloads": { "raw": { "sha1": "f90331df1e5badeadc501d8dd70714c62a920204", "size": 46, "url": "https://launcher.mojang.com/v1/objects/f90331df1e5badeadc501d8dd70714c62a920204/README" } }, "executable": false, "type": "file" }, "THIRDPARTYLICENSEREADME-JAVAFX.txt": { "downloads": { "lzma": { "sha1": "4fee85109d7ff04b982d0576dabd15397f599125", "size": 15455, "url": "https://launcher.mojang.com/v1/objects/4fee85109d7ff04b982d0576dabd15397f599125/THIRDPARTYLICENSEREADME-JAVAFX.txt" }, "raw": { "sha1": "56ff42f87607b997b52ae0ef8bf315e36932e870", "size": 112724, "url": "https://launcher.mojang.com/v1/objects/56ff42f87607b997b52ae0ef8bf315e36932e870/THIRDPARTYLICENSEREADME-JAVAFX.txt" } }, "executable": false, "type": "file" }, "THIRDPARTYLICENSEREADME.txt": { "downloads": { "lzma": { "sha1": "419c1414ba46ae9dbfd38cf4e0601fff61644429", "size": 32266, "url": "https://launcher.mojang.com/v1/objects/419c1414ba46ae9dbfd38cf4e0601fff61644429/THIRDPARTYLICENSEREADME.txt" }, "raw": { "sha1": "b83c3f32261de3e48ccd20614a11e066b1ec9027", "size": 153824, "url": "https://launcher.mojang.com/v1/objects/b83c3f32261de3e48ccd20614a11e066b1ec9027/THIRDPARTYLICENSEREADME.txt" } }, "executable": false, "type": "file" }, "Welcome.html": { "downloads": { "lzma": { "sha1": "01c21a74b4aafb7cbe0388233c43cbdf77dcaaea", "size": 528, "url": "https://launcher.mojang.com/v1/objects/01c21a74b4aafb7cbe0388233c43cbdf77dcaaea/Welcome.html" }, "raw": { "sha1": "d98ae54f03dac87419abc19b97e315830c2da55f", "size": 955, "url": "https://launcher.mojang.com/v1/objects/d98ae54f03dac87419abc19b97e315830c2da55f/Welcome.html" } }, "executable": false, "type": "file" }, "bin": { "type": "directory" }, "bin/ControlPanel": { "target": "jcontrol", "type": "link" }, "bin/java": { "downloads": { "lzma": { "sha1": "3857eea1d59e1bc545c67a753ed2768254807b8a", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/3857eea1d59e1bc545c67a753ed2768254807b8a/java" }, "raw": { "sha1": "3d20560fb5d1a49cb689c2226972e92e06d27ba6", "size": 8464, "url": "https://launcher.mojang.com/v1/objects/3d20560fb5d1a49cb689c2226972e92e06d27ba6/java" } }, "executable": true, "type": "file" }, "bin/javaws": { "downloads": { "lzma": { "sha1": "a6bec5c049e76c4488294a256a2084ea23ddb440", "size": 38173, "url": "https://launcher.mojang.com/v1/objects/a6bec5c049e76c4488294a256a2084ea23ddb440/javaws" }, "raw": { "sha1": "955c0f0066e2f893b0c2b3ccd83e223722e4ab74", "size": 140296, "url": "https://launcher.mojang.com/v1/objects/955c0f0066e2f893b0c2b3ccd83e223722e4ab74/javaws" } }, "executable": true, "type": "file" }, "bin/jcontrol": { "downloads": { "lzma": { "sha1": "40c5e33748f252e1d950b579a4185ab2c23fc908", "size": 2166, "url": "https://launcher.mojang.com/v1/objects/40c5e33748f252e1d950b579a4185ab2c23fc908/jcontrol" }, "raw": { "sha1": "ed541733c8b51e34349c1f8010b277e58ad73f1e", "size": 6264, "url": "https://launcher.mojang.com/v1/objects/ed541733c8b51e34349c1f8010b277e58ad73f1e/jcontrol" } }, "executable": true, "type": "file" }, "bin/jjs": { "downloads": { "lzma": { "sha1": "d44d1ac421979f7671921986214812095a5b0e3b", "size": 2168, "url": "https://launcher.mojang.com/v1/objects/d44d1ac421979f7671921986214812095a5b0e3b/jjs" }, "raw": { "sha1": "f00f944c3dbe556793b5dc686aaeee3e5722e99b", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/f00f944c3dbe556793b5dc686aaeee3e5722e99b/jjs" } }, "executable": true, "type": "file" }, "bin/keytool": { "downloads": { "lzma": { "sha1": "93c607dce450976667c382f609a367167bdec05c", "size": 2175, "url": "https://launcher.mojang.com/v1/objects/93c607dce450976667c382f609a367167bdec05c/keytool" }, "raw": { "sha1": "7114b561546270e441e9ed1bcc24e5188c068a42", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/7114b561546270e441e9ed1bcc24e5188c068a42/keytool" } }, "executable": true, "type": "file" }, "bin/orbd": { "downloads": { "lzma": { "sha1": "b27dfded5e2b2f6f02c555971c94e46ca14ac81b", "size": 2254, "url": "https://launcher.mojang.com/v1/objects/b27dfded5e2b2f6f02c555971c94e46ca14ac81b/orbd" }, "raw": { "sha1": "7f31217fecb3dbbd89f1dd3783fca58793a66fd2", "size": 8656, "url": "https://launcher.mojang.com/v1/objects/7f31217fecb3dbbd89f1dd3783fca58793a66fd2/orbd" } }, "executable": true, "type": "file" }, "bin/pack200": { "downloads": { "lzma": { "sha1": "b52da4497b49b1508b6225a5740857ddb8f52e97", "size": 2183, "url": "https://launcher.mojang.com/v1/objects/b52da4497b49b1508b6225a5740857ddb8f52e97/pack200" }, "raw": { "sha1": "16ef3e801efb57e50bc6477a27a9d95d02d0775b", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/16ef3e801efb57e50bc6477a27a9d95d02d0775b/pack200" } }, "executable": true, "type": "file" }, "bin/policytool": { "downloads": { "lzma": { "sha1": "87da4c07da45f3d1a1a9d732af197cd39bf69d10", "size": 2182, "url": "https://launcher.mojang.com/v1/objects/87da4c07da45f3d1a1a9d732af197cd39bf69d10/policytool" }, "raw": { "sha1": "a52a29424470cb9b8db5c2fb1751d0b697a7ec8e", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/a52a29424470cb9b8db5c2fb1751d0b697a7ec8e/policytool" } }, "executable": true, "type": "file" }, "bin/rmid": { "downloads": { "lzma": { "sha1": "1494c1174fde0c0a93ea117bc7edf7eb936c0512", "size": 2172, "url": "https://launcher.mojang.com/v1/objects/1494c1174fde0c0a93ea117bc7edf7eb936c0512/rmid" }, "raw": { "sha1": "5c8710e1ab924e5b09a07bcb4c6e106293bbd1a8", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/5c8710e1ab924e5b09a07bcb4c6e106293bbd1a8/rmid" } }, "executable": true, "type": "file" }, "bin/rmiregistry": { "downloads": { "lzma": { "sha1": "7070cf2ec5a5e520a880bae699431edf02083e7e", "size": 2174, "url": "https://launcher.mojang.com/v1/objects/7070cf2ec5a5e520a880bae699431edf02083e7e/rmiregistry" }, "raw": { "sha1": "5f518daa7050028d5d9d849634c73136f2b23a54", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/5f518daa7050028d5d9d849634c73136f2b23a54/rmiregistry" } }, "executable": true, "type": "file" }, "bin/servertool": { "downloads": { "lzma": { "sha1": "1db683a11cc9b7313426c84412f4d95be2fa7ccd", "size": 2185, "url": "https://launcher.mojang.com/v1/objects/1db683a11cc9b7313426c84412f4d95be2fa7ccd/servertool" }, "raw": { "sha1": "49d0ebfeb265ce5a8733e1014541ea2525674a60", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/49d0ebfeb265ce5a8733e1014541ea2525674a60/servertool" } }, "executable": true, "type": "file" }, "bin/tnameserv": { "downloads": { "lzma": { "sha1": "36da9c9a2c5a8b662a3f8d52ca67339bce1c2714", "size": 2291, "url": "https://launcher.mojang.com/v1/objects/36da9c9a2c5a8b662a3f8d52ca67339bce1c2714/tnameserv" }, "raw": { "sha1": "09d998f8efcb6f55d0d87f59e08f8b89662796d9", "size": 8656, "url": "https://launcher.mojang.com/v1/objects/09d998f8efcb6f55d0d87f59e08f8b89662796d9/tnameserv" } }, "executable": true, "type": "file" }, "bin/unpack200": { "downloads": { "lzma": { "sha1": "344959e32fc7ee19eebe7b3cf5ab6d1a7d6641f2", "size": 79721, "url": "https://launcher.mojang.com/v1/objects/344959e32fc7ee19eebe7b3cf5ab6d1a7d6641f2/unpack200" }, "raw": { "sha1": "5dd933132f1b202e19e0c8e093f7113711cfdfc1", "size": 182616, "url": "https://launcher.mojang.com/v1/objects/5dd933132f1b202e19e0c8e093f7113711cfdfc1/unpack200" } }, "executable": true, "type": "file" }, "lib": { "type": "directory" }, "lib/amd64": { "type": "directory" }, "lib/amd64/jli": { "type": "directory" }, "lib/amd64/jli/libjli.so": { "downloads": { "lzma": { "sha1": "372331ee8e375888f798a2e88180a94493e141b0", "size": 48327, "url": "https://launcher.mojang.com/v1/objects/372331ee8e375888f798a2e88180a94493e141b0/libjli.so" }, "raw": { "sha1": "73b0cf8b7415686bc40c561ff77ff2740ccf7a44", "size": 108616, "url": "https://launcher.mojang.com/v1/objects/73b0cf8b7415686bc40c561ff77ff2740ccf7a44/libjli.so" } }, "executable": true, "type": "file" }, "lib/amd64/jvm.cfg": { "downloads": { "lzma": { "sha1": "86bcfebec37b38415525ffd77d3eaf70d0b1b4ca", "size": 435, "url": "https://launcher.mojang.com/v1/objects/86bcfebec37b38415525ffd77d3eaf70d0b1b4ca/jvm.cfg" }, "raw": { "sha1": "84b38bdc745de446ba0ca0232ea3aaf2efd721da", "size": 627, "url": "https://launcher.mojang.com/v1/objects/84b38bdc745de446ba0ca0232ea3aaf2efd721da/jvm.cfg" } }, "executable": false, "type": "file" }, "lib/amd64/libavplugin-53.so": { "downloads": { "lzma": { "sha1": "a332366762d9efc7b845a682b7edce62db44618c", "size": 14747, "url": "https://launcher.mojang.com/v1/objects/a332366762d9efc7b845a682b7edce62db44618c/libavplugin-53.so" }, "raw": { "sha1": "9bd1473dd8a0dc7950c7af1cc69a45548df26eb5", "size": 51720, "url": "https://launcher.mojang.com/v1/objects/9bd1473dd8a0dc7950c7af1cc69a45548df26eb5/libavplugin-53.so" } }, "executable": true, "type": "file" }, "lib/amd64/libavplugin-54.so": { "downloads": { "lzma": { "sha1": "2c615852a0720a275163e00597c1f711f11341da", "size": 15153, "url": "https://launcher.mojang.com/v1/objects/2c615852a0720a275163e00597c1f711f11341da/libavplugin-54.so" }, "raw": { "sha1": "8808050c5949c4800b42d1b19b1f8b0d120bcacb", "size": 51768, "url": "https://launcher.mojang.com/v1/objects/8808050c5949c4800b42d1b19b1f8b0d120bcacb/libavplugin-54.so" } }, "executable": true, "type": "file" }, "lib/amd64/libavplugin-55.so": { "downloads": { "lzma": { "sha1": "39ee8e7fe14f0010c78973962800f539c3e4c16b", "size": 15168, "url": "https://launcher.mojang.com/v1/objects/39ee8e7fe14f0010c78973962800f539c3e4c16b/libavplugin-55.so" }, "raw": { "sha1": "f10ea4ea3489e96d8d161a96790133c417ec44e1", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/f10ea4ea3489e96d8d161a96790133c417ec44e1/libavplugin-55.so" } }, "executable": true, "type": "file" }, "lib/amd64/libavplugin-56.so": { "downloads": { "lzma": { "sha1": "abe7feced5a559f1bdc868526dc69484e0e591a0", "size": 15169, "url": "https://launcher.mojang.com/v1/objects/abe7feced5a559f1bdc868526dc69484e0e591a0/libavplugin-56.so" }, "raw": { "sha1": "e5bfcbff5a5a5a5993a3e689a05ef358c131a3ed", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/e5bfcbff5a5a5a5993a3e689a05ef358c131a3ed/libavplugin-56.so" } }, "executable": true, "type": "file" }, "lib/amd64/libavplugin-57.so": { "downloads": { "lzma": { "sha1": "4dd26b4ef2294b6929dcb2c7546b47eac5cc78a9", "size": 15174, "url": "https://launcher.mojang.com/v1/objects/4dd26b4ef2294b6929dcb2c7546b47eac5cc78a9/libavplugin-57.so" }, "raw": { "sha1": "2949e7ff9b0ac90e8943c211cff141ab12eec3f8", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/2949e7ff9b0ac90e8943c211cff141ab12eec3f8/libavplugin-57.so" } }, "executable": true, "type": "file" }, "lib/amd64/libavplugin-ffmpeg-56.so": { "downloads": { "lzma": { "sha1": "c688ba1cfa442bf18bee43b2fa870b4dc1ce3fb6", "size": 15231, "url": "https://launcher.mojang.com/v1/objects/c688ba1cfa442bf18bee43b2fa870b4dc1ce3fb6/libavplugin-ffmpeg-56.so" }, "raw": { "sha1": "0d36c971a9ad99fc2292092fdec3a4179b1021b9", "size": 51920, "url": "https://launcher.mojang.com/v1/objects/0d36c971a9ad99fc2292092fdec3a4179b1021b9/libavplugin-ffmpeg-56.so" } }, "executable": true, "type": "file" }, "lib/amd64/libavplugin-ffmpeg-57.so": { "downloads": { "lzma": { "sha1": "087426bdbffebcfa372a438e863785f4ffbe9a6b", "size": 15180, "url": "https://launcher.mojang.com/v1/objects/087426bdbffebcfa372a438e863785f4ffbe9a6b/libavplugin-ffmpeg-57.so" }, "raw": { "sha1": "5e9c4eb4b49eb8e57c01003ec73a1eb8d6d8c462", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/5e9c4eb4b49eb8e57c01003ec73a1eb8d6d8c462/libavplugin-ffmpeg-57.so" } }, "executable": true, "type": "file" }, "lib/amd64/libawt.so": { "downloads": { "lzma": { "sha1": "018be58b205b73c842a55df811b70d0e8237216e", "size": 195720, "url": "https://launcher.mojang.com/v1/objects/018be58b205b73c842a55df811b70d0e8237216e/libawt.so" }, "raw": { "sha1": "02632cd326e3161c00a7e784599dd7b9ee053dce", "size": 759184, "url": "https://launcher.mojang.com/v1/objects/02632cd326e3161c00a7e784599dd7b9ee053dce/libawt.so" } }, "executable": true, "type": "file" }, "lib/amd64/libawt_headless.so": { "downloads": { "lzma": { "sha1": "7ac2517cff75d4bbb0a0412a9b5f18c74ea188fa", "size": 11211, "url": "https://launcher.mojang.com/v1/objects/7ac2517cff75d4bbb0a0412a9b5f18c74ea188fa/libawt_headless.so" }, "raw": { "sha1": "862157ec957008d0911c5daedc004b3a202623a4", "size": 39176, "url": "https://launcher.mojang.com/v1/objects/862157ec957008d0911c5daedc004b3a202623a4/libawt_headless.so" } }, "executable": true, "type": "file" }, "lib/amd64/libawt_xawt.so": { "downloads": { "lzma": { "sha1": "d536a96af27dfe35de6bb2c8759d51c488cdd8d4", "size": 149598, "url": "https://launcher.mojang.com/v1/objects/d536a96af27dfe35de6bb2c8759d51c488cdd8d4/libawt_xawt.so" }, "raw": { "sha1": "28232b3e01b6f11bfe098bfc6eafc3a513dcebf1", "size": 470232, "url": "https://launcher.mojang.com/v1/objects/28232b3e01b6f11bfe098bfc6eafc3a513dcebf1/libawt_xawt.so" } }, "executable": true, "type": "file" }, "lib/amd64/libbci.so": { "downloads": { "lzma": { "sha1": "c36fad091d11e64c815d5ca17c0ef7a55b0776b1", "size": 3509, "url": "https://launcher.mojang.com/v1/objects/c36fad091d11e64c815d5ca17c0ef7a55b0776b1/libbci.so" }, "raw": { "sha1": "33824051db1ccb6332e22c2b63231055240d0af0", "size": 12760, "url": "https://launcher.mojang.com/v1/objects/33824051db1ccb6332e22c2b63231055240d0af0/libbci.so" } }, "executable": true, "type": "file" }, "lib/amd64/libdcpr.so": { "downloads": { "lzma": { "sha1": "70c6b0933a37f2b1124d6e7c131039241fe796ee", "size": 75969, "url": "https://launcher.mojang.com/v1/objects/70c6b0933a37f2b1124d6e7c131039241fe796ee/libdcpr.so" }, "raw": { "sha1": "fa7001bc5d80579e2716590f3eee8027da0beae7", "size": 204456, "url": "https://launcher.mojang.com/v1/objects/fa7001bc5d80579e2716590f3eee8027da0beae7/libdcpr.so" } }, "executable": true, "type": "file" }, "lib/amd64/libdecora_sse.so": { "downloads": { "lzma": { "sha1": "514acc017dfb6cefaf8cc6d18006ce55781cc9bc", "size": 24397, "url": "https://launcher.mojang.com/v1/objects/514acc017dfb6cefaf8cc6d18006ce55781cc9bc/libdecora_sse.so" }, "raw": { "sha1": "d0c84233504c916e548e29f513e25f6a7479abfc", "size": 74912, "url": "https://launcher.mojang.com/v1/objects/d0c84233504c916e548e29f513e25f6a7479abfc/libdecora_sse.so" } }, "executable": true, "type": "file" }, "lib/amd64/libdeploy.so": { "downloads": { "lzma": { "sha1": "6cf31fd98301c749ac0d2c7825f6d925a4409760", "size": 168999, "url": "https://launcher.mojang.com/v1/objects/6cf31fd98301c749ac0d2c7825f6d925a4409760/libdeploy.so" }, "raw": { "sha1": "b3832e97ed8ca794884b56a591b83d02a2c0c06f", "size": 642368, "url": "https://launcher.mojang.com/v1/objects/b3832e97ed8ca794884b56a591b83d02a2c0c06f/libdeploy.so" } }, "executable": true, "type": "file" }, "lib/amd64/libdt_socket.so": { "downloads": { "lzma": { "sha1": "4cc5c880dbb6fa180436d12d60f0abec8ebb59dc", "size": 7784, "url": "https://launcher.mojang.com/v1/objects/4cc5c880dbb6fa180436d12d60f0abec8ebb59dc/libdt_socket.so" }, "raw": { "sha1": "91ce96f252b8139fc12f0f224ed5b1a041767ab7", "size": 24616, "url": "https://launcher.mojang.com/v1/objects/91ce96f252b8139fc12f0f224ed5b1a041767ab7/libdt_socket.so" } }, "executable": true, "type": "file" }, "lib/amd64/libfontmanager.so": { "downloads": { "lzma": { "sha1": "f94e5e94c71c603ff4d3cd1e7e3d9e181fcc145d", "size": 146951, "url": "https://launcher.mojang.com/v1/objects/f94e5e94c71c603ff4d3cd1e7e3d9e181fcc145d/libfontmanager.so" }, "raw": { "sha1": "2428e805f2c53d1283a033dfd11a86fbb7bd7159", "size": 490672, "url": "https://launcher.mojang.com/v1/objects/2428e805f2c53d1283a033dfd11a86fbb7bd7159/libfontmanager.so" } }, "executable": true, "type": "file" }, "lib/amd64/libfxplugins.so": { "downloads": { "lzma": { "sha1": "a640143365d382a5ad743a784bc2f3706d9d6d67", "size": 50048, "url": "https://launcher.mojang.com/v1/objects/a640143365d382a5ad743a784bc2f3706d9d6d67/libfxplugins.so" }, "raw": { "sha1": "0fd4ac04a84c131f1aaee9e6b0898ff9ea69e3ee", "size": 151448, "url": "https://launcher.mojang.com/v1/objects/0fd4ac04a84c131f1aaee9e6b0898ff9ea69e3ee/libfxplugins.so" } }, "executable": true, "type": "file" }, "lib/amd64/libglass.so": { "downloads": { "lzma": { "sha1": "f1ff517714fa5f2c861f33b32db823fe851541f1", "size": 2856, "url": "https://launcher.mojang.com/v1/objects/f1ff517714fa5f2c861f33b32db823fe851541f1/libglass.so" }, "raw": { "sha1": "e7f4fece30ac727be8148d33b8256abd3a41cef9", "size": 13072, "url": "https://launcher.mojang.com/v1/objects/e7f4fece30ac727be8148d33b8256abd3a41cef9/libglass.so" } }, "executable": true, "type": "file" }, "lib/amd64/libglassgtk2.so": { "downloads": { "lzma": { "sha1": "15b90f7a2baacd15e80aa9785d87cf1e4258376d", "size": 220476, "url": "https://launcher.mojang.com/v1/objects/15b90f7a2baacd15e80aa9785d87cf1e4258376d/libglassgtk2.so" }, "raw": { "sha1": "e30a634c2ff2143bdee512360553d6e0304f33b2", "size": 844984, "url": "https://launcher.mojang.com/v1/objects/e30a634c2ff2143bdee512360553d6e0304f33b2/libglassgtk2.so" } }, "executable": true, "type": "file" }, "lib/amd64/libglassgtk3.so": { "downloads": { "lzma": { "sha1": "868c231165f8c9043b7f0e7de208ec023f06a6e7", "size": 220560, "url": "https://launcher.mojang.com/v1/objects/868c231165f8c9043b7f0e7de208ec023f06a6e7/libglassgtk3.so" }, "raw": { "sha1": "762a11a2b376b7b5a2a7cad780715524fdd176d5", "size": 845304, "url": "https://launcher.mojang.com/v1/objects/762a11a2b376b7b5a2a7cad780715524fdd176d5/libglassgtk3.so" } }, "executable": true, "type": "file" }, "lib/amd64/libglib-lite.so": { "downloads": { "lzma": { "sha1": "61b8871242febe1be262de167dc20ae94bf964b4", "size": 457046, "url": "https://launcher.mojang.com/v1/objects/61b8871242febe1be262de167dc20ae94bf964b4/libglib-lite.so" }, "raw": { "sha1": "63afa060fc3f120af76128e51d32603fc4336fa8", "size": 1538352, "url": "https://launcher.mojang.com/v1/objects/63afa060fc3f120af76128e51d32603fc4336fa8/libglib-lite.so" } }, "executable": true, "type": "file" }, "lib/amd64/libgstreamer-lite.so": { "downloads": { "lzma": { "sha1": "2447dc368406ba1b989a29937d41924620e01988", "size": 673056, "url": "https://launcher.mojang.com/v1/objects/2447dc368406ba1b989a29937d41924620e01988/libgstreamer-lite.so" }, "raw": { "sha1": "5505e7ca592ac64371d3db8fe53bcb602e9723d3", "size": 2263872, "url": "https://launcher.mojang.com/v1/objects/5505e7ca592ac64371d3db8fe53bcb602e9723d3/libgstreamer-lite.so" } }, "executable": true, "type": "file" }, "lib/amd64/libhprof.so": { "downloads": { "lzma": { "sha1": "94a5589c818db1fb1cf1881e24e217c309fce2e4", "size": 64471, "url": "https://launcher.mojang.com/v1/objects/94a5589c818db1fb1cf1881e24e217c309fce2e4/libhprof.so" }, "raw": { "sha1": "4bb9bdeef6133b6dd558d52d691b077c03e9b0ee", "size": 175504, "url": "https://launcher.mojang.com/v1/objects/4bb9bdeef6133b6dd558d52d691b077c03e9b0ee/libhprof.so" } }, "executable": true, "type": "file" }, "lib/amd64/libinstrument.so": { "downloads": { "lzma": { "sha1": "84ffea356caf725b42c86a8ebc9587f477ddde29", "size": 18603, "url": "https://launcher.mojang.com/v1/objects/84ffea356caf725b42c86a8ebc9587f477ddde29/libinstrument.so" }, "raw": { "sha1": "cb8009769601e3fecd7ea2b36c344f737b1a9da7", "size": 51560, "url": "https://launcher.mojang.com/v1/objects/cb8009769601e3fecd7ea2b36c344f737b1a9da7/libinstrument.so" } }, "executable": true, "type": "file" }, "lib/amd64/libj2gss.so": { "downloads": { "lzma": { "sha1": "4b2aa699506b126098b585a9617ce1c05707fa29", "size": 14132, "url": "https://launcher.mojang.com/v1/objects/4b2aa699506b126098b585a9617ce1c05707fa29/libj2gss.so" }, "raw": { "sha1": "cbce4a302b255d4d1924ef7606f038af766c5e86", "size": 47688, "url": "https://launcher.mojang.com/v1/objects/cbce4a302b255d4d1924ef7606f038af766c5e86/libj2gss.so" } }, "executable": true, "type": "file" }, "lib/amd64/libj2pcsc.so": { "downloads": { "lzma": { "sha1": "2361d3b2e3da48593c391b29b0d2b5409e4c55e5", "size": 5074, "url": "https://launcher.mojang.com/v1/objects/2361d3b2e3da48593c391b29b0d2b5409e4c55e5/libj2pcsc.so" }, "raw": { "sha1": "1274178492e7a3e997e12f67794616f7c3d8d0b9", "size": 18296, "url": "https://launcher.mojang.com/v1/objects/1274178492e7a3e997e12f67794616f7c3d8d0b9/libj2pcsc.so" } }, "executable": true, "type": "file" }, "lib/amd64/libj2pkcs11.so": { "downloads": { "lzma": { "sha1": "ef927e2790ba05931d0f0bdd63da3d275a834946", "size": 21573, "url": "https://launcher.mojang.com/v1/objects/ef927e2790ba05931d0f0bdd63da3d275a834946/libj2pkcs11.so" }, "raw": { "sha1": "bd4f2af9bfdc6168633d1920c1a1415de06bb45a", "size": 79472, "url": "https://launcher.mojang.com/v1/objects/bd4f2af9bfdc6168633d1920c1a1415de06bb45a/libj2pkcs11.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjaas_unix.so": { "downloads": { "lzma": { "sha1": "7f7e843544ee1eb1454a5826bdd4218685b79430", "size": 2404, "url": "https://launcher.mojang.com/v1/objects/7f7e843544ee1eb1454a5826bdd4218685b79430/libjaas_unix.so" }, "raw": { "sha1": "4c517925c7d464a5b719898eb0bea1b04df31f1f", "size": 8192, "url": "https://launcher.mojang.com/v1/objects/4c517925c7d464a5b719898eb0bea1b04df31f1f/libjaas_unix.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjava.so": { "downloads": { "lzma": { "sha1": "5eee7a42600a44a8bb8d6d7f510fd96a29637ac0", "size": 63113, "url": "https://launcher.mojang.com/v1/objects/5eee7a42600a44a8bb8d6d7f510fd96a29637ac0/libjava.so" }, "raw": { "sha1": "e280aeddf3fc0ec664aef7efc0e0e197a54aaf02", "size": 227672, "url": "https://launcher.mojang.com/v1/objects/e280aeddf3fc0ec664aef7efc0e0e197a54aaf02/libjava.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjava_crw_demo.so": { "downloads": { "lzma": { "sha1": "b197cf23ae3556eb0b45c663f0a8cb62408b961e", "size": 10412, "url": "https://launcher.mojang.com/v1/objects/b197cf23ae3556eb0b45c663f0a8cb62408b961e/libjava_crw_demo.so" }, "raw": { "sha1": "18f20f906977c90d0090b41dbda8dd5cfead5a4c", "size": 26144, "url": "https://launcher.mojang.com/v1/objects/18f20f906977c90d0090b41dbda8dd5cfead5a4c/libjava_crw_demo.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjavafx_font.so": { "downloads": { "lzma": { "sha1": "ffbba0e5022f829412b86063d8a90f95f16709b1", "size": 5608, "url": "https://launcher.mojang.com/v1/objects/ffbba0e5022f829412b86063d8a90f95f16709b1/libjavafx_font.so" }, "raw": { "sha1": "8634a0aca612fc40420a4a7cc8af4cc46cfc6725", "size": 17104, "url": "https://launcher.mojang.com/v1/objects/8634a0aca612fc40420a4a7cc8af4cc46cfc6725/libjavafx_font.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjavafx_font_freetype.so": { "downloads": { "lzma": { "sha1": "310271eda8a2ac264ffc3640a9d847b49438d0bd", "size": 6942, "url": "https://launcher.mojang.com/v1/objects/310271eda8a2ac264ffc3640a9d847b49438d0bd/libjavafx_font_freetype.so" }, "raw": { "sha1": "3e7572d047c12ba2bc43acec7f98a67c20af8042", "size": 27616, "url": "https://launcher.mojang.com/v1/objects/3e7572d047c12ba2bc43acec7f98a67c20af8042/libjavafx_font_freetype.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjavafx_font_pango.so": { "downloads": { "lzma": { "sha1": "a7bcf0669e70b0f43099a99c81e6b6440cb40ac0", "size": 5820, "url": "https://launcher.mojang.com/v1/objects/a7bcf0669e70b0f43099a99c81e6b6440cb40ac0/libjavafx_font_pango.so" }, "raw": { "sha1": "f0b775cc9a514c7ee8b4d6fb300653ce548caf10", "size": 25560, "url": "https://launcher.mojang.com/v1/objects/f0b775cc9a514c7ee8b4d6fb300653ce548caf10/libjavafx_font_pango.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjavafx_font_t2k.so": { "downloads": { "lzma": { "sha1": "551c29dc7c7fc83223aa36a6187f7e0c5d650538", "size": 431450, "url": "https://launcher.mojang.com/v1/objects/551c29dc7c7fc83223aa36a6187f7e0c5d650538/libjavafx_font_t2k.so" }, "raw": { "sha1": "91e5813057c3b852d411540160f8ad05fb9f1ed3", "size": 1486128, "url": "https://launcher.mojang.com/v1/objects/91e5813057c3b852d411540160f8ad05fb9f1ed3/libjavafx_font_t2k.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjavafx_iio.so": { "downloads": { "lzma": { "sha1": "c832998fd5e06ed6dcd6428816194c350785420c", "size": 101479, "url": "https://launcher.mojang.com/v1/objects/c832998fd5e06ed6dcd6428816194c350785420c/libjavafx_iio.so" }, "raw": { "sha1": "dcdf68cb25677b76c1cf0bb94294e6e9880a6678", "size": 256336, "url": "https://launcher.mojang.com/v1/objects/dcdf68cb25677b76c1cf0bb94294e6e9880a6678/libjavafx_iio.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjawt.so": { "downloads": { "lzma": { "sha1": "c1ced6aad5c69ff444dc67d0fd7e333558953831", "size": 1872, "url": "https://launcher.mojang.com/v1/objects/c1ced6aad5c69ff444dc67d0fd7e333558953831/libjawt.so" }, "raw": { "sha1": "c5032f2c6fa40bea24e56605cf76b26a27e87b67", "size": 8048, "url": "https://launcher.mojang.com/v1/objects/c5032f2c6fa40bea24e56605cf76b26a27e87b67/libjawt.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjdwp.so": { "downloads": { "lzma": { "sha1": "c1aabbb3f5a624b9ad10ed871a1d83510a99b646", "size": 94884, "url": "https://launcher.mojang.com/v1/objects/c1aabbb3f5a624b9ad10ed871a1d83510a99b646/libjdwp.so" }, "raw": { "sha1": "a043e97be47937f6f552e94cf79c76c1c57f9594", "size": 272248, "url": "https://launcher.mojang.com/v1/objects/a043e97be47937f6f552e94cf79c76c1c57f9594/libjdwp.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjfr.so": { "downloads": { "lzma": { "sha1": "11b8e6bfffdccbacbf9dd29dea4b90b753f3c1b7", "size": 8780, "url": "https://launcher.mojang.com/v1/objects/11b8e6bfffdccbacbf9dd29dea4b90b753f3c1b7/libjfr.so" }, "raw": { "sha1": "312392dd186b11c418183e818f1928e8685a07e5", "size": 28384, "url": "https://launcher.mojang.com/v1/objects/312392dd186b11c418183e818f1928e8685a07e5/libjfr.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjfxmedia.so": { "downloads": { "lzma": { "sha1": "a4e7a126eb648ce6e5e6dc151831da37d8334139", "size": 391897, "url": "https://launcher.mojang.com/v1/objects/a4e7a126eb648ce6e5e6dc151831da37d8334139/libjfxmedia.so" }, "raw": { "sha1": "5fa54944327a6012c3d34cb5c1c4432762178dc8", "size": 1636376, "url": "https://launcher.mojang.com/v1/objects/5fa54944327a6012c3d34cb5c1c4432762178dc8/libjfxmedia.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjfxwebkit.so": { "downloads": { "lzma": { "sha1": "b274debd222cdcc2ee84160ebb95144b3880bc97", "size": 20492825, "url": "https://launcher.mojang.com/v1/objects/b274debd222cdcc2ee84160ebb95144b3880bc97/libjfxwebkit.so" }, "raw": { "sha1": "ecee564c3b2f645131b35bb3004abd4caeabd291", "size": 91014584, "url": "https://launcher.mojang.com/v1/objects/ecee564c3b2f645131b35bb3004abd4caeabd291/libjfxwebkit.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjpeg.so": { "downloads": { "lzma": { "sha1": "9ad55e370c5eaaa73c3158339db3c368b1aaf0cb", "size": 113072, "url": "https://launcher.mojang.com/v1/objects/9ad55e370c5eaaa73c3158339db3c368b1aaf0cb/libjpeg.so" }, "raw": { "sha1": "651e6d53ae67db1f0efbf7f104447a9b49b7e333", "size": 292520, "url": "https://launcher.mojang.com/v1/objects/651e6d53ae67db1f0efbf7f104447a9b49b7e333/libjpeg.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjsdt.so": { "downloads": { "lzma": { "sha1": "04b6d1361a34c496b5f652b2477784d69b8b6baf", "size": 3964, "url": "https://launcher.mojang.com/v1/objects/04b6d1361a34c496b5f652b2477784d69b8b6baf/libjsdt.so" }, "raw": { "sha1": "82b48a82bf6183d34cf00a0f81661b45c616f31b", "size": 12904, "url": "https://launcher.mojang.com/v1/objects/82b48a82bf6183d34cf00a0f81661b45c616f31b/libjsdt.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjsig.so": { "downloads": { "lzma": { "sha1": "37d3b89abde397216cc4ecb1339d8543d99b8428", "size": 3536, "url": "https://launcher.mojang.com/v1/objects/37d3b89abde397216cc4ecb1339d8543d99b8428/libjsig.so" }, "raw": { "sha1": "42e52ba1bcbe0362ab24bcf65c93797354db6fb9", "size": 13336, "url": "https://launcher.mojang.com/v1/objects/42e52ba1bcbe0362ab24bcf65c93797354db6fb9/libjsig.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjsound.so": { "downloads": { "lzma": { "sha1": "7e3c565d74d8ffae716f32b05544fa4a6f108adc", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7e3c565d74d8ffae716f32b05544fa4a6f108adc/libjsound.so" }, "raw": { "sha1": "0c0fc63b92d7b83c9960fa80d45c80553ea20254", "size": 8232, "url": "https://launcher.mojang.com/v1/objects/0c0fc63b92d7b83c9960fa80d45c80553ea20254/libjsound.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjsoundalsa.so": { "downloads": { "lzma": { "sha1": "b06c51858a25ff776519495f1b9b3d9f604b089f", "size": 23097, "url": "https://launcher.mojang.com/v1/objects/b06c51858a25ff776519495f1b9b3d9f604b089f/libjsoundalsa.so" }, "raw": { "sha1": "281d37f0326d4a12dc7ea316ead09c198ff7bdf7", "size": 83256, "url": "https://launcher.mojang.com/v1/objects/281d37f0326d4a12dc7ea316ead09c198ff7bdf7/libjsoundalsa.so" } }, "executable": true, "type": "file" }, "lib/amd64/liblcms.so": { "downloads": { "lzma": { "sha1": "7a239baba2086cae49114b382b74b971da02f08e", "size": 176175, "url": "https://launcher.mojang.com/v1/objects/7a239baba2086cae49114b382b74b971da02f08e/liblcms.so" }, "raw": { "sha1": "c8895cc3c3d023d9e059225969ab67954772c0a1", "size": 526872, "url": "https://launcher.mojang.com/v1/objects/c8895cc3c3d023d9e059225969ab67954772c0a1/liblcms.so" } }, "executable": true, "type": "file" }, "lib/amd64/libmanagement.so": { "downloads": { "lzma": { "sha1": "aed3fdbcefd1716abfc6a306687c8b741cbb318e", "size": 12838, "url": "https://launcher.mojang.com/v1/objects/aed3fdbcefd1716abfc6a306687c8b741cbb318e/libmanagement.so" }, "raw": { "sha1": "eba35f61e0d50e30874b7c7b335edf2d52662423", "size": 51808, "url": "https://launcher.mojang.com/v1/objects/eba35f61e0d50e30874b7c7b335edf2d52662423/libmanagement.so" } }, "executable": true, "type": "file" }, "lib/amd64/libmlib_image.so": { "downloads": { "lzma": { "sha1": "1bb181f079492d55c7a458e96488cd17fe0a7b86", "size": 310272, "url": "https://launcher.mojang.com/v1/objects/1bb181f079492d55c7a458e96488cd17fe0a7b86/libmlib_image.so" }, "raw": { "sha1": "c973c450d33873675945d4694be484e3427f58f1", "size": 1048136, "url": "https://launcher.mojang.com/v1/objects/c973c450d33873675945d4694be484e3427f58f1/libmlib_image.so" } }, "executable": true, "type": "file" }, "lib/amd64/libnet.so": { "downloads": { "lzma": { "sha1": "9dd79703b6deb86e0321afe01c6ac508263c8312", "size": 38123, "url": "https://launcher.mojang.com/v1/objects/9dd79703b6deb86e0321afe01c6ac508263c8312/libnet.so" }, "raw": { "sha1": "b3a17b7d53fcdf1e689e1ec29ce851eee6022ead", "size": 116920, "url": "https://launcher.mojang.com/v1/objects/b3a17b7d53fcdf1e689e1ec29ce851eee6022ead/libnet.so" } }, "executable": true, "type": "file" }, "lib/amd64/libnio.so": { "downloads": { "lzma": { "sha1": "5697c89d5d5d9b74f2e1555fcbba79dd4049e287", "size": 24445, "url": "https://launcher.mojang.com/v1/objects/5697c89d5d5d9b74f2e1555fcbba79dd4049e287/libnio.so" }, "raw": { "sha1": "573bf8f64dbcc397f8abd3e1da28f90ab0679f5b", "size": 93872, "url": "https://launcher.mojang.com/v1/objects/573bf8f64dbcc397f8abd3e1da28f90ab0679f5b/libnio.so" } }, "executable": true, "type": "file" }, "lib/amd64/libnpjp2.so": { "downloads": { "lzma": { "sha1": "6fe53b5951ff740e7f2ef7ffe5975af26da06718", "size": 57892, "url": "https://launcher.mojang.com/v1/objects/6fe53b5951ff740e7f2ef7ffe5975af26da06718/libnpjp2.so" }, "raw": { "sha1": "2bb13c53a4280379253475e51216b97eed1d4ce3", "size": 216592, "url": "https://launcher.mojang.com/v1/objects/2bb13c53a4280379253475e51216b97eed1d4ce3/libnpjp2.so" } }, "executable": true, "type": "file" }, "lib/amd64/libnpt.so": { "downloads": { "lzma": { "sha1": "1b170b09a32b1b8b6624fa5d1f94ec60b2bf3876", "size": 5070, "url": "https://launcher.mojang.com/v1/objects/1b170b09a32b1b8b6624fa5d1f94ec60b2bf3876/libnpt.so" }, "raw": { "sha1": "6b1ff6b9b4624f3cc7801f221c82b8046fb76364", "size": 17504, "url": "https://launcher.mojang.com/v1/objects/6b1ff6b9b4624f3cc7801f221c82b8046fb76364/libnpt.so" } }, "executable": true, "type": "file" }, "lib/amd64/libprism_common.so": { "downloads": { "lzma": { "sha1": "f4aca04c90bc7505851c074a08b2c31cae1f94fa", "size": 23315, "url": "https://launcher.mojang.com/v1/objects/f4aca04c90bc7505851c074a08b2c31cae1f94fa/libprism_common.so" }, "raw": { "sha1": "b00866b6ed8646a29a334d46e297267552f27de8", "size": 59008, "url": "https://launcher.mojang.com/v1/objects/b00866b6ed8646a29a334d46e297267552f27de8/libprism_common.so" } }, "executable": true, "type": "file" }, "lib/amd64/libprism_es2.so": { "downloads": { "lzma": { "sha1": "7ff4173c338c7a6f370f231670055737e032da3e", "size": 18416, "url": "https://launcher.mojang.com/v1/objects/7ff4173c338c7a6f370f231670055737e032da3e/libprism_es2.so" }, "raw": { "sha1": "1390a1dc14345e5a948148e59195d62f3a83863f", "size": 63808, "url": "https://launcher.mojang.com/v1/objects/1390a1dc14345e5a948148e59195d62f3a83863f/libprism_es2.so" } }, "executable": true, "type": "file" }, "lib/amd64/libprism_sw.so": { "downloads": { "lzma": { "sha1": "6728e8bf7b214067d715be6fe0325910d63c2468", "size": 29457, "url": "https://launcher.mojang.com/v1/objects/6728e8bf7b214067d715be6fe0325910d63c2468/libprism_sw.so" }, "raw": { "sha1": "7a6c34cb2bbcde411778d1b3f8677c39e32c3ac4", "size": 71608, "url": "https://launcher.mojang.com/v1/objects/7a6c34cb2bbcde411778d1b3f8677c39e32c3ac4/libprism_sw.so" } }, "executable": true, "type": "file" }, "lib/amd64/libresource.so": { "downloads": { "lzma": { "sha1": "1e35e63f1e74915fba620f1febf420b919d49bc5", "size": 2633, "url": "https://launcher.mojang.com/v1/objects/1e35e63f1e74915fba620f1febf420b919d49bc5/libresource.so" }, "raw": { "sha1": "57490353ad0d83ab1930355213dea269795434fe", "size": 13456, "url": "https://launcher.mojang.com/v1/objects/57490353ad0d83ab1930355213dea269795434fe/libresource.so" } }, "executable": true, "type": "file" }, "lib/amd64/libsctp.so": { "downloads": { "lzma": { "sha1": "4340132ed250d7849a016e071be773eaedd33aa8", "size": 9332, "url": "https://launcher.mojang.com/v1/objects/4340132ed250d7849a016e071be773eaedd33aa8/libsctp.so" }, "raw": { "sha1": "4a80e743750f127c0d5a359f5cd60b97e7ee12ae", "size": 29552, "url": "https://launcher.mojang.com/v1/objects/4a80e743750f127c0d5a359f5cd60b97e7ee12ae/libsctp.so" } }, "executable": true, "type": "file" }, "lib/amd64/libsplashscreen.so": { "downloads": { "lzma": { "sha1": "b226c8dbd73a548fc2b042ee6db6cc80e727597c", "size": 190305, "url": "https://launcher.mojang.com/v1/objects/b226c8dbd73a548fc2b042ee6db6cc80e727597c/libsplashscreen.so" }, "raw": { "sha1": "87d6a491f5ba7e6c4d972264a0c9063afea567a2", "size": 441376, "url": "https://launcher.mojang.com/v1/objects/87d6a491f5ba7e6c4d972264a0c9063afea567a2/libsplashscreen.so" } }, "executable": true, "type": "file" }, "lib/amd64/libsunec.so": { "downloads": { "lzma": { "sha1": "6ebba98fab1e3d872d1363235b76497f6f9babdc", "size": 88829, "url": "https://launcher.mojang.com/v1/objects/6ebba98fab1e3d872d1363235b76497f6f9babdc/libsunec.so" }, "raw": { "sha1": "3b262a0a530f6e4e539aed2cd27b4de1d0ed8859", "size": 283368, "url": "https://launcher.mojang.com/v1/objects/3b262a0a530f6e4e539aed2cd27b4de1d0ed8859/libsunec.so" } }, "executable": true, "type": "file" }, "lib/amd64/libt2k.so": { "downloads": { "lzma": { "sha1": "602cb812ef0b350ccf56ce209a260ddbe3743d92", "size": 190720, "url": "https://launcher.mojang.com/v1/objects/602cb812ef0b350ccf56ce209a260ddbe3743d92/libt2k.so" }, "raw": { "sha1": "b072c56df997f61e15e6b5a43b8907b0d25c2043", "size": 504840, "url": "https://launcher.mojang.com/v1/objects/b072c56df997f61e15e6b5a43b8907b0d25c2043/libt2k.so" } }, "executable": true, "type": "file" }, "lib/amd64/libunpack.so": { "downloads": { "lzma": { "sha1": "7107b615e941074f0b14c31c88fb67200aacd37f", "size": 70308, "url": "https://launcher.mojang.com/v1/objects/7107b615e941074f0b14c31c88fb67200aacd37f/libunpack.so" }, "raw": { "sha1": "b05ff862ed87928ed91e80e5604673c5ea710a53", "size": 197712, "url": "https://launcher.mojang.com/v1/objects/b05ff862ed87928ed91e80e5604673c5ea710a53/libunpack.so" } }, "executable": true, "type": "file" }, "lib/amd64/libverify.so": { "downloads": { "lzma": { "sha1": "ecd98efb8c7da441a8c3580e8f5598f3cb4165b1", "size": 21885, "url": "https://launcher.mojang.com/v1/objects/ecd98efb8c7da441a8c3580e8f5598f3cb4165b1/libverify.so" }, "raw": { "sha1": "e2c8d92531c45ab9be69ffb72c87fa12e9e59827", "size": 66112, "url": "https://launcher.mojang.com/v1/objects/e2c8d92531c45ab9be69ffb72c87fa12e9e59827/libverify.so" } }, "executable": true, "type": "file" }, "lib/amd64/libzip.so": { "downloads": { "lzma": { "sha1": "7c562342e3f7b138dc978495447e3e6a96c2cf45", "size": 54876, "url": "https://launcher.mojang.com/v1/objects/7c562342e3f7b138dc978495447e3e6a96c2cf45/libzip.so" }, "raw": { "sha1": "5f4bf35a5c3e8f8c650e891d1031589b8ab6d77f", "size": 127016, "url": "https://launcher.mojang.com/v1/objects/5f4bf35a5c3e8f8c650e891d1031589b8ab6d77f/libzip.so" } }, "executable": true, "type": "file" }, "lib/amd64/server": { "type": "directory" }, "lib/amd64/server/Xusage.txt": { "downloads": { "lzma": { "sha1": "acb2da24a4c765887df83985e4c26d6be302a0a3", "size": 629, "url": "https://launcher.mojang.com/v1/objects/acb2da24a4c765887df83985e4c26d6be302a0a3/Xusage.txt" }, "raw": { "sha1": "6983727eafe140f9dd793c78aa6f3e007438243a", "size": 1423, "url": "https://launcher.mojang.com/v1/objects/6983727eafe140f9dd793c78aa6f3e007438243a/Xusage.txt" } }, "executable": false, "type": "file" }, "lib/amd64/server/libjsig.so": { "target": "../libjsig.so", "type": "link" }, "lib/amd64/server/libjvm.so": { "downloads": { "lzma": { "sha1": "d5c6f3338aaa6712f79d680ac8c3e31beebaa886", "size": 4154311, "url": "https://launcher.mojang.com/v1/objects/d5c6f3338aaa6712f79d680ac8c3e31beebaa886/libjvm.so" }, "raw": { "sha1": "23a98e1eb505cc3fb91bc0cb2adb71ab9270e9ca", "size": 17045016, "url": "https://launcher.mojang.com/v1/objects/23a98e1eb505cc3fb91bc0cb2adb71ab9270e9ca/libjvm.so" } }, "executable": true, "type": "file" }, "lib/applet": { "type": "directory" }, "lib/calendars.properties": { "downloads": { "lzma": { "sha1": "4a757c23f2942bd802a4f80235131146d9267750", "size": 558, "url": "https://launcher.mojang.com/v1/objects/4a757c23f2942bd802a4f80235131146d9267750/calendars.properties" }, "raw": { "sha1": "42ebb0988124433b8f2a6e5d9a74ed41240bcfc6", "size": 1378, "url": "https://launcher.mojang.com/v1/objects/42ebb0988124433b8f2a6e5d9a74ed41240bcfc6/calendars.properties" } }, "executable": false, "type": "file" }, "lib/charsets.jar": { "downloads": { "lzma": { "sha1": "2bf44143b2ad9d7d55045a4de4a562330c194dc0", "size": 412367, "url": "https://launcher.mojang.com/v1/objects/2bf44143b2ad9d7d55045a4de4a562330c194dc0/charsets.jar" }, "raw": { "sha1": "d73ab9f8de255a7e112ddd13622bf7f6b18c8447", "size": 3135615, "url": "https://launcher.mojang.com/v1/objects/d73ab9f8de255a7e112ddd13622bf7f6b18c8447/charsets.jar" } }, "executable": false, "type": "file" }, "lib/classlist": { "downloads": { "lzma": { "sha1": "14e7c73d21b8513b0aff8d86e5cb34c52021fbca", "size": 15024, "url": "https://launcher.mojang.com/v1/objects/14e7c73d21b8513b0aff8d86e5cb34c52021fbca/classlist" }, "raw": { "sha1": "9c0404b63c87e2fed35e3a6cd137d6cf876c42bd", "size": 84311, "url": "https://launcher.mojang.com/v1/objects/9c0404b63c87e2fed35e3a6cd137d6cf876c42bd/classlist" } }, "executable": false, "type": "file" }, "lib/cmm": { "type": "directory" }, "lib/cmm/CIEXYZ.pf": { "downloads": { "lzma": { "sha1": "fcc5ca2fd3f45cac3434b480fa3ce00007e96529", "size": 8964, "url": "https://launcher.mojang.com/v1/objects/fcc5ca2fd3f45cac3434b480fa3ce00007e96529/CIEXYZ.pf" }, "raw": { "sha1": "b7779924c70554647b87c2a86159ca7781e929f8", "size": 51236, "url": "https://launcher.mojang.com/v1/objects/b7779924c70554647b87c2a86159ca7781e929f8/CIEXYZ.pf" } }, "executable": false, "type": "file" }, "lib/cmm/GRAY.pf": { "downloads": { "lzma": { "sha1": "5388ccfe67d3131d6d02143d8e8895003ab14ff6", "size": 299, "url": "https://launcher.mojang.com/v1/objects/5388ccfe67d3131d6d02143d8e8895003ab14ff6/GRAY.pf" }, "raw": { "sha1": "27f93961d66b8230d0cdb8b166bc8b4153d5bc2d", "size": 632, "url": "https://launcher.mojang.com/v1/objects/27f93961d66b8230d0cdb8b166bc8b4153d5bc2d/GRAY.pf" } }, "executable": false, "type": "file" }, "lib/cmm/LINEAR_RGB.pf": { "downloads": { "lzma": { "sha1": "2bd90f09c8deb64b1729d6b8173c78f9e9cab27b", "size": 678, "url": "https://launcher.mojang.com/v1/objects/2bd90f09c8deb64b1729d6b8173c78f9e9cab27b/LINEAR_RGB.pf" }, "raw": { "sha1": "7913274c2f73bafcf888f09ff60990b100214ede", "size": 1044, "url": "https://launcher.mojang.com/v1/objects/7913274c2f73bafcf888f09ff60990b100214ede/LINEAR_RGB.pf" } }, "executable": false, "type": "file" }, "lib/cmm/PYCC.pf": { "downloads": { "lzma": { "sha1": "dbb2197ecff3fcdd142e9006490c8cb5c8d19af8", "size": 171521, "url": "https://launcher.mojang.com/v1/objects/dbb2197ecff3fcdd142e9006490c8cb5c8d19af8/PYCC.pf" }, "raw": { "sha1": "4f7eed05b8f0eea7bcdc8f8f7aaeb1925ce7b144", "size": 274474, "url": "https://launcher.mojang.com/v1/objects/4f7eed05b8f0eea7bcdc8f8f7aaeb1925ce7b144/PYCC.pf" } }, "executable": false, "type": "file" }, "lib/cmm/sRGB.pf": { "downloads": { "raw": { "sha1": "9eaea0911d89d63e39e95f2e2116eaec7e0bb91e", "size": 3144, "url": "https://launcher.mojang.com/v1/objects/9eaea0911d89d63e39e95f2e2116eaec7e0bb91e/sRGB.pf" } }, "executable": false, "type": "file" }, "lib/content-types.properties": { "downloads": { "lzma": { "sha1": "43a23d9a6c637c128b14cfa3feced93cbcf85b1a", "size": 1617, "url": "https://launcher.mojang.com/v1/objects/43a23d9a6c637c128b14cfa3feced93cbcf85b1a/content-types.properties" }, "raw": { "sha1": "b21698017c4a2866b5fabe59681b7592e72c83b1", "size": 5916, "url": "https://launcher.mojang.com/v1/objects/b21698017c4a2866b5fabe59681b7592e72c83b1/content-types.properties" } }, "executable": false, "type": "file" }, "lib/currency.data": { "downloads": { "lzma": { "sha1": "451b3f166ea34ef2aefbb01606ea5adcc0d65b42", "size": 1184, "url": "https://launcher.mojang.com/v1/objects/451b3f166ea34ef2aefbb01606ea5adcc0d65b42/currency.data" }, "raw": { "sha1": "bf524381a7a9b9d5bbab48069c583d2936e367a1", "size": 4134, "url": "https://launcher.mojang.com/v1/objects/bf524381a7a9b9d5bbab48069c583d2936e367a1/currency.data" } }, "executable": false, "type": "file" }, "lib/deploy": { "type": "directory" }, "lib/deploy.jar": { "downloads": { "raw": { "sha1": "fbe1de8fcd9a3d482c59414dce9311e4194766c9", "size": 2255881, "url": "https://launcher.mojang.com/v1/objects/fbe1de8fcd9a3d482c59414dce9311e4194766c9/deploy.jar" } }, "executable": false, "type": "file" }, "lib/deploy/MixedCodeMainDialog.ui": { "downloads": { "lzma": { "sha1": "7d812964343d1e978442f5c847c709784fc18fc0", "size": 683, "url": "https://launcher.mojang.com/v1/objects/7d812964343d1e978442f5c847c709784fc18fc0/MixedCodeMainDialog.ui" }, "raw": { "sha1": "c9b1af1c229e54b2d8a3d642d4f0bb31dc15be79", "size": 4507, "url": "https://launcher.mojang.com/v1/objects/c9b1af1c229e54b2d8a3d642d4f0bb31dc15be79/MixedCodeMainDialog.ui" } }, "executable": false, "type": "file" }, "lib/deploy/MixedCodeMainDialogJs.ui": { "downloads": { "lzma": { "sha1": "54fb58dbcc59e35e0ae896d0e266ae0c5bcf85c2", "size": 792, "url": "https://launcher.mojang.com/v1/objects/54fb58dbcc59e35e0ae896d0e266ae0c5bcf85c2/MixedCodeMainDialogJs.ui" }, "raw": { "sha1": "ad6337fb6d46750e14c12b439a5856f4b6864d0d", "size": 6110, "url": "https://launcher.mojang.com/v1/objects/ad6337fb6d46750e14c12b439a5856f4b6864d0d/MixedCodeMainDialogJs.ui" } }, "executable": false, "type": "file" }, "lib/deploy/cautionshield.icns": { "downloads": { "lzma": { "sha1": "7cea751dc168605054ec38ce8bfa71812be405c1", "size": 2333, "url": "https://launcher.mojang.com/v1/objects/7cea751dc168605054ec38ce8bfa71812be405c1/cautionshield.icns" }, "raw": { "sha1": "1de7ed5d5fc75aa1bcede088c655bee3bde64c38", "size": 3588, "url": "https://launcher.mojang.com/v1/objects/1de7ed5d5fc75aa1bcede088c655bee3bde64c38/cautionshield.icns" } }, "executable": false, "type": "file" }, "lib/deploy/ffjcext.zip": { "downloads": { "lzma": { "sha1": "80bcb9b3794f69d87dba93e90230f288e651e798", "size": 1809, "url": "https://launcher.mojang.com/v1/objects/80bcb9b3794f69d87dba93e90230f288e651e798/ffjcext.zip" }, "raw": { "sha1": "76d051ca7d3666ff25ea8eb9957a05574a45287f", "size": 13454, "url": "https://launcher.mojang.com/v1/objects/76d051ca7d3666ff25ea8eb9957a05574a45287f/ffjcext.zip" } }, "executable": false, "type": "file" }, "lib/deploy/java-icon.ico": { "downloads": { "lzma": { "sha1": "2a24f0207d7ab5976a8b0d92b4b381d49e895c9d", "size": 8468, "url": "https://launcher.mojang.com/v1/objects/2a24f0207d7ab5976a8b0d92b4b381d49e895c9d/java-icon.ico" }, "raw": { "sha1": "2997ceb26ff49a7d7c5e7a2405b5fb50b62c7914", "size": 29926, "url": "https://launcher.mojang.com/v1/objects/2997ceb26ff49a7d7c5e7a2405b5fb50b62c7914/java-icon.ico" } }, "executable": false, "type": "file" }, "lib/deploy/messages.properties": { "downloads": { "lzma": { "sha1": "c1e16f80dc0b1f1a591cecf3cbab4ba5e47492f4", "size": 1225, "url": "https://launcher.mojang.com/v1/objects/c1e16f80dc0b1f1a591cecf3cbab4ba5e47492f4/messages.properties" }, "raw": { "sha1": "dc52841c708e3c1eb2a044088a43396d1291bb5e", "size": 2860, "url": "https://launcher.mojang.com/v1/objects/dc52841c708e3c1eb2a044088a43396d1291bb5e/messages.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_de.properties": { "downloads": { "lzma": { "sha1": "42b42e6e1d2cb2d781f2226bde612ce519b29bc8", "size": 1394, "url": "https://launcher.mojang.com/v1/objects/42b42e6e1d2cb2d781f2226bde612ce519b29bc8/messages_de.properties" }, "raw": { "sha1": "d989fe1b8f7904888d5102294ebefd28d932ecdb", "size": 3306, "url": "https://launcher.mojang.com/v1/objects/d989fe1b8f7904888d5102294ebefd28d932ecdb/messages_de.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_es.properties": { "downloads": { "lzma": { "sha1": "c4a653e9802ca982e892b45d88c1e259c09e8c8e", "size": 1404, "url": "https://launcher.mojang.com/v1/objects/c4a653e9802ca982e892b45d88c1e259c09e8c8e/messages_es.properties" }, "raw": { "sha1": "1b0334b79db481c3a59be6915d5118d760c97baa", "size": 3600, "url": "https://launcher.mojang.com/v1/objects/1b0334b79db481c3a59be6915d5118d760c97baa/messages_es.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_fr.properties": { "downloads": { "lzma": { "sha1": "2d8dee07e3f5aab7318a22e169810b216ac44f97", "size": 1401, "url": "https://launcher.mojang.com/v1/objects/2d8dee07e3f5aab7318a22e169810b216ac44f97/messages_fr.properties" }, "raw": { "sha1": "69bd2d03c2064f8679de5b4e430ea61b567c69c5", "size": 3409, "url": "https://launcher.mojang.com/v1/objects/69bd2d03c2064f8679de5b4e430ea61b567c69c5/messages_fr.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_it.properties": { "downloads": { "lzma": { "sha1": "7c28cdd8d9e34355ba0fc03004c4f64749cae57e", "size": 1375, "url": "https://launcher.mojang.com/v1/objects/7c28cdd8d9e34355ba0fc03004c4f64749cae57e/messages_it.properties" }, "raw": { "sha1": "dbe49949308f28540a42ae6cd2ad58afbf615592", "size": 3223, "url": "https://launcher.mojang.com/v1/objects/dbe49949308f28540a42ae6cd2ad58afbf615592/messages_it.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_ja.properties": { "downloads": { "lzma": { "sha1": "9a6a4c086e48b9e615b72b6bbebb3c724d178ff4", "size": 1680, "url": "https://launcher.mojang.com/v1/objects/9a6a4c086e48b9e615b72b6bbebb3c724d178ff4/messages_ja.properties" }, "raw": { "sha1": "751170a7cdefcb1226604ac3f8196e06a04fd7ac", "size": 6349, "url": "https://launcher.mojang.com/v1/objects/751170a7cdefcb1226604ac3f8196e06a04fd7ac/messages_ja.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_ko.properties": { "downloads": { "lzma": { "sha1": "0c57c2ebfa0830f816657a384898487fc492efac", "size": 1645, "url": "https://launcher.mojang.com/v1/objects/0c57c2ebfa0830f816657a384898487fc492efac/messages_ko.properties" }, "raw": { "sha1": "bf9e055b5ab138ad6d49769e2b7630b7938848d6", "size": 5712, "url": "https://launcher.mojang.com/v1/objects/bf9e055b5ab138ad6d49769e2b7630b7938848d6/messages_ko.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_pt_BR.properties": { "downloads": { "lzma": { "sha1": "f8364dba0aa0a7e445a1a8d0e7ad66b996f70063", "size": 1388, "url": "https://launcher.mojang.com/v1/objects/f8364dba0aa0a7e445a1a8d0e7ad66b996f70063/messages_pt_BR.properties" }, "raw": { "sha1": "24e4951743521ab9a11381c77bd0cdb1ed30f5b5", "size": 3285, "url": "https://launcher.mojang.com/v1/objects/24e4951743521ab9a11381c77bd0cdb1ed30f5b5/messages_pt_BR.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_sv.properties": { "downloads": { "lzma": { "sha1": "65e5897d552258141aacf02f087c7c9c33ad0727", "size": 1355, "url": "https://launcher.mojang.com/v1/objects/65e5897d552258141aacf02f087c7c9c33ad0727/messages_sv.properties" }, "raw": { "sha1": "bb5a4aa0ba499f6b1916a83e3c7922a4583b4adb", "size": 3384, "url": "https://launcher.mojang.com/v1/objects/bb5a4aa0ba499f6b1916a83e3c7922a4583b4adb/messages_sv.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_zh_CN.properties": { "downloads": { "lzma": { "sha1": "de7d39a6e6748e9f47e842c9da90f515921c222c", "size": 1506, "url": "https://launcher.mojang.com/v1/objects/de7d39a6e6748e9f47e842c9da90f515921c222c/messages_zh_CN.properties" }, "raw": { "sha1": "1c2b96673dddd3596890ef4fc22017d484a1f652", "size": 4072, "url": "https://launcher.mojang.com/v1/objects/1c2b96673dddd3596890ef4fc22017d484a1f652/messages_zh_CN.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_zh_HK.properties": { "downloads": { "lzma": { "sha1": "e8d0e3a63caa2535a4f361033941f34dcc170a7e", "size": 1529, "url": "https://launcher.mojang.com/v1/objects/e8d0e3a63caa2535a4f361033941f34dcc170a7e/messages_zh_TW.properties" }, "raw": { "sha1": "37a57aad121c14c25e149206179728fa62203bf0", "size": 3752, "url": "https://launcher.mojang.com/v1/objects/37a57aad121c14c25e149206179728fa62203bf0/messages_zh_TW.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_zh_TW.properties": { "downloads": { "lzma": { "sha1": "e8d0e3a63caa2535a4f361033941f34dcc170a7e", "size": 1529, "url": "https://launcher.mojang.com/v1/objects/e8d0e3a63caa2535a4f361033941f34dcc170a7e/messages_zh_TW.properties" }, "raw": { "sha1": "37a57aad121c14c25e149206179728fa62203bf0", "size": 3752, "url": "https://launcher.mojang.com/v1/objects/37a57aad121c14c25e149206179728fa62203bf0/messages_zh_TW.properties" } }, "executable": false, "type": "file" }, "lib/deploy/mixcode_s.png": { "downloads": { "raw": { "sha1": "4604e9f265eec97bccd0151c3a81afa9e69499e5", "size": 3115, "url": "https://launcher.mojang.com/v1/objects/4604e9f265eec97bccd0151c3a81afa9e69499e5/mixcode_s.png" } }, "executable": false, "type": "file" }, "lib/deploy/splash.gif": { "downloads": { "raw": { "sha1": "20e7aec75f6d036d504277542e507eb7dc24aae8", "size": 8590, "url": "https://launcher.mojang.com/v1/objects/20e7aec75f6d036d504277542e507eb7dc24aae8/splash.gif" } }, "executable": false, "type": "file" }, "lib/deploy/splash@2x.gif": { "downloads": { "raw": { "sha1": "0ae4a5bda2a6d628fac51462390b503c99509fdc", "size": 15276, "url": "https://launcher.mojang.com/v1/objects/0ae4a5bda2a6d628fac51462390b503c99509fdc/splash2x.gif" } }, "executable": false, "type": "file" }, "lib/deploy/splash_11-lic.gif": { "downloads": { "raw": { "sha1": "8def364e07f40142822df84b5bb4f50846cb5e4e", "size": 7805, "url": "https://launcher.mojang.com/v1/objects/8def364e07f40142822df84b5bb4f50846cb5e4e/splash_11-lic.gif" } }, "executable": false, "type": "file" }, "lib/deploy/splash_11@2x-lic.gif": { "downloads": { "raw": { "sha1": "d2bff9bbf7920ca743b81a0ee23b0719b4d057ca", "size": 12250, "url": "https://launcher.mojang.com/v1/objects/d2bff9bbf7920ca743b81a0ee23b0719b4d057ca/splash_11%402x-lic.gif" } }, "executable": false, "type": "file" }, "lib/desktop": { "type": "directory" }, "lib/desktop/applications": { "type": "directory" }, "lib/desktop/applications/sun-java.desktop": { "downloads": { "lzma": { "sha1": "109d1cdf165f38da92da70b403ca940192a7a9a8", "size": 536, "url": "https://launcher.mojang.com/v1/objects/109d1cdf165f38da92da70b403ca940192a7a9a8/sun-java.desktop" }, "raw": { "sha1": "d346dfe90505603ce5aff5a3c6c2e4a23d5bd990", "size": 777, "url": "https://launcher.mojang.com/v1/objects/d346dfe90505603ce5aff5a3c6c2e4a23d5bd990/sun-java.desktop" } }, "executable": false, "type": "file" }, "lib/desktop/applications/sun-javaws.desktop": { "downloads": { "lzma": { "sha1": "5e1815e7f83515881e6998584dc6bb02c5bef09a", "size": 451, "url": "https://launcher.mojang.com/v1/objects/5e1815e7f83515881e6998584dc6bb02c5bef09a/sun-javaws.desktop" }, "raw": { "sha1": "50ce8e519b836e0f53d58ce1a359d98b6cafdda6", "size": 619, "url": "https://launcher.mojang.com/v1/objects/50ce8e519b836e0f53d58ce1a359d98b6cafdda6/sun-javaws.desktop" } }, "executable": false, "type": "file" }, "lib/desktop/applications/sun_java.desktop": { "downloads": { "lzma": { "sha1": "49ab0ccb54c3be68281d05055bc56a88b1281d3c", "size": 447, "url": "https://launcher.mojang.com/v1/objects/49ab0ccb54c3be68281d05055bc56a88b1281d3c/sun_java.desktop" }, "raw": { "sha1": "79120ee8160ad6f3c9b90c2641fb7edf3af96b5d", "size": 624, "url": "https://launcher.mojang.com/v1/objects/79120ee8160ad6f3c9b90c2641fb7edf3af96b5d/sun_java.desktop" } }, "executable": false, "type": "file" }, "lib/desktop/icons": { "type": "directory" }, "lib/desktop/icons/HighContrast": { "type": "directory" }, "lib/desktop/icons/HighContrast/16x16": { "type": "directory" }, "lib/desktop/icons/HighContrast/16x16/apps": { "type": "directory" }, "lib/desktop/icons/HighContrast/16x16/apps/sun-java.png": { "downloads": { "raw": { "sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/16x16/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/16x16/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/16x16/mimetypes": { "type": "directory" }, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/48x48": { "type": "directory" }, "lib/desktop/icons/HighContrast/48x48/apps": { "type": "directory" }, "lib/desktop/icons/HighContrast/48x48/apps/sun-java.png": { "downloads": { "raw": { "sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/48x48/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/48x48/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/48x48/mimetypes": { "type": "directory" }, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/16x16": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/16x16/apps": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-java.png": { "downloads": { "raw": { "sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/48x48": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/48x48/apps": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-java.png": { "downloads": { "raw": { "sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast": { "type": "directory" }, "lib/desktop/icons/LowContrast/16x16": { "type": "directory" }, "lib/desktop/icons/LowContrast/16x16/apps": { "type": "directory" }, "lib/desktop/icons/LowContrast/16x16/apps/sun-java.png": { "downloads": { "raw": { "sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/16x16/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/16x16/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/16x16/mimetypes": { "type": "directory" }, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/48x48": { "type": "directory" }, "lib/desktop/icons/LowContrast/48x48/apps": { "type": "directory" }, "lib/desktop/icons/LowContrast/48x48/apps/sun-java.png": { "downloads": { "raw": { "sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/48x48/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/48x48/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/48x48/mimetypes": { "type": "directory" }, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor": { "type": "directory" }, "lib/desktop/icons/hicolor/16x16": { "type": "directory" }, "lib/desktop/icons/hicolor/16x16/apps": { "type": "directory" }, "lib/desktop/icons/hicolor/16x16/apps/sun-java.png": { "downloads": { "raw": { "sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/16x16/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/16x16/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/16x16/mimetypes": { "type": "directory" }, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/48x48": { "type": "directory" }, "lib/desktop/icons/hicolor/48x48/apps": { "type": "directory" }, "lib/desktop/icons/hicolor/48x48/apps/sun-java.png": { "downloads": { "raw": { "sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/48x48/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/48x48/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/48x48/mimetypes": { "type": "directory" }, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/mime": { "type": "directory" }, "lib/desktop/mime/packages": { "type": "directory" }, "lib/desktop/mime/packages/x-java-archive.xml": { "downloads": { "lzma": { "sha1": "841230729f0a59de2a1071d155d96358232b2ba1", "size": 591, "url": "https://launcher.mojang.com/v1/objects/841230729f0a59de2a1071d155d96358232b2ba1/x-java-archive.xml" }, "raw": { "sha1": "b6297fd36efa799312961f95ebf0c85c920d5037", "size": 1822, "url": "https://launcher.mojang.com/v1/objects/b6297fd36efa799312961f95ebf0c85c920d5037/x-java-archive.xml" } }, "executable": false, "type": "file" }, "lib/desktop/mime/packages/x-java-jnlp-file.xml": { "downloads": { "lzma": { "sha1": "abf9acbe7c18027c4f036c4e42bb2cf1115525fa", "size": 302, "url": "https://launcher.mojang.com/v1/objects/abf9acbe7c18027c4f036c4e42bb2cf1115525fa/x-java-jnlp-file.xml" }, "raw": { "sha1": "72f03da83ddb76c9105f619fcfa4dbdad70e6b30", "size": 412, "url": "https://launcher.mojang.com/v1/objects/72f03da83ddb76c9105f619fcfa4dbdad70e6b30/x-java-jnlp-file.xml" } }, "executable": false, "type": "file" }, "lib/ext": { "type": "directory" }, "lib/ext/cldrdata.jar": { "downloads": { "raw": { "sha1": "6cacc961942d3f02a88907aa8f2eaae8e20c95a0", "size": 3860502, "url": "https://launcher.mojang.com/v1/objects/6cacc961942d3f02a88907aa8f2eaae8e20c95a0/cldrdata.jar" } }, "executable": false, "type": "file" }, "lib/ext/dnsns.jar": { "downloads": { "raw": { "sha1": "93bebdd7514e53ae31d60c5daba673878c8822ec", "size": 8286, "url": "https://launcher.mojang.com/v1/objects/93bebdd7514e53ae31d60c5daba673878c8822ec/dnsns.jar" } }, "executable": false, "type": "file" }, "lib/ext/jaccess.jar": { "downloads": { "raw": { "sha1": "2f54879df7c29ec67c40d40cfc95c0d4a968bea1", "size": 44516, "url": "https://launcher.mojang.com/v1/objects/2f54879df7c29ec67c40d40cfc95c0d4a968bea1/jaccess.jar" } }, "executable": false, "type": "file" }, "lib/ext/jfxrt.jar": { "downloads": { "lzma": { "sha1": "a6c5b6a782ba360ada6651f5322dcab88c75711c", "size": 3374270, "url": "https://launcher.mojang.com/v1/objects/a6c5b6a782ba360ada6651f5322dcab88c75711c/jfxrt.jar" }, "raw": { "sha1": "1ad7a876f045399c23ee4ab7dee380a04ca2ac08", "size": 18508094, "url": "https://launcher.mojang.com/v1/objects/1ad7a876f045399c23ee4ab7dee380a04ca2ac08/jfxrt.jar" } }, "executable": false, "type": "file" }, "lib/ext/localedata.jar": { "downloads": { "raw": { "sha1": "0cc9f550d4e410b5aa29dbfd2c1b5c99391c7f70", "size": 1178926, "url": "https://launcher.mojang.com/v1/objects/0cc9f550d4e410b5aa29dbfd2c1b5c99391c7f70/localedata.jar" } }, "executable": false, "type": "file" }, "lib/ext/meta-index": { "downloads": { "lzma": { "sha1": "1359457529f42bacf495afcb68149ae036442dd9", "size": 594, "url": "https://launcher.mojang.com/v1/objects/1359457529f42bacf495afcb68149ae036442dd9/meta-index" }, "raw": { "sha1": "334649c6e2d5d7248211f30855e97cfcb4558851", "size": 1269, "url": "https://launcher.mojang.com/v1/objects/334649c6e2d5d7248211f30855e97cfcb4558851/meta-index" } }, "executable": false, "type": "file" }, "lib/ext/nashorn.jar": { "downloads": { "raw": { "sha1": "dec5dd17a0f52ae79dfbfb38840bffb8b7a679a5", "size": 2023869, "url": "https://launcher.mojang.com/v1/objects/dec5dd17a0f52ae79dfbfb38840bffb8b7a679a5/nashorn.jar" } }, "executable": false, "type": "file" }, "lib/ext/sunec.jar": { "downloads": { "raw": { "sha1": "bf1c817820341a246f7130fe046e8310b03d04f6", "size": 41672, "url": "https://launcher.mojang.com/v1/objects/bf1c817820341a246f7130fe046e8310b03d04f6/sunec.jar" } }, "executable": false, "type": "file" }, "lib/ext/sunjce_provider.jar": { "downloads": { "raw": { "sha1": "bb3494e4b5cb3c3e60da767207731f18b267cb34", "size": 279326, "url": "https://launcher.mojang.com/v1/objects/bb3494e4b5cb3c3e60da767207731f18b267cb34/sunjce_provider.jar" } }, "executable": false, "type": "file" }, "lib/ext/sunpkcs11.jar": { "downloads": { "raw": { "sha1": "5bb1dedc3344cd3bb86828d4aa8ca82f4a606ed4", "size": 250218, "url": "https://launcher.mojang.com/v1/objects/5bb1dedc3344cd3bb86828d4aa8ca82f4a606ed4/sunpkcs11.jar" } }, "executable": false, "type": "file" }, "lib/ext/zipfs.jar": { "downloads": { "raw": { "sha1": "37b338f0e8e60d6396f51275130e8110816d7b30", "size": 68964, "url": "https://launcher.mojang.com/v1/objects/37b338f0e8e60d6396f51275130e8110816d7b30/zipfs.jar" } }, "executable": false, "type": "file" }, "lib/flavormap.properties": { "downloads": { "lzma": { "sha1": "2d5ef19ee77ccfc95c9413eea155cde59a48fadd", "size": 1541, "url": "https://launcher.mojang.com/v1/objects/2d5ef19ee77ccfc95c9413eea155cde59a48fadd/flavormap.properties" }, "raw": { "sha1": "4e66e8fe12d7f8b3b0c4e1a1915f329bb1fbf6d2", "size": 3901, "url": "https://launcher.mojang.com/v1/objects/4e66e8fe12d7f8b3b0c4e1a1915f329bb1fbf6d2/flavormap.properties" } }, "executable": false, "type": "file" }, "lib/fontconfig.RedHat.5.bfc": { "downloads": { "lzma": { "sha1": "5197f6e387f16458b7408134e38b06f20f625e4c", "size": 795, "url": "https://launcher.mojang.com/v1/objects/5197f6e387f16458b7408134e38b06f20f625e4c/fontconfig.RedHat.5.bfc" }, "raw": { "sha1": "fb806ada6e68f16a9fe2b726a39d9ef5a835c0c2", "size": 4532, "url": "https://launcher.mojang.com/v1/objects/fb806ada6e68f16a9fe2b726a39d9ef5a835c0c2/fontconfig.RedHat.5.bfc" } }, "executable": false, "type": "file" }, "lib/fontconfig.RedHat.5.properties.src": { "downloads": { "lzma": { "sha1": "3897ae198e96e5a687c9c9b218ff5df60c868e0d", "size": 1089, "url": "https://launcher.mojang.com/v1/objects/3897ae198e96e5a687c9c9b218ff5df60c868e0d/fontconfig.RedHat.5.properties.src" }, "raw": { "sha1": "c67d1a06cb37b66e69560c9f5e4be7cf08af0493", "size": 8841, "url": "https://launcher.mojang.com/v1/objects/c67d1a06cb37b66e69560c9f5e4be7cf08af0493/fontconfig.RedHat.5.properties.src" } }, "executable": false, "type": "file" }, "lib/fontconfig.RedHat.6.bfc": { "downloads": { "lzma": { "sha1": "ef2f5d1f8d620be9927db45d3a28bd75777245cb", "size": 818, "url": "https://launcher.mojang.com/v1/objects/ef2f5d1f8d620be9927db45d3a28bd75777245cb/fontconfig.RedHat.6.bfc" }, "raw": { "sha1": "9ba3b3e2c621c31d0ef1b2053c80f77419a19965", "size": 4250, "url": "https://launcher.mojang.com/v1/objects/9ba3b3e2c621c31d0ef1b2053c80f77419a19965/fontconfig.RedHat.6.bfc" } }, "executable": false, "type": "file" }, "lib/fontconfig.RedHat.6.properties.src": { "downloads": { "lzma": { "sha1": "74f4148f9d7ec3d67bbd724834d478a72cfdb0db", "size": 1111, "url": "https://launcher.mojang.com/v1/objects/74f4148f9d7ec3d67bbd724834d478a72cfdb0db/fontconfig.RedHat.6.properties.src" }, "raw": { "sha1": "768e58d4d314621c38daf9fde6d67119f370acd9", "size": 8735, "url": "https://launcher.mojang.com/v1/objects/768e58d4d314621c38daf9fde6d67119f370acd9/fontconfig.RedHat.6.properties.src" } }, "executable": false, "type": "file" }, "lib/fontconfig.SuSE.10.bfc": { "downloads": { "lzma": { "sha1": "d8fe9b1d8d02368dcd452de93024c6f60670eb87", "size": 1083, "url": "https://launcher.mojang.com/v1/objects/d8fe9b1d8d02368dcd452de93024c6f60670eb87/fontconfig.SuSE.10.bfc" }, "raw": { "sha1": "ffd0dfbd1553e15b11649a73a0b3f48318138e0d", "size": 6702, "url": "https://launcher.mojang.com/v1/objects/ffd0dfbd1553e15b11649a73a0b3f48318138e0d/fontconfig.SuSE.10.bfc" } }, "executable": false, "type": "file" }, "lib/fontconfig.SuSE.10.properties.src": { "downloads": { "lzma": { "sha1": "2c382bd741a9e23114be3da82dee8290ebfca8a9", "size": 1555, "url": "https://launcher.mojang.com/v1/objects/2c382bd741a9e23114be3da82dee8290ebfca8a9/fontconfig.SuSE.10.properties.src" }, "raw": { "sha1": "a38dbdbbc514567b8281e1aea726865f37e97894", "size": 16772, "url": "https://launcher.mojang.com/v1/objects/a38dbdbbc514567b8281e1aea726865f37e97894/fontconfig.SuSE.10.properties.src" } }, "executable": false, "type": "file" }, "lib/fontconfig.SuSE.11.bfc": { "downloads": { "lzma": { "sha1": "2b78cbf11289c9858951fea7180696ba3b7176d6", "size": 1092, "url": "https://launcher.mojang.com/v1/objects/2b78cbf11289c9858951fea7180696ba3b7176d6/fontconfig.SuSE.11.bfc" }, "raw": { "sha1": "a4d8500fcb47f6327460a95851b1368660da8302", "size": 7032, "url": "https://launcher.mojang.com/v1/objects/a4d8500fcb47f6327460a95851b1368660da8302/fontconfig.SuSE.11.bfc" } }, "executable": false, "type": "file" }, "lib/fontconfig.SuSE.11.properties.src": { "downloads": { "lzma": { "sha1": "5c1635803906e2c59d36492dec724dd7ae49a5ab", "size": 1589, "url": "https://launcher.mojang.com/v1/objects/5c1635803906e2c59d36492dec724dd7ae49a5ab/fontconfig.SuSE.11.properties.src" }, "raw": { "sha1": "c4b69589e41a7279a71866a9134213be19cdf88d", "size": 16781, "url": "https://launcher.mojang.com/v1/objects/c4b69589e41a7279a71866a9134213be19cdf88d/fontconfig.SuSE.11.properties.src" } }, "executable": false, "type": "file" }, "lib/fontconfig.Turbo.bfc": { "downloads": { "lzma": { "sha1": "1c771325d9ee4af209a3db92294451d58962c7a4", "size": 822, "url": "https://launcher.mojang.com/v1/objects/1c771325d9ee4af209a3db92294451d58962c7a4/fontconfig.Turbo.bfc" }, "raw": { "sha1": "f24368deeb85cc9d0781083bc56e773518d72219", "size": 4668, "url": "https://launcher.mojang.com/v1/objects/f24368deeb85cc9d0781083bc56e773518d72219/fontconfig.Turbo.bfc" } }, "executable": false, "type": "file" }, "lib/fontconfig.Turbo.properties.src": { "downloads": { "lzma": { "sha1": "7748ffa17e2c8a34754138efa963ba39bd1cbbb3", "size": 1113, "url": "https://launcher.mojang.com/v1/objects/7748ffa17e2c8a34754138efa963ba39bd1cbbb3/fontconfig.Turbo.properties.src" }, "raw": { "sha1": "2bb7258bed7ccd4f117e4e5f892c9b13424b0c82", "size": 9192, "url": "https://launcher.mojang.com/v1/objects/2bb7258bed7ccd4f117e4e5f892c9b13424b0c82/fontconfig.Turbo.properties.src" } }, "executable": false, "type": "file" }, "lib/fontconfig.bfc": { "downloads": { "lzma": { "sha1": "be6d49ee8c64f458c4f0e64254963fec48d25150", "size": 286, "url": "https://launcher.mojang.com/v1/objects/be6d49ee8c64f458c4f0e64254963fec48d25150/fontconfig.bfc" }, "raw": { "sha1": "de39b0e19637f58d92a0188122514aa7247ebb5b", "size": 1678, "url": "https://launcher.mojang.com/v1/objects/de39b0e19637f58d92a0188122514aa7247ebb5b/fontconfig.bfc" } }, "executable": false, "type": "file" }, "lib/fontconfig.properties.src": { "downloads": { "lzma": { "sha1": "9498d5e00e5401200667687e826e28c60fa60ba4", "size": 417, "url": "https://launcher.mojang.com/v1/objects/9498d5e00e5401200667687e826e28c60fa60ba4/fontconfig.properties.src" }, "raw": { "sha1": "3617ff1424fd204415242565541facf862b16eb4", "size": 1938, "url": "https://launcher.mojang.com/v1/objects/3617ff1424fd204415242565541facf862b16eb4/fontconfig.properties.src" } }, "executable": false, "type": "file" }, "lib/fonts": { "type": "directory" }, "lib/fonts/LucidaBrightDemiBold.ttf": { "downloads": { "lzma": { "sha1": "4f748750831a7719440dff5457f4d207d0f24d21", "size": 42347, "url": "https://launcher.mojang.com/v1/objects/4f748750831a7719440dff5457f4d207d0f24d21/LucidaBrightDemiBold.ttf" }, "raw": { "sha1": "b5c97f985639e19a3b712193ee48b55dda581fd1", "size": 75144, "url": "https://launcher.mojang.com/v1/objects/b5c97f985639e19a3b712193ee48b55dda581fd1/LucidaBrightDemiBold.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaBrightDemiItalic.ttf": { "downloads": { "lzma": { "sha1": "f82e9a688553c100ecb98412b985807ed56dff5d", "size": 43119, "url": "https://launcher.mojang.com/v1/objects/f82e9a688553c100ecb98412b985807ed56dff5d/LucidaBrightDemiItalic.ttf" }, "raw": { "sha1": "1fd1f757febf3e5f5fbb7fbf7a56587a40d57de7", "size": 75124, "url": "https://launcher.mojang.com/v1/objects/1fd1f757febf3e5f5fbb7fbf7a56587a40d57de7/LucidaBrightDemiItalic.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaBrightItalic.ttf": { "downloads": { "lzma": { "sha1": "6d630df719271319c3d53f90a3d425118b908266", "size": 46206, "url": "https://launcher.mojang.com/v1/objects/6d630df719271319c3d53f90a3d425118b908266/LucidaBrightItalic.ttf" }, "raw": { "sha1": "aa5c037865c563726ecd63d61ca26443589be425", "size": 80856, "url": "https://launcher.mojang.com/v1/objects/aa5c037865c563726ecd63d61ca26443589be425/LucidaBrightItalic.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaBrightRegular.ttf": { "downloads": { "lzma": { "sha1": "4b2e31aaec2238b6ecf9f845bad0a1c6d09fbbfe", "size": 181085, "url": "https://launcher.mojang.com/v1/objects/4b2e31aaec2238b6ecf9f845bad0a1c6d09fbbfe/LucidaBrightRegular.ttf" }, "raw": { "sha1": "5d7ed564791c900a8786936930ba99385653139c", "size": 344908, "url": "https://launcher.mojang.com/v1/objects/5d7ed564791c900a8786936930ba99385653139c/LucidaBrightRegular.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaSansDemiBold.ttf": { "downloads": { "lzma": { "sha1": "079b16dc3c4918ab1f4f760b6dc5e6586c219042", "size": 173229, "url": "https://launcher.mojang.com/v1/objects/079b16dc3c4918ab1f4f760b6dc5e6586c219042/LucidaSansDemiBold.ttf" }, "raw": { "sha1": "92b79fefc35e96190250c602a8fed85276b32a95", "size": 317896, "url": "https://launcher.mojang.com/v1/objects/92b79fefc35e96190250c602a8fed85276b32a95/LucidaSansDemiBold.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaSansRegular.ttf": { "downloads": { "lzma": { "sha1": "64a65d7b94d7153d20957ef6d06bebb4dd0f48e4", "size": 326062, "url": "https://launcher.mojang.com/v1/objects/64a65d7b94d7153d20957ef6d06bebb4dd0f48e4/LucidaSansRegular.ttf" }, "raw": { "sha1": "39cc8bcb8d4a71d4657fc92ef0b9f4e3e9e67add", "size": 698236, "url": "https://launcher.mojang.com/v1/objects/39cc8bcb8d4a71d4657fc92ef0b9f4e3e9e67add/LucidaSansRegular.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaTypewriterBold.ttf": { "downloads": { "lzma": { "sha1": "cdb017f7c34bea0802bc5ea5583aef721ed99c49", "size": 130412, "url": "https://launcher.mojang.com/v1/objects/cdb017f7c34bea0802bc5ea5583aef721ed99c49/LucidaTypewriterBold.ttf" }, "raw": { "sha1": "a5da2eb49448f461470387c939f0e69119310e0b", "size": 234068, "url": "https://launcher.mojang.com/v1/objects/a5da2eb49448f461470387c939f0e69119310e0b/LucidaTypewriterBold.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaTypewriterRegular.ttf": { "downloads": { "lzma": { "sha1": "aeda4a09a53783b0dc97de8e20071bea874cbfe5", "size": 135184, "url": "https://launcher.mojang.com/v1/objects/aeda4a09a53783b0dc97de8e20071bea874cbfe5/LucidaTypewriterRegular.ttf" }, "raw": { "sha1": "c144dcafe4faf2e79cfd74d8134a631f30234db1", "size": 242700, "url": "https://launcher.mojang.com/v1/objects/c144dcafe4faf2e79cfd74d8134a631f30234db1/LucidaTypewriterRegular.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/fonts.dir": { "downloads": { "lzma": { "sha1": "68f2dd93b215ec8b8d9409d2b9c825632c6b907d", "size": 273, "url": "https://launcher.mojang.com/v1/objects/68f2dd93b215ec8b8d9409d2b9c825632c6b907d/fonts.dir" }, "raw": { "sha1": "97f40cca185c954adf5cc582345a7cb8e4c50578", "size": 4041, "url": "https://launcher.mojang.com/v1/objects/97f40cca185c954adf5cc582345a7cb8e4c50578/fonts.dir" } }, "executable": false, "type": "file" }, "lib/hijrah-config-umalqura.properties": { "downloads": { "lzma": { "sha1": "02e8d296e3b18a450f1ed1547cbf2b7275664c9a", "size": 1969, "url": "https://launcher.mojang.com/v1/objects/02e8d296e3b18a450f1ed1547cbf2b7275664c9a/hijrah-config-umalqura.properties" }, "raw": { "sha1": "84aa425100740722e91f4725caf849e7863d12ba", "size": 13962, "url": "https://launcher.mojang.com/v1/objects/84aa425100740722e91f4725caf849e7863d12ba/hijrah-config-umalqura.properties" } }, "executable": false, "type": "file" }, "lib/images": { "type": "directory" }, "lib/images/cursors": { "type": "directory" }, "lib/images/cursors/cursors.properties": { "downloads": { "lzma": { "sha1": "612bd0f610ee1023947c4a2a8d3fc7d6f97e7d8f", "size": 385, "url": "https://launcher.mojang.com/v1/objects/612bd0f610ee1023947c4a2a8d3fc7d6f97e7d8f/cursors.properties" }, "raw": { "sha1": "f2b9a22ddd0a77869497a64f28f07e89a7d41f48", "size": 1274, "url": "https://launcher.mojang.com/v1/objects/f2b9a22ddd0a77869497a64f28f07e89a7d41f48/cursors.properties" } }, "executable": false, "type": "file" }, "lib/images/cursors/invalid32x32.gif": { "downloads": { "raw": { "sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/cursors/motif_CopyDrop32x32.gif": { "downloads": { "raw": { "sha1": "eb7620fae702172aa663a19d170a0b929d3b11d1", "size": 158, "url": "https://launcher.mojang.com/v1/objects/eb7620fae702172aa663a19d170a0b929d3b11d1/motif_CopyDrop32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/cursors/motif_CopyNoDrop32x32.gif": { "downloads": { "raw": { "sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/cursors/motif_LinkDrop32x32.gif": { "downloads": { "raw": { "sha1": "9699137f990c240e714481563181069c8f6c17bb", "size": 162, "url": "https://launcher.mojang.com/v1/objects/9699137f990c240e714481563181069c8f6c17bb/motif_LinkDrop32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/cursors/motif_LinkNoDrop32x32.gif": { "downloads": { "raw": { "sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/cursors/motif_MoveDrop32x32.gif": { "downloads": { "raw": { "sha1": "03c1617ce3c5ab8af03e46d30a8c8f31ab57fb1b", "size": 141, "url": "https://launcher.mojang.com/v1/objects/03c1617ce3c5ab8af03e46d30a8c8f31ab57fb1b/motif_MoveDrop32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/cursors/motif_MoveNoDrop32x32.gif": { "downloads": { "raw": { "sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/icons": { "type": "directory" }, "lib/images/icons/sun-java.png": { "downloads": { "raw": { "sha1": "d101b693aa054f51097eebdfeed8b8a6ca7b55b8", "size": 4707, "url": "https://launcher.mojang.com/v1/objects/d101b693aa054f51097eebdfeed8b8a6ca7b55b8/sun-java.png" } }, "executable": false, "type": "file" }, "lib/images/icons/sun-java_HighContrast.png": { "downloads": { "raw": { "sha1": "a6b1e418d6b5d03719b96f61f0c5236a60970151", "size": 3729, "url": "https://launcher.mojang.com/v1/objects/a6b1e418d6b5d03719b96f61f0c5236a60970151/sun-java_HighContrast.png" } }, "executable": false, "type": "file" }, "lib/images/icons/sun-java_HighContrastInverse.png": { "downloads": { "raw": { "sha1": "2dda28b9bddc9b5b018e3e8a8b062a99d9b2f887", "size": 3777, "url": "https://launcher.mojang.com/v1/objects/2dda28b9bddc9b5b018e3e8a8b062a99d9b2f887/sun-java_HighContrastInverse.png" } }, "executable": false, "type": "file" }, "lib/images/icons/sun-java_LowContrast.png": { "downloads": { "raw": { "sha1": "7714cc4e894c3626c8da6fe742ed22b2829122d9", "size": 4012, "url": "https://launcher.mojang.com/v1/objects/7714cc4e894c3626c8da6fe742ed22b2829122d9/sun-java_LowContrast.png" } }, "executable": false, "type": "file" }, "lib/javafx.properties": { "downloads": { "raw": { "sha1": "49e6b75d109e5fd3f6cbe7cc5fa9a7980796d14d", "size": 56, "url": "https://launcher.mojang.com/v1/objects/49e6b75d109e5fd3f6cbe7cc5fa9a7980796d14d/javafx.properties" } }, "executable": false, "type": "file" }, "lib/javaws.jar": { "downloads": { "raw": { "sha1": "04fa5ae04ead65b91be5dee575497e49ffd49fe9", "size": 488118, "url": "https://launcher.mojang.com/v1/objects/04fa5ae04ead65b91be5dee575497e49ffd49fe9/javaws.jar" } }, "executable": false, "type": "file" }, "lib/jce.jar": { "downloads": { "raw": { "sha1": "5460adee09cc5fc8829c0acfc46c34670a7d70a0", "size": 115646, "url": "https://launcher.mojang.com/v1/objects/5460adee09cc5fc8829c0acfc46c34670a7d70a0/jce.jar" } }, "executable": false, "type": "file" }, "lib/jexec": { "downloads": { "lzma": { "sha1": "2d4323d4e060f8126d026ca6c03b8972aedd2fab", "size": 3311, "url": "https://launcher.mojang.com/v1/objects/2d4323d4e060f8126d026ca6c03b8972aedd2fab/jexec" }, "raw": { "sha1": "6aa01f1d8d103974164bcfaea03c04eeeefd7d41", "size": 13376, "url": "https://launcher.mojang.com/v1/objects/6aa01f1d8d103974164bcfaea03c04eeeefd7d41/jexec" } }, "executable": true, "type": "file" }, "lib/jfr": { "type": "directory" }, "lib/jfr.jar": { "downloads": { "lzma": { "sha1": "5b9d615c91c72f4fe356d9b4105946679452d1e1", "size": 137982, "url": "https://launcher.mojang.com/v1/objects/5b9d615c91c72f4fe356d9b4105946679452d1e1/jfr.jar" }, "raw": { "sha1": "0f3fd66a336703d935bdc22ad8082bc51d34e534", "size": 560713, "url": "https://launcher.mojang.com/v1/objects/0f3fd66a336703d935bdc22ad8082bc51d34e534/jfr.jar" } }, "executable": false, "type": "file" }, "lib/jfr/default.jfc": { "downloads": { "lzma": { "sha1": "373ddd878146dd8ce8991c2c5115a05a82859bdb", "size": 2207, "url": "https://launcher.mojang.com/v1/objects/373ddd878146dd8ce8991c2c5115a05a82859bdb/default.jfc" }, "raw": { "sha1": "1a64b68d0e7d43f8149faba94440be54f4f24527", "size": 20109, "url": "https://launcher.mojang.com/v1/objects/1a64b68d0e7d43f8149faba94440be54f4f24527/default.jfc" } }, "executable": false, "type": "file" }, "lib/jfr/profile.jfc": { "downloads": { "lzma": { "sha1": "3dcdc5feee3ccfb66bc8726b666944cd4bdadae3", "size": 2199, "url": "https://launcher.mojang.com/v1/objects/3dcdc5feee3ccfb66bc8726b666944cd4bdadae3/profile.jfc" }, "raw": { "sha1": "5d7d08a595f76322c51ae43ea966fbba6b69eebe", "size": 20065, "url": "https://launcher.mojang.com/v1/objects/5d7d08a595f76322c51ae43ea966fbba6b69eebe/profile.jfc" } }, "executable": false, "type": "file" }, "lib/jfxswt.jar": { "downloads": { "raw": { "sha1": "99d9a264c898d84c01e1c42565e7fe1a89dcd72d", "size": 33932, "url": "https://launcher.mojang.com/v1/objects/99d9a264c898d84c01e1c42565e7fe1a89dcd72d/jfxswt.jar" } }, "executable": false, "type": "file" }, "lib/jsse.jar": { "downloads": { "lzma": { "sha1": "94a17dfbc2e76cd12c33970a15341424f875a9ce", "size": 187549, "url": "https://launcher.mojang.com/v1/objects/94a17dfbc2e76cd12c33970a15341424f875a9ce/jsse.jar" }, "raw": { "sha1": "92c5c626e8a2d16f41272c0e404d4f992dd8310a", "size": 675599, "url": "https://launcher.mojang.com/v1/objects/92c5c626e8a2d16f41272c0e404d4f992dd8310a/jsse.jar" } }, "executable": false, "type": "file" }, "lib/jvm.hprof.txt": { "downloads": { "lzma": { "sha1": "eccdb240a815b2a83a502749339b27bb8669965b", "size": 1863, "url": "https://launcher.mojang.com/v1/objects/eccdb240a815b2a83a502749339b27bb8669965b/jvm.hprof.txt" }, "raw": { "sha1": "fbd61d52534cdd0c15df332114d469c65d001e33", "size": 4226, "url": "https://launcher.mojang.com/v1/objects/fbd61d52534cdd0c15df332114d469c65d001e33/jvm.hprof.txt" } }, "executable": false, "type": "file" }, "lib/locale": { "type": "directory" }, "lib/locale/de": { "type": "directory" }, "lib/locale/de/LC_MESSAGES": { "type": "directory" }, "lib/locale/de/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "3061d922907cc557208109088fc6ab81d577ff6f", "size": 970, "url": "https://launcher.mojang.com/v1/objects/3061d922907cc557208109088fc6ab81d577ff6f/sunw_java_plugin.mo" }, "raw": { "sha1": "5b223a3d723ac1cfce63623fb109f2868d47d1b7", "size": 2483, "url": "https://launcher.mojang.com/v1/objects/5b223a3d723ac1cfce63623fb109f2868d47d1b7/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/es": { "type": "directory" }, "lib/locale/es/LC_MESSAGES": { "type": "directory" }, "lib/locale/es/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "24338049a89b323e17182b3a3006b50565d4fa0f", "size": 979, "url": "https://launcher.mojang.com/v1/objects/24338049a89b323e17182b3a3006b50565d4fa0f/sunw_java_plugin.mo" }, "raw": { "sha1": "6cc63dc97f2fdb2ed799e48b1dc98c4f37cdecc1", "size": 2477, "url": "https://launcher.mojang.com/v1/objects/6cc63dc97f2fdb2ed799e48b1dc98c4f37cdecc1/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/fr": { "type": "directory" }, "lib/locale/fr/LC_MESSAGES": { "type": "directory" }, "lib/locale/fr/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "22796a48ef39f57d2d6fa70f41308e493d7f05c1", "size": 1033, "url": "https://launcher.mojang.com/v1/objects/22796a48ef39f57d2d6fa70f41308e493d7f05c1/sunw_java_plugin.mo" }, "raw": { "sha1": "d9d5b458db6e83fdf85c3526aeee3f57c4929840", "size": 2746, "url": "https://launcher.mojang.com/v1/objects/d9d5b458db6e83fdf85c3526aeee3f57c4929840/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/it": { "type": "directory" }, "lib/locale/it/LC_MESSAGES": { "type": "directory" }, "lib/locale/it/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "59a4cae38bfb8927745674d0efc2f284bc277987", "size": 958, "url": "https://launcher.mojang.com/v1/objects/59a4cae38bfb8927745674d0efc2f284bc277987/sunw_java_plugin.mo" }, "raw": { "sha1": "f6e72e3b2141ccc3dffab10ae14a754e494577ba", "size": 2434, "url": "https://launcher.mojang.com/v1/objects/f6e72e3b2141ccc3dffab10ae14a754e494577ba/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/ja": { "type": "directory" }, "lib/locale/ja/LC_MESSAGES": { "type": "directory" }, "lib/locale/ja/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "7d6aeed563e1cefcf0224cf522048468088884a9", "size": 1036, "url": "https://launcher.mojang.com/v1/objects/7d6aeed563e1cefcf0224cf522048468088884a9/sunw_java_plugin.mo" }, "raw": { "sha1": "378881a8cb8dd2aebb43eacd0c68519be4f258b1", "size": 2415, "url": "https://launcher.mojang.com/v1/objects/378881a8cb8dd2aebb43eacd0c68519be4f258b1/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/ko": { "type": "directory" }, "lib/locale/ko.UTF-8": { "type": "directory" }, "lib/locale/ko.UTF-8/LC_MESSAGES": { "type": "directory" }, "lib/locale/ko.UTF-8/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "12ee3b21511e8497d95ea0ba9d6fe519227d0b16", "size": 1069, "url": "https://launcher.mojang.com/v1/objects/12ee3b21511e8497d95ea0ba9d6fe519227d0b16/sunw_java_plugin.mo" }, "raw": { "sha1": "cb19df01c59662dbe2f4050b1290d374b82fe1fa", "size": 2753, "url": "https://launcher.mojang.com/v1/objects/cb19df01c59662dbe2f4050b1290d374b82fe1fa/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/ko/LC_MESSAGES": { "type": "directory" }, "lib/locale/ko/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "6e2e47c64c360517fd436bc79c823b5679a1efe6", "size": 996, "url": "https://launcher.mojang.com/v1/objects/6e2e47c64c360517fd436bc79c823b5679a1efe6/sunw_java_plugin.mo" }, "raw": { "sha1": "12c8a118d150c78f719314df6dec49a967af71e9", "size": 2399, "url": "https://launcher.mojang.com/v1/objects/12c8a118d150c78f719314df6dec49a967af71e9/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/pt_BR": { "type": "directory" }, "lib/locale/pt_BR/LC_MESSAGES": { "type": "directory" }, "lib/locale/pt_BR/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "bcaa7e7916493f071f1bf64bf58c6b038e3569c9", "size": 940, "url": "https://launcher.mojang.com/v1/objects/bcaa7e7916493f071f1bf64bf58c6b038e3569c9/sunw_java_plugin.mo" }, "raw": { "sha1": "a3bc0c43994c53c59bba94982cf95f6d36283dd0", "size": 2420, "url": "https://launcher.mojang.com/v1/objects/a3bc0c43994c53c59bba94982cf95f6d36283dd0/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/sv": { "type": "directory" }, "lib/locale/sv/LC_MESSAGES": { "type": "directory" }, "lib/locale/sv/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "76017835d6261fe2eedbcbe5eb08a7484c3080c5", "size": 946, "url": "https://launcher.mojang.com/v1/objects/76017835d6261fe2eedbcbe5eb08a7484c3080c5/sunw_java_plugin.mo" }, "raw": { "sha1": "09a47686edec4bbb34e82fbd08559f8bb6266544", "size": 2359, "url": "https://launcher.mojang.com/v1/objects/09a47686edec4bbb34e82fbd08559f8bb6266544/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/zh": { "type": "directory" }, "lib/locale/zh.GBK": { "type": "directory" }, "lib/locale/zh.GBK/LC_MESSAGES": { "type": "directory" }, "lib/locale/zh.GBK/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "75fd04045bf5890b8bb822770bfdb90a2e9ea65b", "size": 902, "url": "https://launcher.mojang.com/v1/objects/75fd04045bf5890b8bb822770bfdb90a2e9ea65b/sunw_java_plugin.mo" }, "raw": { "sha1": "7006fe7767b8807441a1f359a90509b3e507b0d1", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7006fe7767b8807441a1f359a90509b3e507b0d1/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/zh/LC_MESSAGES": { "type": "directory" }, "lib/locale/zh/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "75fd04045bf5890b8bb822770bfdb90a2e9ea65b", "size": 902, "url": "https://launcher.mojang.com/v1/objects/75fd04045bf5890b8bb822770bfdb90a2e9ea65b/sunw_java_plugin.mo" }, "raw": { "sha1": "7006fe7767b8807441a1f359a90509b3e507b0d1", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7006fe7767b8807441a1f359a90509b3e507b0d1/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/zh_HK.BIG5HK": { "type": "directory" }, "lib/locale/zh_HK.BIG5HK/LC_MESSAGES": { "type": "directory" }, "lib/locale/zh_HK.BIG5HK/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "3a1397bb1b1741697be1479232b6d9599940c851", "size": 912, "url": "https://launcher.mojang.com/v1/objects/3a1397bb1b1741697be1479232b6d9599940c851/sunw_java_plugin.mo" }, "raw": { "sha1": "c6023544067278c78599921f1032de353ff7da42", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/c6023544067278c78599921f1032de353ff7da42/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/zh_TW": { "type": "directory" }, "lib/locale/zh_TW.BIG5": { "type": "directory" }, "lib/locale/zh_TW.BIG5/LC_MESSAGES": { "type": "directory" }, "lib/locale/zh_TW.BIG5/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "3a1397bb1b1741697be1479232b6d9599940c851", "size": 912, "url": "https://launcher.mojang.com/v1/objects/3a1397bb1b1741697be1479232b6d9599940c851/sunw_java_plugin.mo" }, "raw": { "sha1": "c6023544067278c78599921f1032de353ff7da42", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/c6023544067278c78599921f1032de353ff7da42/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/zh_TW/LC_MESSAGES": { "type": "directory" }, "lib/locale/zh_TW/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "c05e610e75182f0c4e77f3e7a4d9670ed62bf63c", "size": 897, "url": "https://launcher.mojang.com/v1/objects/c05e610e75182f0c4e77f3e7a4d9670ed62bf63c/sunw_java_plugin.mo" }, "raw": { "sha1": "f9b972dd059eae3cd337dfcef6a178e8ed8a7db6", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/f9b972dd059eae3cd337dfcef6a178e8ed8a7db6/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/logging.properties": { "downloads": { "lzma": { "sha1": "642202a58e5216d086ad37c0b5a633be802edc78", "size": 896, "url": "https://launcher.mojang.com/v1/objects/642202a58e5216d086ad37c0b5a633be802edc78/logging.properties" }, "raw": { "sha1": "89da8094484891f9ec1fa40c6c8b61f94c5869d0", "size": 2455, "url": "https://launcher.mojang.com/v1/objects/89da8094484891f9ec1fa40c6c8b61f94c5869d0/logging.properties" } }, "executable": false, "type": "file" }, "lib/management": { "type": "directory" }, "lib/management-agent.jar": { "downloads": { "lzma": { "sha1": "3ea0bf17e14b3428296a0f4011bf4025fcbfa4bd", "size": 243, "url": "https://launcher.mojang.com/v1/objects/3ea0bf17e14b3428296a0f4011bf4025fcbfa4bd/management-agent.jar" }, "raw": { "sha1": "9fbed36522aa3a80bac08a328942cbc5ef39ca8e", "size": 381, "url": "https://launcher.mojang.com/v1/objects/9fbed36522aa3a80bac08a328942cbc5ef39ca8e/management-agent.jar" } }, "executable": false, "type": "file" }, "lib/management/jmxremote.access": { "downloads": { "lzma": { "sha1": "69042ff1b14165db19c9d728614639dec16d6a31", "size": 1419, "url": "https://launcher.mojang.com/v1/objects/69042ff1b14165db19c9d728614639dec16d6a31/jmxremote.access" }, "raw": { "sha1": "21200eaad898ba4a2a8834a032efb6616fabb930", "size": 3998, "url": "https://launcher.mojang.com/v1/objects/21200eaad898ba4a2a8834a032efb6616fabb930/jmxremote.access" } }, "executable": false, "type": "file" }, "lib/management/jmxremote.password.template": { "downloads": { "lzma": { "sha1": "556c64b1e920766f8867be3964de6e49f5b81a60", "size": 1129, "url": "https://launcher.mojang.com/v1/objects/556c64b1e920766f8867be3964de6e49f5b81a60/jmxremote.password.template" }, "raw": { "sha1": "c1e0f01408bf20fbbb8b4810520c725f70050db5", "size": 2856, "url": "https://launcher.mojang.com/v1/objects/c1e0f01408bf20fbbb8b4810520c725f70050db5/jmxremote.password.template" } }, "executable": false, "type": "file" }, "lib/management/management.properties": { "downloads": { "lzma": { "sha1": "3e52f9baa6394ca6956845424c607e5cde5d3c67", "size": 3176, "url": "https://launcher.mojang.com/v1/objects/3e52f9baa6394ca6956845424c607e5cde5d3c67/management.properties" }, "raw": { "sha1": "e0451d8d7d9e84d7b1c39ec7d00993307a5cbbf1", "size": 14630, "url": "https://launcher.mojang.com/v1/objects/e0451d8d7d9e84d7b1c39ec7d00993307a5cbbf1/management.properties" } }, "executable": false, "type": "file" }, "lib/management/snmp.acl.template": { "downloads": { "lzma": { "sha1": "9a4aa6396c3b488b0663bed5e5ecb762985669c9", "size": 1121, "url": "https://launcher.mojang.com/v1/objects/9a4aa6396c3b488b0663bed5e5ecb762985669c9/snmp.acl.template" }, "raw": { "sha1": "2e9f9ac287274532eb1f0d1afcefd7f3e97cc794", "size": 3376, "url": "https://launcher.mojang.com/v1/objects/2e9f9ac287274532eb1f0d1afcefd7f3e97cc794/snmp.acl.template" } }, "executable": false, "type": "file" }, "lib/meta-index": { "downloads": { "lzma": { "sha1": "1ac60b31362fda4725c665b591c5fbe384cbc8c1", "size": 788, "url": "https://launcher.mojang.com/v1/objects/1ac60b31362fda4725c665b591c5fbe384cbc8c1/meta-index" }, "raw": { "sha1": "bf204f09242203e713c31785158a0792f9edb600", "size": 2034, "url": "https://launcher.mojang.com/v1/objects/bf204f09242203e713c31785158a0792f9edb600/meta-index" } }, "executable": false, "type": "file" }, "lib/net.properties": { "downloads": { "lzma": { "sha1": "e9ec3981a0797bf55bb87b24d9eb651ce7e6916b", "size": 1830, "url": "https://launcher.mojang.com/v1/objects/e9ec3981a0797bf55bb87b24d9eb651ce7e6916b/net.properties" }, "raw": { "sha1": "fd9471742eb759f4478bb1de9a0dc0527265b6ea", "size": 5352, "url": "https://launcher.mojang.com/v1/objects/fd9471742eb759f4478bb1de9a0dc0527265b6ea/net.properties" } }, "executable": false, "type": "file" }, "lib/oblique-fonts": { "type": "directory" }, "lib/oblique-fonts/LucidaSansDemiOblique.ttf": { "downloads": { "lzma": { "sha1": "49c8980c1b89bbdbab59d0f5bd5bebf0afcb93b2", "size": 38580, "url": "https://launcher.mojang.com/v1/objects/49c8980c1b89bbdbab59d0f5bd5bebf0afcb93b2/LucidaSansDemiOblique.ttf" }, "raw": { "sha1": "53e4e12a675ac222469341c3dbc102464a1be4c7", "size": 91352, "url": "https://launcher.mojang.com/v1/objects/53e4e12a675ac222469341c3dbc102464a1be4c7/LucidaSansDemiOblique.ttf" } }, "executable": false, "type": "file" }, "lib/oblique-fonts/LucidaSansOblique.ttf": { "downloads": { "lzma": { "sha1": "553123c0edcd08035dede4ffd92b5b81c9a7538a", "size": 116575, "url": "https://launcher.mojang.com/v1/objects/553123c0edcd08035dede4ffd92b5b81c9a7538a/LucidaSansOblique.ttf" }, "raw": { "sha1": "95a195ad4fc520b3e395c85b747fc3024d118dd9", "size": 253724, "url": "https://launcher.mojang.com/v1/objects/95a195ad4fc520b3e395c85b747fc3024d118dd9/LucidaSansOblique.ttf" } }, "executable": false, "type": "file" }, "lib/oblique-fonts/LucidaTypewriterBoldOblique.ttf": { "downloads": { "lzma": { "sha1": "2475b08151556ad4d89bb1d2b6494c6bee9abd82", "size": 29954, "url": "https://launcher.mojang.com/v1/objects/2475b08151556ad4d89bb1d2b6494c6bee9abd82/LucidaTypewriterBoldOblique.ttf" }, "raw": { "sha1": "f331fc8b0cc494702bc46b690f2b8eed36469a02", "size": 63168, "url": "https://launcher.mojang.com/v1/objects/f331fc8b0cc494702bc46b690f2b8eed36469a02/LucidaTypewriterBoldOblique.ttf" } }, "executable": false, "type": "file" }, "lib/oblique-fonts/LucidaTypewriterOblique.ttf": { "downloads": { "lzma": { "sha1": "5b970bc3b7abb21dce1aa28ff7f03459d351e552", "size": 60133, "url": "https://launcher.mojang.com/v1/objects/5b970bc3b7abb21dce1aa28ff7f03459d351e552/LucidaTypewriterOblique.ttf" }, "raw": { "sha1": "f8ea00db73f8a89a27674d050edc37c2280930e1", "size": 137484, "url": "https://launcher.mojang.com/v1/objects/f8ea00db73f8a89a27674d050edc37c2280930e1/LucidaTypewriterOblique.ttf" } }, "executable": false, "type": "file" }, "lib/oblique-fonts/fonts.dir": { "downloads": { "lzma": { "sha1": "067528c789bd713c7c3f34e779aa6e2e8253dcf6", "size": 188, "url": "https://launcher.mojang.com/v1/objects/067528c789bd713c7c3f34e779aa6e2e8253dcf6/fonts.dir" }, "raw": { "sha1": "5aee54ffba9e33de56fd84ef64fa496b898585bb", "size": 2115, "url": "https://launcher.mojang.com/v1/objects/5aee54ffba9e33de56fd84ef64fa496b898585bb/fonts.dir" } }, "executable": false, "type": "file" }, "lib/plugin.jar": { "downloads": { "raw": { "sha1": "3f250842c79112bae5369e372025b166990820e8", "size": 950772, "url": "https://launcher.mojang.com/v1/objects/3f250842c79112bae5369e372025b166990820e8/plugin.jar" } }, "executable": false, "type": "file" }, "lib/psfont.properties.ja": { "downloads": { "lzma": { "sha1": "7ca1cc244ed251cd1eb2347f1eea37d7d18c8ad4", "size": 701, "url": "https://launcher.mojang.com/v1/objects/7ca1cc244ed251cd1eb2347f1eea37d7d18c8ad4/psfont.properties.ja" }, "raw": { "sha1": "56ed1c661eeede17b4fae8c9de7b5edbad387abc", "size": 2796, "url": "https://launcher.mojang.com/v1/objects/56ed1c661eeede17b4fae8c9de7b5edbad387abc/psfont.properties.ja" } }, "executable": false, "type": "file" }, "lib/psfontj2d.properties": { "downloads": { "lzma": { "sha1": "4252fa01af8739a3545e2b705e3383892e22ab40", "size": 2278, "url": "https://launcher.mojang.com/v1/objects/4252fa01af8739a3545e2b705e3383892e22ab40/psfontj2d.properties" }, "raw": { "sha1": "aa327a22a49967f4d74afeee6726f505f209692f", "size": 10393, "url": "https://launcher.mojang.com/v1/objects/aa327a22a49967f4d74afeee6726f505f209692f/psfontj2d.properties" } }, "executable": false, "type": "file" }, "lib/resources.jar": { "downloads": { "lzma": { "sha1": "1b0e08441750dc17efe4b527aa146da6cc14e8a6", "size": 579294, "url": "https://launcher.mojang.com/v1/objects/1b0e08441750dc17efe4b527aa146da6cc14e8a6/resources.jar" }, "raw": { "sha1": "daa021906e4648d4c37e798c11733dc2047f2da1", "size": 3505206, "url": "https://launcher.mojang.com/v1/objects/daa021906e4648d4c37e798c11733dc2047f2da1/resources.jar" } }, "executable": false, "type": "file" }, "lib/rt.jar": { "downloads": { "lzma": { "sha1": "fc4a8681aeda29c2a2a3fd11bad7729543283f3d", "size": 14378994, "url": "https://launcher.mojang.com/v1/objects/fc4a8681aeda29c2a2a3fd11bad7729543283f3d/rt.jar" }, "raw": { "sha1": "5396b0954a20f3210f1f4f1886ead30880d6ebfe", "size": 66334986, "url": "https://launcher.mojang.com/v1/objects/5396b0954a20f3210f1f4f1886ead30880d6ebfe/rt.jar" } }, "executable": false, "type": "file" }, "lib/security": { "type": "directory" }, "lib/security/blacklist": { "downloads": { "lzma": { "sha1": "8206fce6c1d91a39fdf78e8e79e953913994a1cd", "size": 1969, "url": "https://launcher.mojang.com/v1/objects/8206fce6c1d91a39fdf78e8e79e953913994a1cd/blacklist" }, "raw": { "sha1": "d4ffb3857eab403955ce9d156e46d056061e6a5a", "size": 4054, "url": "https://launcher.mojang.com/v1/objects/d4ffb3857eab403955ce9d156e46d056061e6a5a/blacklist" } }, "executable": false, "type": "file" }, "lib/security/blacklisted.certs": { "downloads": { "lzma": { "sha1": "8311bead054caf6cfe678d4b7998de4caaabfa53", "size": 806, "url": "https://launcher.mojang.com/v1/objects/8311bead054caf6cfe678d4b7998de4caaabfa53/blacklisted.certs" }, "raw": { "sha1": "c5c005c29a80493f5c31cd7eb629ac1b9c752404", "size": 1273, "url": "https://launcher.mojang.com/v1/objects/c5c005c29a80493f5c31cd7eb629ac1b9c752404/blacklisted.certs" } }, "executable": false, "type": "file" }, "lib/security/cacerts": { "downloads": { "lzma": { "sha1": "654dd94809655d5b28385cbb5eba8d6ad9f2c1aa", "size": 67802, "url": "https://launcher.mojang.com/v1/objects/654dd94809655d5b28385cbb5eba8d6ad9f2c1aa/cacerts" }, "raw": { "sha1": "2917859c443c68e19f93abcd1315c3c2904cbef9", "size": 104430, "url": "https://launcher.mojang.com/v1/objects/2917859c443c68e19f93abcd1315c3c2904cbef9/cacerts" } }, "executable": false, "type": "file" }, "lib/security/java.policy": { "downloads": { "lzma": { "sha1": "b601c420d02ef3dbd8595453d08fdef91134e8b5", "size": 647, "url": "https://launcher.mojang.com/v1/objects/b601c420d02ef3dbd8595453d08fdef91134e8b5/java.policy" }, "raw": { "sha1": "c0112209a567b3b523cfed7041709f9440227968", "size": 2466, "url": "https://launcher.mojang.com/v1/objects/c0112209a567b3b523cfed7041709f9440227968/java.policy" } }, "executable": false, "type": "file" }, "lib/security/java.security": { "downloads": { "lzma": { "sha1": "531620e82ca0365ce8dc97096bb0ac5a7ace5952", "size": 10959, "url": "https://launcher.mojang.com/v1/objects/531620e82ca0365ce8dc97096bb0ac5a7ace5952/java.security" }, "raw": { "sha1": "5dcc17a168c53d0b366784e520bd4d55aa61ac18", "size": 41528, "url": "https://launcher.mojang.com/v1/objects/5dcc17a168c53d0b366784e520bd4d55aa61ac18/java.security" } }, "executable": false, "type": "file" }, "lib/security/javaws.policy": { "downloads": { "raw": { "sha1": "4384ca5e4d32f7dd86d8baddd1e690730d74e694", "size": 98, "url": "https://launcher.mojang.com/v1/objects/4384ca5e4d32f7dd86d8baddd1e690730d74e694/javaws.policy" } }, "executable": false, "type": "file" }, "lib/security/policy": { "type": "directory" }, "lib/security/policy/limited": { "type": "directory" }, "lib/security/policy/limited/US_export_policy.jar": { "downloads": { "raw": { "sha1": "7d69ea3b385bc067738520f1b5c549e1084be285", "size": 3026, "url": "https://launcher.mojang.com/v1/objects/7d69ea3b385bc067738520f1b5c549e1084be285/US_export_policy.jar" } }, "executable": false, "type": "file" }, "lib/security/policy/limited/local_policy.jar": { "downloads": { "raw": { "sha1": "238b8826e110f58acb2e1959773b0a577cd4d569", "size": 3527, "url": "https://launcher.mojang.com/v1/objects/238b8826e110f58acb2e1959773b0a577cd4d569/local_policy.jar" } }, "executable": false, "type": "file" }, "lib/security/policy/unlimited": { "type": "directory" }, "lib/security/policy/unlimited/US_export_policy.jar": { "downloads": { "raw": { "sha1": "f6fb2af1e87fc622cda194a7d6b5f5f069653ff1", "size": 3023, "url": "https://launcher.mojang.com/v1/objects/f6fb2af1e87fc622cda194a7d6b5f5f069653ff1/US_export_policy.jar" } }, "executable": false, "type": "file" }, "lib/security/policy/unlimited/local_policy.jar": { "downloads": { "raw": { "sha1": "517368ab2cbaf6b42ea0b963f98eeedd996e83e3", "size": 3035, "url": "https://launcher.mojang.com/v1/objects/517368ab2cbaf6b42ea0b963f98eeedd996e83e3/local_policy.jar" } }, "executable": false, "type": "file" }, "lib/security/trusted.libraries": { "downloads": { "raw": { "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", "size": 0, "url": "https://launcher.mojang.com/v1/objects/da39a3ee5e6b4b0d3255bfef95601890afd80709/trusted.libraries" } }, "executable": false, "type": "file" }, "lib/sound.properties": { "downloads": { "lzma": { "sha1": "3b5f7e4ec437d79048af35094290577f483b3fe1", "size": 473, "url": "https://launcher.mojang.com/v1/objects/3b5f7e4ec437d79048af35094290577f483b3fe1/sound.properties" }, "raw": { "sha1": "9afceb218059d981d0fa9f07aad3c5097cf41b0c", "size": 1210, "url": "https://launcher.mojang.com/v1/objects/9afceb218059d981d0fa9f07aad3c5097cf41b0c/sound.properties" } }, "executable": false, "type": "file" }, "lib/tzdb.dat": { "downloads": { "lzma": { "sha1": "39c69339965484afe89c14111baeeb862fdefd97", "size": 32547, "url": "https://launcher.mojang.com/v1/objects/39c69339965484afe89c14111baeeb862fdefd97/tzdb.dat" }, "raw": { "sha1": "b59c07e3619271a3b9861e999f4b138e971baf69", "size": 105734, "url": "https://launcher.mojang.com/v1/objects/b59c07e3619271a3b9861e999f4b138e971baf69/tzdb.dat" } }, "executable": false, "type": "file" }, "man": { "type": "directory" }, "man/ja": { "target": "ja_JP.UTF-8", "type": "link" }, "man/ja_JP.UTF-8": { "type": "directory" }, "man/ja_JP.UTF-8/man1": { "type": "directory" }, "man/ja_JP.UTF-8/man1/java.1": { "downloads": { "lzma": { "sha1": "f9da09710b6c6df23c256e324a0c4df00a0d6ded", "size": 25461, "url": "https://launcher.mojang.com/v1/objects/f9da09710b6c6df23c256e324a0c4df00a0d6ded/java.1" }, "raw": { "sha1": "b0b12a0bb66e6171771ca4b1dfca32fb759bcaec", "size": 148688, "url": "https://launcher.mojang.com/v1/objects/b0b12a0bb66e6171771ca4b1dfca32fb759bcaec/java.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/javaws.1": { "downloads": { "lzma": { "sha1": "6188fae453ca09ccb19be5c9f4d2059926b36267", "size": 2154, "url": "https://launcher.mojang.com/v1/objects/6188fae453ca09ccb19be5c9f4d2059926b36267/javaws.1" }, "raw": { "sha1": "8f39d928870268ace07bedfebd18db1e1d07fc37", "size": 6641, "url": "https://launcher.mojang.com/v1/objects/8f39d928870268ace07bedfebd18db1e1d07fc37/javaws.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/jjs.1": { "downloads": { "lzma": { "sha1": "6e42b989d28b185dc1aab50c0389834e649a37d4", "size": 3452, "url": "https://launcher.mojang.com/v1/objects/6e42b989d28b185dc1aab50c0389834e649a37d4/jjs.1" }, "raw": { "sha1": "e023322a2013912315a2bd1034e6f829a27c76e0", "size": 11365, "url": "https://launcher.mojang.com/v1/objects/e023322a2013912315a2bd1034e6f829a27c76e0/jjs.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/keytool.1": { "downloads": { "lzma": { "sha1": "a78134a4bddd53d684a70aa677e51a215db1c9cb", "size": 20698, "url": "https://launcher.mojang.com/v1/objects/a78134a4bddd53d684a70aa677e51a215db1c9cb/keytool.1" }, "raw": { "sha1": "148583c837eaaf6333ccfd8c9e8df08574e14b0c", "size": 111033, "url": "https://launcher.mojang.com/v1/objects/148583c837eaaf6333ccfd8c9e8df08574e14b0c/keytool.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/orbd.1": { "downloads": { "lzma": { "sha1": "326af0dcbff173ef8aee29163dbe146d7389cc3e", "size": 4225, "url": "https://launcher.mojang.com/v1/objects/326af0dcbff173ef8aee29163dbe146d7389cc3e/orbd.1" }, "raw": { "sha1": "95651622d33c08286858ec337edd3ea72acd93dc", "size": 16092, "url": "https://launcher.mojang.com/v1/objects/95651622d33c08286858ec337edd3ea72acd93dc/orbd.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/pack200.1": { "downloads": { "lzma": { "sha1": "e0eedafa748c61a44e5be4355fe9d44b05048e80", "size": 4293, "url": "https://launcher.mojang.com/v1/objects/e0eedafa748c61a44e5be4355fe9d44b05048e80/pack200.1" }, "raw": { "sha1": "aa21a0ab75707f7fc66e83c7a392e69b37ddf80e", "size": 14482, "url": "https://launcher.mojang.com/v1/objects/aa21a0ab75707f7fc66e83c7a392e69b37ddf80e/pack200.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/policytool.1": { "downloads": { "lzma": { "sha1": "3c766ed12dab58166169d35680c392a6be1814a1", "size": 1380, "url": "https://launcher.mojang.com/v1/objects/3c766ed12dab58166169d35680c392a6be1814a1/policytool.1" }, "raw": { "sha1": "80879c74e072a98fad6f32b3283331aaf9bd002f", "size": 4020, "url": "https://launcher.mojang.com/v1/objects/80879c74e072a98fad6f32b3283331aaf9bd002f/policytool.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/rmid.1": { "downloads": { "lzma": { "sha1": "1e20779d990beacc32a48237777d670fcc47ca14", "size": 4836, "url": "https://launcher.mojang.com/v1/objects/1e20779d990beacc32a48237777d670fcc47ca14/rmid.1" }, "raw": { "sha1": "7e40cb8003d098d6e36f45640b26f979ac94b5c5", "size": 19715, "url": "https://launcher.mojang.com/v1/objects/7e40cb8003d098d6e36f45640b26f979ac94b5c5/rmid.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/rmiregistry.1": { "downloads": { "lzma": { "sha1": "aaf4ffe07e954f8696eef1ecb7a5e244628d0ad9", "size": 1627, "url": "https://launcher.mojang.com/v1/objects/aaf4ffe07e954f8696eef1ecb7a5e244628d0ad9/rmiregistry.1" }, "raw": { "sha1": "c53c52f3ae7a011c135894c9fc51b741e729c33d", "size": 4557, "url": "https://launcher.mojang.com/v1/objects/c53c52f3ae7a011c135894c9fc51b741e729c33d/rmiregistry.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/servertool.1": { "downloads": { "lzma": { "sha1": "3b9e624e9d1cf2959b438a35061162e2100ddecd", "size": 2626, "url": "https://launcher.mojang.com/v1/objects/3b9e624e9d1cf2959b438a35061162e2100ddecd/servertool.1" }, "raw": { "sha1": "50ab8bcd9dd9d0b1a3d81348fbce1c8f82e7189e", "size": 9081, "url": "https://launcher.mojang.com/v1/objects/50ab8bcd9dd9d0b1a3d81348fbce1c8f82e7189e/servertool.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/tnameserv.1": { "downloads": { "lzma": { "sha1": "bb3106ff74c60a76de3d20659b9c2128c70f3bf2", "size": 4478, "url": "https://launcher.mojang.com/v1/objects/bb3106ff74c60a76de3d20659b9c2128c70f3bf2/tnameserv.1" }, "raw": { "sha1": "01e714671ecd1167edcb5310b16a9c59c33c3eaa", "size": 17722, "url": "https://launcher.mojang.com/v1/objects/01e714671ecd1167edcb5310b16a9c59c33c3eaa/tnameserv.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/unpack200.1": { "downloads": { "lzma": { "sha1": "c115a881cf800b08df294df55d9f250ae944e33c", "size": 1973, "url": "https://launcher.mojang.com/v1/objects/c115a881cf800b08df294df55d9f250ae944e33c/unpack200.1" }, "raw": { "sha1": "7c882bba0067367a41ad84868d18793b8a7397a3", "size": 5382, "url": "https://launcher.mojang.com/v1/objects/7c882bba0067367a41ad84868d18793b8a7397a3/unpack200.1" } }, "executable": false, "type": "file" }, "man/man1": { "type": "directory" }, "man/man1/java.1": { "downloads": { "lzma": { "sha1": "06a6b0275c202bf698d73ca71f95618d56d81c15", "size": 25796, "url": "https://launcher.mojang.com/v1/objects/06a6b0275c202bf698d73ca71f95618d56d81c15/java.1" }, "raw": { "sha1": "69fec7a341aa91f18dbdcdb95952dede7e1b689a", "size": 124796, "url": "https://launcher.mojang.com/v1/objects/69fec7a341aa91f18dbdcdb95952dede7e1b689a/java.1" } }, "executable": false, "type": "file" }, "man/man1/javaws.1": { "downloads": { "lzma": { "sha1": "4bae251c6dfb5420f56928815cf80d0b6d517a1f", "size": 1759, "url": "https://launcher.mojang.com/v1/objects/4bae251c6dfb5420f56928815cf80d0b6d517a1f/javaws.1" }, "raw": { "sha1": "e61e44e101b1bc119c2d2d4b10320f38b36a8036", "size": 4897, "url": "https://launcher.mojang.com/v1/objects/e61e44e101b1bc119c2d2d4b10320f38b36a8036/javaws.1" } }, "executable": false, "type": "file" }, "man/man1/jjs.1": { "downloads": { "lzma": { "sha1": "29683cf2bd47015c9461b688749ddffd95f6671d", "size": 1881, "url": "https://launcher.mojang.com/v1/objects/29683cf2bd47015c9461b688749ddffd95f6671d/jjs.1" }, "raw": { "sha1": "78d419bd3a7f3e0802d5220e690429194b5d1beb", "size": 4932, "url": "https://launcher.mojang.com/v1/objects/78d419bd3a7f3e0802d5220e690429194b5d1beb/jjs.1" } }, "executable": false, "type": "file" }, "man/man1/keytool.1": { "downloads": { "lzma": { "sha1": "b67e5126d43713ee3675706724b34061578b42db", "size": 19690, "url": "https://launcher.mojang.com/v1/objects/b67e5126d43713ee3675706724b34061578b42db/keytool.1" }, "raw": { "sha1": "4c976f86057ab779763fcfb98f5702ebef47f629", "size": 86925, "url": "https://launcher.mojang.com/v1/objects/4c976f86057ab779763fcfb98f5702ebef47f629/keytool.1" } }, "executable": false, "type": "file" }, "man/man1/orbd.1": { "downloads": { "lzma": { "sha1": "147064d6f7e027002e296bb246ae572d0ce0495b", "size": 3708, "url": "https://launcher.mojang.com/v1/objects/147064d6f7e027002e296bb246ae572d0ce0495b/orbd.1" }, "raw": { "sha1": "64201e1846fcf1dcc45c786ffeab89426d1c7742", "size": 12180, "url": "https://launcher.mojang.com/v1/objects/64201e1846fcf1dcc45c786ffeab89426d1c7742/orbd.1" } }, "executable": false, "type": "file" }, "man/man1/pack200.1": { "downloads": { "lzma": { "sha1": "fe17486bbe9c58cf4182fa056b9cd124e8295607", "size": 3724, "url": "https://launcher.mojang.com/v1/objects/fe17486bbe9c58cf4182fa056b9cd124e8295607/pack200.1" }, "raw": { "sha1": "26826cf52b89924f2d2a60d6cda798891875eae6", "size": 11623, "url": "https://launcher.mojang.com/v1/objects/26826cf52b89924f2d2a60d6cda798891875eae6/pack200.1" } }, "executable": false, "type": "file" }, "man/man1/policytool.1": { "downloads": { "lzma": { "sha1": "bd154e7c39aca71d15b2098c588866f8d95bc743", "size": 1122, "url": "https://launcher.mojang.com/v1/objects/bd154e7c39aca71d15b2098c588866f8d95bc743/policytool.1" }, "raw": { "sha1": "ab296625155d9a2b25ecc2b4feff2f741b3ad136", "size": 3235, "url": "https://launcher.mojang.com/v1/objects/ab296625155d9a2b25ecc2b4feff2f741b3ad136/policytool.1" } }, "executable": false, "type": "file" }, "man/man1/rmid.1": { "downloads": { "lzma": { "sha1": "6a7da234e7f43ebca5c4ba8cd862fda3be62fbaa", "size": 4255, "url": "https://launcher.mojang.com/v1/objects/6a7da234e7f43ebca5c4ba8cd862fda3be62fbaa/rmid.1" }, "raw": { "sha1": "6f10e214d7950a6a8460524e41dc700f112f89e5", "size": 15979, "url": "https://launcher.mojang.com/v1/objects/6f10e214d7950a6a8460524e41dc700f112f89e5/rmid.1" } }, "executable": false, "type": "file" }, "man/man1/rmiregistry.1": { "downloads": { "lzma": { "sha1": "f40dd17e3a734600ad1828b0c42d3a1685c4c520", "size": 1301, "url": "https://launcher.mojang.com/v1/objects/f40dd17e3a734600ad1828b0c42d3a1685c4c520/rmiregistry.1" }, "raw": { "sha1": "d9a3d23fab689df5bb9a792b88f462f939b49f70", "size": 3449, "url": "https://launcher.mojang.com/v1/objects/d9a3d23fab689df5bb9a792b88f462f939b49f70/rmiregistry.1" } }, "executable": false, "type": "file" }, "man/man1/servertool.1": { "downloads": { "lzma": { "sha1": "74f1e10712202cd3ca0ff5833de05b7ee67092e1", "size": 2307, "url": "https://launcher.mojang.com/v1/objects/74f1e10712202cd3ca0ff5833de05b7ee67092e1/servertool.1" }, "raw": { "sha1": "e6c7b510740ac8681a9bfb5f4ee1f0306125b728", "size": 7237, "url": "https://launcher.mojang.com/v1/objects/e6c7b510740ac8681a9bfb5f4ee1f0306125b728/servertool.1" } }, "executable": false, "type": "file" }, "man/man1/tnameserv.1": { "downloads": { "lzma": { "sha1": "4bec7f4e070d023f124f9352a8971d7acd249a15", "size": 3955, "url": "https://launcher.mojang.com/v1/objects/4bec7f4e070d023f124f9352a8971d7acd249a15/tnameserv.1" }, "raw": { "sha1": "a31dbbe800d49cb371fab9a4b73d22c3bf8799ad", "size": 15747, "url": "https://launcher.mojang.com/v1/objects/a31dbbe800d49cb371fab9a4b73d22c3bf8799ad/tnameserv.1" } }, "executable": false, "type": "file" }, "man/man1/unpack200.1": { "downloads": { "lzma": { "sha1": "f8e73863187929debf2ea6dadefb2995ec7917e7", "size": 1672, "url": "https://launcher.mojang.com/v1/objects/f8e73863187929debf2ea6dadefb2995ec7917e7/unpack200.1" }, "raw": { "sha1": "437f7233d738cb9b822e99003127049005663e0f", "size": 4244, "url": "https://launcher.mojang.com/v1/objects/437f7233d738cb9b822e99003127049005663e0f/unpack200.1" } }, "executable": false, "type": "file" }, "plugin": { "type": "directory" }, "plugin/desktop": { "type": "directory" }, "plugin/desktop/sun_java.desktop": { "downloads": { "lzma": { "sha1": "49ab0ccb54c3be68281d05055bc56a88b1281d3c", "size": 447, "url": "https://launcher.mojang.com/v1/objects/49ab0ccb54c3be68281d05055bc56a88b1281d3c/sun_java.desktop" }, "raw": { "sha1": "79120ee8160ad6f3c9b90c2641fb7edf3af96b5d", "size": 624, "url": "https://launcher.mojang.com/v1/objects/79120ee8160ad6f3c9b90c2641fb7edf3af96b5d/sun_java.desktop" } }, "executable": false, "type": "file" }, "plugin/desktop/sun_java.png": { "downloads": { "raw": { "sha1": "699c41e97a35414e72a80327a54d6e14e874e951", "size": 4351, "url": "https://launcher.mojang.com/v1/objects/699c41e97a35414e72a80327a54d6e14e874e951/sun_java.png" } }, "executable": false, "type": "file" }, "release": { "downloads": { "raw": { "sha1": "cb462682644c0275d94a45b759108815f3112064", "size": 424, "url": "https://launcher.mojang.com/v1/objects/cb462682644c0275d94a45b759108815f3112064/release" } }, "executable": false, "type": "file" } } }PrismLauncher-11.0.3/tests/testdata/PackageManifest/inspect_win/0000755000175100017510000000000015224505336024302 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/PackageManifest/inspect_win/a/0000755000175100017510000000000015224505336024522 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/PackageManifest/inspect_win/a/b.txt0000644000175100017510000000000015224505336025472 0ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/PackageManifest/inspect_win/a/b/0000755000175100017510000000000015224505336024743 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/PackageManifest/inspect_win/a/b/b.txt0000644000175100017510000000000015224505336025713 0ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/ShaderPackParse/0000755000175100017510000000000015224505336021736 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/ShaderPackParse/shaderpack3.zip0000644000175100017510000000020015224505336024642 0ustar runnerrunnerPKlUno_shaders_herePK?lUno_shaders_herePK=-PrismLauncher-11.0.3/tests/testdata/ShaderPackParse/shaderpack1.zip0000644000175100017510000000036215224505336024651 0ustar runnerrunnerPKuUshaders/PKGUshaders/shaders.propertiesPK?uUAshaders/PK?GU&shaders/shaders.propertiesPK~^PrismLauncher-11.0.3/tests/testdata/ShaderPackParse/shaderpack2/0000755000175100017510000000000015224505336024125 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/ShaderPackParse/shaderpack2/shaders/0000755000175100017510000000000015224505336025556 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/ShaderPackParse/shaderpack2/shaders/shaders.properties0000644000175100017510000000000015224505336031313 0ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/Version/0000755000175100017510000000000015224505336020363 5ustar runnerrunnerPrismLauncher-11.0.3/tests/testdata/Version/test_vectors.txt0000644000175100017510000000326715224505336023660 0ustar runnerrunner# Test vector from: # https://git.sleeping.town/exa/FlexVer/src/branch/trunk/test/test_vectors.txt # # This test file is formatted as " ", seperated by the space character # Implementations should ignore lines starting with "#" and lines that have a length of 0 # Basic numeric ordering (lexical string sort fails these) 10 > 2 100 > 10 # Trivial common numerics 1.0 < 1.1 1.0 < 1.0.1 1.1 > 1.0.1 # SemVer compatibility 1.5 > 1.5-pre1 1.5 = 1.5+foobar # SemVer incompatibility 1.5 < 1.5-2 1.5-pre10 > 1.5-pre2 # Empty strings = 1 > < 1 # Check boundary between textual and prerelease a-a < a # Check boundary between textual and appendix a+a = a # Dash is included in prerelease comparison (if stripped it will be a smaller component) # Note that a-a < a=a regardless since the prerelease splits the component creating a smaller first component; 0 is added to force splitting regardless a0-a < a0=a # Pre-releases must contain only non-digit 1.16.5-10 > 1.16.5 # Pre-releases can have multiple dashes (should not be split) # Reasoning for test data: "p-a!" > "p-a-" (correct); "p-a!" < "p-a t-" (what happens if every dash creates a new component) -a- > -a! # Misc b1.7.3 > a1.2.6 b1.2.6 > a1.7.3 a1.1.2 < a1.1.2_01 1.16.5-0.00.5 > 1.14.2-1.3.7 1.0.0 < 1.0.0_01 1.0.1 > 1.0.0_01 1.0.0_01 < 1.0.1 0.17.1-beta.1 < 0.17.1 0.17.1-beta.1 < 0.17.1-beta.2 1.4.5_01 = 1.4.5_01+fabric-1.17 1.4.5_01 = 1.4.5_01+fabric-1.17+ohgod 14w16a < 18w40b 18w40a < 18w40b 1.4.5_01+fabric-1.17 < 18w40b 13w02a < c0.3.0_01 0.6.0-1.18.x < 0.9.beta-1.18.x # removeLeadingZeroes (#17) 0000.0.0 = 0.0.0 0000.00.0 = 0.00.0 0.0.0 = 0.00.0000 # General leading zeroes 1.0.01 = 1.0.1 1.0.0001 = 1.0.01 PrismLauncher-11.0.3/tests/WorldSaveParse_test.cpp0000644000175100017510000000547715224505336021606 0ustar runnerrunner // SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include class WorldSaveParseTest : public QObject { Q_OBJECT private slots: void test_parseZIP() { QString source = QFINDTESTDATA("testdata/WorldSaveParse"); QString zip_ws = FS::PathCombine(source, "minecraft_save_1.zip"); WorldSave save{ QFileInfo(zip_ws) }; bool valid = WorldSaveUtils::processZIP(save); QVERIFY(save.saveFormat() == WorldSaveFormat::SINGLE); QVERIFY(save.saveDirName() == "world_1"); QVERIFY(valid == true); } void test_parse_ZIP2() { QString source = QFINDTESTDATA("testdata/WorldSaveParse"); QString zip_ws = FS::PathCombine(source, "minecraft_save_2.zip"); WorldSave save{ QFileInfo(zip_ws) }; bool valid = WorldSaveUtils::processZIP(save); QVERIFY(save.saveFormat() == WorldSaveFormat::MULTI); QVERIFY(save.saveDirName() == "world_2"); QVERIFY(valid == true); } void test_parseFolder() { QString source = QFINDTESTDATA("testdata/WorldSaveParse"); QString folder_ws = FS::PathCombine(source, "minecraft_save_3"); WorldSave save{ QFileInfo(folder_ws) }; bool valid = WorldSaveUtils::processFolder(save); QVERIFY(save.saveFormat() == WorldSaveFormat::SINGLE); QVERIFY(save.saveDirName() == "world_3"); QVERIFY(valid == true); } void test_parseFolder2() { QString source = QFINDTESTDATA("testdata/WorldSaveParse"); QString folder_ws = FS::PathCombine(source, "minecraft_save_4"); WorldSave save{ QFileInfo(folder_ws) }; bool valid = WorldSaveUtils::process(save); QVERIFY(save.saveFormat() == WorldSaveFormat::MULTI); QVERIFY(save.saveDirName() == "world_4"); QVERIFY(valid == true); } }; QTEST_GUILESS_MAIN(WorldSaveParseTest) #include "WorldSaveParse_test.moc" PrismLauncher-11.0.3/tests/JavaVersion_test.cpp0000644000175100017510000001300515224505336021116 0ustar runnerrunner#include #include class JavaVersionTest : public QObject { Q_OBJECT private slots: void test_Parse_data() { QTest::addColumn("string"); QTest::addColumn("major"); QTest::addColumn("minor"); QTest::addColumn("security"); QTest::addColumn("prerelease"); QTest::newRow("old format") << "1.6.0_33" << 6 << 0 << 33 << QString(); QTest::newRow("old format prerelease") << "1.9.0_1-ea" << 9 << 0 << 1 << "ea"; QTest::newRow("new format major") << "9" << 9 << 0 << 0 << QString(); QTest::newRow("new format minor") << "9.1" << 9 << 1 << 0 << QString(); QTest::newRow("new format security") << "9.0.1" << 9 << 0 << 1 << QString(); QTest::newRow("new format prerelease") << "9-ea" << 9 << 0 << 0 << "ea"; QTest::newRow("new format long prerelease") << "9.0.1-ea" << 9 << 0 << 1 << "ea"; } void test_Parse() { QFETCH(QString, string); QFETCH(int, major); QFETCH(int, minor); QFETCH(int, security); QFETCH(QString, prerelease); JavaVersion test(string); QCOMPARE(test.m_string, string); QCOMPARE(test.toString(), string); QCOMPARE(test.m_major, major); QCOMPARE(test.m_minor, minor); QCOMPARE(test.m_security, security); QCOMPARE(test.m_prerelease, prerelease); } void test_Sort_data() { QTest::addColumn("lhs"); QTest::addColumn("rhs"); QTest::addColumn("smaller"); QTest::addColumn("equal"); QTest::addColumn("bigger"); // old format and new format equivalence QTest::newRow("1.6.0_33 == 6.0.33") << "1.6.0_33" << "6.0.33" << false << true << false; // old format major version QTest::newRow("1.5.0_33 < 1.6.0_33") << "1.5.0_33" << "1.6.0_33" << true << false << false; // new format - first release vs first security patch QTest::newRow("9 < 9.0.1") << "9" << "9.0.1" << true << false << false; QTest::newRow("9.0.1 > 9") << "9.0.1" << "9" << false << false << true; // new format - first minor vs first release/security patch QTest::newRow("9.1 > 9.0.1") << "9.1" << "9.0.1" << false << false << true; QTest::newRow("9.0.1 < 9.1") << "9.0.1" << "9.1" << true << false << false; QTest::newRow("9.1 > 9") << "9.1" << "9" << false << false << true; QTest::newRow("9 > 9.1") << "9" << "9.1" << true << false << false; // new format - omitted numbers QTest::newRow("9 == 9.0") << "9" << "9.0" << false << true << false; QTest::newRow("9 == 9.0.0") << "9" << "9.0.0" << false << true << false; QTest::newRow("9.0 == 9.0.0") << "9.0" << "9.0.0" << false << true << false; // early access and prereleases compared to final release QTest::newRow("9-ea < 9") << "9-ea" << "9" << true << false << false; QTest::newRow("9 < 9.0.1-ea") << "9" << "9.0.1-ea" << true << false << false; QTest::newRow("9.0.1-ea > 9") << "9.0.1-ea" << "9" << false << false << true; // prerelease difference only testing QTest::newRow("9-1 == 9-1") << "9-1" << "9-1" << false << true << false; QTest::newRow("9-1 < 9-2") << "9-1" << "9-2" << true << false << false; QTest::newRow("9-5 < 9-20") << "9-5" << "9-20" << true << false << false; QTest::newRow("9-rc1 < 9-rc2") << "9-rc1" << "9-rc2" << true << false << false; QTest::newRow("9-rc5 < 9-rc20") << "9-rc5" << "9-rc20" << true << false << false; QTest::newRow("9-rc < 9-rc2") << "9-rc" << "9-rc2" << true << false << false; QTest::newRow("9-ea < 9-rc") << "9-ea" << "9-rc" << true << false << false; } void test_Sort() { QFETCH(QString, lhs); QFETCH(QString, rhs); QFETCH(bool, smaller); QFETCH(bool, equal); QFETCH(bool, bigger); JavaVersion lver(lhs); JavaVersion rver(rhs); QCOMPARE(lver < rver, smaller); QCOMPARE(lver == rver, equal); QCOMPARE(lver > rver, bigger); } void test_PermGen_data() { QTest::addColumn("version"); QTest::addColumn("needs_permgen"); QTest::newRow("1.6.0_33") << "1.6.0_33" << true; QTest::newRow("1.7.0_60") << "1.7.0_60" << true; QTest::newRow("1.8.0_22") << "1.8.0_22" << false; QTest::newRow("9-ea") << "9-ea" << false; QTest::newRow("9.2.4") << "9.2.4" << false; } void test_PermGen() { QFETCH(QString, version); QFETCH(bool, needs_permgen); JavaVersion v(version); QCOMPARE(needs_permgen, v.requiresPermGen()); } }; QTEST_GUILESS_MAIN(JavaVersionTest) #include "JavaVersion_test.moc" PrismLauncher-11.0.3/tests/GZip_test.cpp0000644000175100017510000000240415224505336017541 0ustar runnerrunner#include #include #include void fib(int& prev, int& cur) { auto ret = prev + cur; prev = cur; cur = ret; } class GZipTest : public QObject { Q_OBJECT private slots: void test_Through() { // test up to 10 MB static const int size = 10 * 1024 * 1024; QByteArray random; QByteArray compressed; QByteArray decompressed; std::default_random_engine eng((std::random_device())()); std::uniform_int_distribution idis(0, std::numeric_limits::max()); // initialize random buffer for (int i = 0; i < size; i++) { random.append(static_cast(idis(eng))); } // initialize fibonacci int prev = 1; int cur = 1; // test if fibonacci long random buffers pass through GZip do { QByteArray copy = random; copy.resize(cur); compressed.clear(); decompressed.clear(); QVERIFY(GZip::zip(copy, compressed)); QVERIFY(GZip::unzip(compressed, decompressed)); QCOMPARE(decompressed, copy); fib(prev, cur); } while (cur < size); } }; QTEST_GUILESS_MAIN(GZipTest) #include "GZip_test.moc" PrismLauncher-11.0.3/tests/Library_test.cpp0000644000175100017510000003651315224505336020304 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include class LibraryTest : public QObject { Q_OBJECT private: LibraryPtr readMojangJson(const QString path) { QFile jsonFile(path); if (!jsonFile.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file" << jsonFile.fileName() << "for reading:" << jsonFile.errorString(); return LibraryPtr(); } auto data = jsonFile.readAll(); jsonFile.close(); ProblemContainer problems; return MojangVersionFormat::libraryFromJson(problems, QJsonDocument::fromJson(data).object(), path); } // get absolute path to expected storage, assuming default cache prefix QStringList getStorage(QString relative) { return { FS::PathCombine(cache->getBasePath("libraries"), relative) }; } RuntimeContext dummyContext(QString system = "linux", QString arch = "64", QString realArch = "amd64") { RuntimeContext r; r.javaArchitecture = arch; r.javaRealArchitecture = realArch; r.system = system; return r; } private slots: void initTestCase() { cache.reset(new HttpMetaCache()); cache->addBase("libraries", QDir("libraries").absolutePath()); dataDir = QDir(QFINDTESTDATA("testdata/Libraries")).absolutePath(); } void test_legacy() { RuntimeContext r = dummyContext(); Library test("test.package:testname:testversion"); QCOMPARE(test.artifactPrefix(), QString("test.package:testname")); QCOMPARE(test.isNative(), false); QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, getStorage("test/package/testname/testversion/testname-testversion.jar")); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } void test_legacy_url() { RuntimeContext r = dummyContext(); QStringList failedFiles; Library test("test.package:testname:testversion"); test.setRepositoryURL("file://foo/bar"); auto downloads = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(downloads.size(), 1); QCOMPARE(failedFiles, {}); Net::NetRequest::Ptr dl = downloads[0]; QCOMPARE(dl->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion.jar")); } void test_legacy_url_local_broken() { RuntimeContext r = dummyContext(); Library test("test.package:testname:testversion"); QCOMPARE(test.isNative(), false); QStringList failedFiles; test.setHint("local"); auto downloads = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(downloads.size(), 0); QCOMPARE(failedFiles, { "testname-testversion.jar" }); } void test_legacy_url_local_override() { RuntimeContext r = dummyContext(); Library test("com.paulscode:codecwav:20101023"); QCOMPARE(test.isNative(), false); QStringList failedFiles; test.setHint("local"); auto downloads = test.getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Libraries")); QCOMPARE(downloads.size(), 0); qDebug() << failedFiles; QCOMPARE(failedFiles.size(), 0); QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Libraries")); QCOMPARE(jar, { QFileInfo(QFINDTESTDATA("testdata/Libraries/codecwav-20101023.jar")).absoluteFilePath() }); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } void test_legacy_native() { RuntimeContext r = dummyContext(); Library test("test.package:testname:testversion"); test.m_nativeClassifiers["linux"] = "linux"; QCOMPARE(test.isNative(), true); test.setRepositoryURL("file://foo/bar"); { QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, {}); QCOMPARE(native, getStorage("test/package/testname/testversion/testname-testversion-linux.jar")); QCOMPARE(native32, {}); QCOMPARE(native64, {}); QStringList failedFiles; auto dls = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 1); QCOMPARE(failedFiles, {}); auto dl = dls[0]; QCOMPARE(dl->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux.jar")); } } void test_legacy_native_arch() { RuntimeContext r = dummyContext(); Library test("test.package:testname:testversion"); test.m_nativeClassifiers["linux"] = "linux-${arch}"; test.m_nativeClassifiers["osx"] = "osx-${arch}"; test.m_nativeClassifiers["windows"] = "windows-${arch}"; QCOMPARE(test.isNative(), true); test.setRepositoryURL("file://foo/bar"); { QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, {}); QCOMPARE(native, {}); QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-linux-32.jar")); QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-linux-64.jar")); QStringList failedFiles; auto dls = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 2); QCOMPARE(failedFiles, {}); QCOMPARE(dls[0]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-32.jar")); QCOMPARE(dls[1]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-64.jar")); } r.system = "windows"; { QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, {}); QCOMPARE(native, {}); QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-windows-32.jar")); QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-windows-64.jar")); QStringList failedFiles; auto dls = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 2); QCOMPARE(failedFiles, {}); QCOMPARE(dls[0]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-32.jar")); QCOMPARE(dls[1]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-64.jar")); } r.system = "osx"; { QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, {}); QCOMPARE(native, {}); QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-osx-32.jar")); QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-osx-64.jar")); QStringList failedFiles; auto dls = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 2); QCOMPARE(failedFiles, {}); QCOMPARE(dls[0]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-32.jar")); QCOMPARE(dls[1]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-64.jar")); } } void test_legacy_native_arch_local_override() { RuntimeContext r = dummyContext(); Library test("test.package:testname:testversion"); test.m_nativeClassifiers["linux"] = "linux-${arch}"; test.setHint("local"); QCOMPARE(test.isNative(), true); test.setRepositoryURL("file://foo/bar"); { QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Libraries")); QCOMPARE(jar, {}); QCOMPARE(native, {}); QCOMPARE(native32, { QFileInfo(QFINDTESTDATA("testdata/Libraries/testname-testversion-linux-32.jar")).absoluteFilePath() }); QCOMPARE(native64, { QFileInfo(QFINDTESTDATA("testdata/Libraries") + "/testname-testversion-linux-64.jar").absoluteFilePath() }); QStringList failedFiles; auto dls = test.getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Libraries")); QCOMPARE(dls.size(), 0); QCOMPARE(failedFiles, { QFileInfo(QFINDTESTDATA("testdata/Libraries") + "/testname-testversion-linux-64.jar").absoluteFilePath() }); } } void test_onenine() { RuntimeContext r = dummyContext("osx"); auto test = readMojangJson(QFINDTESTDATA("testdata/Libraries/lib-simple.json")); { QStringList jar, native, native32, native64; test->getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, getStorage("com/paulscode/codecwav/20101023/codecwav-20101023.jar")); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } r.system = "linux"; { QStringList failedFiles; auto dls = test->getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 1); QCOMPARE(failedFiles, {}); QCOMPARE(dls[0]->url(), QUrl("https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar")); } r.system = "osx"; test->setHint("local"); { QStringList jar, native, native32, native64; test->getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Libraries")); QCOMPARE(jar, { QFileInfo(QFINDTESTDATA("testdata/Libraries/codecwav-20101023.jar")).absoluteFilePath() }); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } r.system = "linux"; { QStringList failedFiles; auto dls = test->getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Libraries")); QCOMPARE(dls.size(), 0); QCOMPARE(failedFiles, {}); } } void test_onenine_local_override() { RuntimeContext r = dummyContext("osx"); auto test = readMojangJson(QFINDTESTDATA("testdata/Libraries/lib-simple.json")); test->setHint("local"); { QStringList jar, native, native32, native64; test->getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Libraries")); QCOMPARE(jar, { QFileInfo(QFINDTESTDATA("testdata/Libraries/codecwav-20101023.jar")).absoluteFilePath() }); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } r.system = "linux"; { QStringList failedFiles; auto dls = test->getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Libraries")); QCOMPARE(dls.size(), 0); QCOMPARE(failedFiles, {}); } } void test_onenine_native() { RuntimeContext r = dummyContext("osx"); auto test = readMojangJson(QFINDTESTDATA("testdata/Libraries/lib-native.json")); QStringList jar, native, native32, native64; test->getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, QStringList()); QCOMPARE(native, getStorage("org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar")); QCOMPARE(native32, {}); QCOMPARE(native64, {}); QStringList failedFiles; auto dls = test->getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 1); QCOMPARE(failedFiles, {}); QCOMPARE(dls[0]->url(), QUrl("https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/" "lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar")); } void test_onenine_native_arch() { RuntimeContext r = dummyContext("windows"); auto test = readMojangJson(QFINDTESTDATA("testdata/Libraries/lib-native-arch.json")); QStringList jar, native, native32, native64; test->getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, {}); QCOMPARE(native, {}); QCOMPARE(native32, getStorage("tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar")); QCOMPARE(native64, getStorage("tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar")); QStringList failedFiles; auto dls = test->getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 2); QCOMPARE(failedFiles, {}); QCOMPARE(dls[0]->url(), QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar")); QCOMPARE(dls[1]->url(), QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar")); } private: std::unique_ptr cache; QString dataDir; }; QTEST_GUILESS_MAIN(LibraryTest) #include "Library_test.moc" PrismLauncher-11.0.3/tests/ResourcePackParse_test.cpp0000644000175100017510000000531615224505336022256 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include "minecraft/mod/tasks/LocalDataPackParseTask.h" #include #include class ResourcePackParseTest : public QObject { Q_OBJECT private slots: void test_parseZIP() { QString source = QFINDTESTDATA("testdata/Resources"); QString zip_rp = FS::PathCombine(source, "test_resource_pack_idk.zip"); ResourcePack pack{ QFileInfo(zip_rp) }; bool valid = DataPackUtils::processZIP(&pack, DataPackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 3); QVERIFY(pack.description() == "um dois, feijão com arroz, três quatro, feijão no prato, cinco seis, café inglês, sete oito, comer biscoito, nove dez " "comer pastéis!!"); QVERIFY(valid == true); } void test_parseFolder() { QString source = QFINDTESTDATA("testdata/Resources"); QString folder_rp = FS::PathCombine(source, "test_folder"); ResourcePack pack{ QFileInfo(folder_rp) }; bool valid = DataPackUtils::processFolder(&pack, DataPackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 1); QVERIFY(pack.description() == "Some resource pack maybe"); QVERIFY(valid == true); } void test_parseFolder2() { QString source = QFINDTESTDATA("testdata/Resources"); QString folder_rp = FS::PathCombine(source, "another_test_folder"); ResourcePack pack{ QFileInfo(folder_rp) }; bool valid = DataPackUtils::process(&pack, DataPackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 6); QVERIFY(pack.description() == "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional"); QVERIFY(valid == true); // no assets dir but it is still valid based on https://minecraft.wiki/w/Resource_pack } }; QTEST_GUILESS_MAIN(ResourcePackParseTest) #include "ResourcePackParse_test.moc" PrismLauncher-11.0.3/tests/FileSystem_test.cpp0000644000175100017510000006310715224505336020763 0ustar runnerrunner#include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; class LinkTask : public Task { Q_OBJECT friend class FileSystemTest; LinkTask(QString src, QString dst) { m_lnk = new FS::create_link(src, dst, this); m_lnk->debug(true); } ~LinkTask() { delete m_lnk; } void matcher(Filter filter) { m_lnk->matcher(filter); } void linkRecursively(bool recursive) { m_lnk->linkRecursively(recursive); m_linkRecursive = recursive; } void whitelist(bool b) { m_lnk->whitelist(b); } void setMaxDepth(int depth) { m_lnk->setMaxDepth(depth); } private: void executeTask() override { if (!(*m_lnk)()) { #if defined Q_OS_WIN32 if (!m_useHard) { qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks"; qDebug() << "atempting to run with privelage"; connect(m_lnk, &FS::create_link::finishedPrivileged, this, [this](bool gotResults) { if (gotResults) { emitSucceeded(); } else { qDebug() << "Privileged run exited without results!"; emitFailed(); } }); m_lnk->runPrivileged(); } else { qDebug() << "Link Failed!" << m_lnk->getOSError().value() << m_lnk->getOSError().message().c_str(); } #else qDebug() << "Link Failed!" << m_lnk->getOSError().value() << m_lnk->getOSError().message().c_str(); #endif } else { emitSucceeded(); } } FS::create_link* m_lnk; #if defined Q_OS_WIN32 bool m_useHard = false; #endif bool m_linkRecursive = true; }; class FileSystemTest : public QObject { Q_OBJECT const QString bothSlash = "/foo/"; const QString trailingSlash = "foo/"; const QString leadingSlash = "/foo"; private slots: void test_pathCombine() { QCOMPARE(QString("/foo/foo"), FS::PathCombine(bothSlash, bothSlash)); QCOMPARE(QString("foo/foo"), FS::PathCombine(trailingSlash, trailingSlash)); QCOMPARE(QString("/foo/foo"), FS::PathCombine(leadingSlash, leadingSlash)); QCOMPARE(QString("/foo/foo/foo"), FS::PathCombine(bothSlash, bothSlash, bothSlash)); QCOMPARE(QString("foo/foo/foo"), FS::PathCombine(trailingSlash, trailingSlash, trailingSlash)); QCOMPARE(QString("/foo/foo/foo"), FS::PathCombine(leadingSlash, leadingSlash, leadingSlash)); } void test_PathCombine1_data() { QTest::addColumn("result"); QTest::addColumn("path1"); QTest::addColumn("path2"); QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc/def" << "ghi/jkl"; QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/def/" << "ghi/jkl"; #if defined(Q_OS_WIN) QTest::newRow("win native, from C:") << "C:/abc" << "C:" << "abc"; QTest::newRow("win native 1") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def" << "ghi\\jkl"; QTest::newRow("win native 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def\\" << "ghi\\jkl"; #endif } void test_PathCombine1() { QFETCH(QString, result); QFETCH(QString, path1); QFETCH(QString, path2); QCOMPARE(FS::PathCombine(path1, path2), result); } void test_PathCombine2_data() { QTest::addColumn("result"); QTest::addColumn("path1"); QTest::addColumn("path2"); QTest::addColumn("path3"); QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc" << "def" << "ghi/jkl"; QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/" << "def" << "ghi/jkl"; QTest::newRow("qt 3") << "/abc/def/ghi/jkl" << "/abc" << "def/" << "ghi/jkl"; QTest::newRow("qt 4") << "/abc/def/ghi/jkl" << "/abc/" << "def/" << "ghi/jkl"; #if defined(Q_OS_WIN) QTest::newRow("win 1") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def" << "ghi\\jkl"; QTest::newRow("win 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; QTest::newRow("win 3") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def\\" << "ghi\\jkl"; QTest::newRow("win 4") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; #endif } void test_PathCombine2() { QFETCH(QString, result); QFETCH(QString, path1); QFETCH(QString, path2); QFETCH(QString, path3); QCOMPARE(FS::PathCombine(path1, path2, path3), result); } void test_copy() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); c(); for (auto entry : target_dir.entryList()) { qDebug() << entry; } QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_copy_with_blacklist() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); c.matcher(re); c(); for (auto entry : target_dir.entryList()) { qDebug() << entry; } QVERIFY(!target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_copy_with_whitelist() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); c.matcher(re); c.whitelist(true); c(); for (auto entry : target_dir.entryList()) { qDebug() << entry; } QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(!target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_copy_with_dot_hidden() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); c(); auto filter = QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden; for (auto entry : target_dir.entryList(filter)) { qDebug() << entry; } QVERIFY(target_dir.entryList(filter).contains(".secret_folder")); target_dir.cd(".secret_folder"); QVERIFY(target_dir.entryList(filter).contains(".secret_file.txt")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_copy_single_file() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); { QString file = QFINDTESTDATA("testdata/FileSystem/test_folder/pack.mcmeta"); qDebug() << "From:" << file << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "pack.mcmeta")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(file, target_dir.filePath("pack.mcmeta")); c(); auto filter = QDir::Filter::Files; for (auto entry : target_dir.entryList(filter)) { qDebug() << entry; } QVERIFY(target_dir.entryList(filter).contains("pack.mcmeta")); } } void test_getDesktop() { QCOMPARE(FS::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); } void test_link() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(false); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); for (auto entry : target_dir.entryList()) { qDebug() << entry; QFileInfo entry_lnk_info(target_dir.filePath(entry)); if (!entry_lnk_info.isDir()) QVERIFY(!entry_lnk_info.isSymLink()); } QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(lnk_info.isSymLink()); QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_hard_link() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { // use working dir to prevent makeing a hard link to a tmpfs or across devices QTemporaryDir tempDir("./tmp"); tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::create_link lnk(folder, target_dir.path()); lnk.useHardLinks(true); lnk.debug(true); if (!lnk()) { qDebug() << "Link Failed!" << lnk.getOSError().value() << lnk.getOSError().message().c_str(); } for (auto entry : target_dir.entryList()) { qDebug() << entry; QFileInfo entry_lnk_info(target_dir.filePath(entry)); QVERIFY(!entry_lnk_info.isSymLink()); QFileInfo entry_orig_info(QDir(folder).filePath(entry)); if (!entry_lnk_info.isDir()) { qDebug() << "hard link equivalency?" << entry_lnk_info.absoluteFilePath() << "vs" << entry_orig_info.absoluteFilePath(); QVERIFY(fs::equivalent(fs::path(StringUtils::toStdString(entry_lnk_info.absoluteFilePath())), fs::path(StringUtils::toStdString(entry_orig_info.absoluteFilePath())))); } } QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(!lnk_info.isSymLink()); QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_link_with_blacklist() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); lnk_tsk.matcher(re); lnk_tsk.linkRecursively(true); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); for (auto entry : target_dir.entryList()) { qDebug() << entry; QFileInfo entry_lnk_info(target_dir.filePath(entry)); if (!entry_lnk_info.isDir()) QVERIFY(entry_lnk_info.isSymLink()); } QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(!target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_link_with_whitelist() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); lnk_tsk.matcher(re); lnk_tsk.linkRecursively(true); lnk_tsk.whitelist(true); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); for (auto entry : target_dir.entryList()) { qDebug() << entry; QFileInfo entry_lnk_info(target_dir.filePath(entry)); if (!entry_lnk_info.isDir()) QVERIFY(entry_lnk_info.isSymLink()); } QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(!target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_link_with_dot_hidden() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); auto filter = QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden; for (auto entry : target_dir.entryList(filter)) { qDebug() << entry; QFileInfo entry_lnk_info(target_dir.filePath(entry)); if (!entry_lnk_info.isDir()) QVERIFY(entry_lnk_info.isSymLink()); } QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(target_dir.entryList(filter).contains(".secret_folder")); target_dir.cd(".secret_folder"); QVERIFY(target_dir.entryList(filter).contains(".secret_file.txt")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_link_single_file() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); { QString file = QFINDTESTDATA("testdata/FileSystem/test_folder/pack.mcmeta"); qDebug() << "From:" << file << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "pack.mcmeta")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(file, target_dir.filePath("pack.mcmeta")); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); auto filter = QDir::Filter::Files; for (auto entry : target_dir.entryList(filter)) { qDebug() << entry; } QFileInfo lnk_info(target_dir.filePath("pack.mcmeta")); QVERIFY(lnk_info.exists()); QVERIFY(lnk_info.isSymLink()); QVERIFY(target_dir.entryList(filter).contains("pack.mcmeta")); } } void test_link_with_max_depth() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); lnk_tsk.setMaxDepth(0); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); QVERIFY(!QFileInfo(target_dir.path()).isSymLink()); auto filter = QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden; for (auto entry : target_dir.entryList(filter)) { qDebug() << entry; if (entry == "." || entry == "..") continue; QFileInfo entry_lnk_info(target_dir.filePath(entry)); QVERIFY(entry_lnk_info.isSymLink()); } QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(!lnk_info.isSymLink()); QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_link_with_no_max_depth() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); lnk_tsk.setMaxDepth(-1); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); std::function verify_check = [&verify_check](QString check_path) { QDir check_dir(check_path); auto filter = QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden; for (auto entry : check_dir.entryList(filter)) { QFileInfo entry_lnk_info(check_dir.filePath(entry)); qDebug() << entry << check_dir.filePath(entry); if (!entry_lnk_info.isDir()) { QVERIFY(entry_lnk_info.isSymLink()); } else if (entry != "." && entry != "..") { qDebug() << "Decending tree to verify symlinks:" << check_dir.filePath(entry); verify_check(entry_lnk_info.filePath()); } } }; verify_check(target_dir.path()); QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_path_depth() { QCOMPARE(FS::pathDepth(""), 0); QCOMPARE(FS::pathDepth("."), 0); QCOMPARE(FS::pathDepth("foo.txt"), 0); QCOMPARE(FS::pathDepth("./foo.txt"), 0); QCOMPARE(FS::pathDepth("./bar/foo.txt"), 1); QCOMPARE(FS::pathDepth("../bar/foo.txt"), 0); QCOMPARE(FS::pathDepth("/bar/foo.txt"), 1); QCOMPARE(FS::pathDepth("baz/bar/foo.txt"), 2); QCOMPARE(FS::pathDepth("/baz/bar/foo.txt"), 2); QCOMPARE(FS::pathDepth("./baz/bar/foo.txt"), 2); QCOMPARE(FS::pathDepth("/baz/../bar/foo.txt"), 1); } void test_path_trunc() { QCOMPARE(FS::pathTruncate("", 0), QDir::toNativeSeparators("")); QCOMPARE(FS::pathTruncate("foo.txt", 0), QDir::toNativeSeparators("")); QCOMPARE(FS::pathTruncate("foo.txt", 1), QDir::toNativeSeparators("")); QCOMPARE(FS::pathTruncate("./bar/foo.txt", 0), QDir::toNativeSeparators("./bar")); QCOMPARE(FS::pathTruncate("./bar/foo.txt", 1), QDir::toNativeSeparators("./bar")); QCOMPARE(FS::pathTruncate("/bar/foo.txt", 1), QDir::toNativeSeparators("/bar")); QCOMPARE(FS::pathTruncate("bar/foo.txt", 1), QDir::toNativeSeparators("bar")); QCOMPARE(FS::pathTruncate("baz/bar/foo.txt", 2), QDir::toNativeSeparators("baz/bar")); #if defined(Q_OS_WIN) QCOMPARE(FS::pathTruncate("C:\\bar\\foo.txt", 1), QDir::toNativeSeparators("C:\\bar")); #endif } }; QTEST_GUILESS_MAIN(FileSystemTest) #include "FileSystem_test.moc" PrismLauncher-11.0.3/tests/Task_test.cpp0000644000175100017510000001677015224505336017605 0ustar runnerrunner#include #include #include #include #include #include #include #include /* Does nothing. Only used for testing. */ class BasicTask : public Task { Q_OBJECT friend class TaskTest; public: BasicTask(bool show_debug_log = true) : Task(show_debug_log) {} private: void executeTask() override { emitSucceeded(); } }; /* Does nothing. Only used for testing. */ class BasicTask_MultiStep : public Task { Q_OBJECT friend class TaskTest; private: auto isMultiStep() const -> bool override { return true; } void executeTask() override {} }; class BigConcurrentTask : public ConcurrentTask { Q_OBJECT void executeNextSubTask() override { // This is here only to help fill the stack a bit more quickly (if there's an issue, of course :^)) // Each tasks thus adds 1024 * 4 bytes to the stack, at the very least. [[maybe_unused]] volatile std::array some_data_on_the_stack{}; ConcurrentTask::executeNextSubTask(); } }; class BigConcurrentTaskThread : public QThread { Q_OBJECT QTimer m_deadline; void run() override { BigConcurrentTask big_task; m_deadline.setInterval(10000); // NOTE: Arbitrary value that manages to trigger a problem when there is one. // Considering each tasks, in a problematic state, adds 1024 * 4 bytes to the stack, // this number is enough to fill up 16 MiB of stack, more than enough to cause a problem. static const unsigned s_num_tasks = 1 << 12; for (unsigned i = 0; i < s_num_tasks; i++) { auto sub_task = makeShared(false); big_task.addTask(sub_task); } connect(&big_task, &Task::finished, this, &QThread::quit); connect(&m_deadline, &QTimer::timeout, this, [this] { passed_the_deadline = true; quit(); }); if (thread() != QThread::currentThread()) { QMetaObject::invokeMethod(this, &BigConcurrentTaskThread::start_timer, Qt::QueuedConnection); } big_task.run(); exec(); } void start_timer() { m_deadline.start(); } public: bool passed_the_deadline = false; }; class TaskTest : public QObject { Q_OBJECT private slots: void test_SetStatus_NoMultiStep() { BasicTask t; QString status{ "test status" }; t.setStatus(status); QCOMPARE(t.getStatus(), status); QCOMPARE(t.getStepProgress().isEmpty(), TaskStepProgressList{}.isEmpty()); } void test_SetStatus_MultiStep() { BasicTask_MultiStep t; QString status{ "test status" }; t.setStatus(status); QCOMPARE(t.getStatus(), status); // Even though it is multi step, it does not override the getStepStatus method, // so it should remain the same. QCOMPARE(t.getStepProgress().isEmpty(), TaskStepProgressList{}.isEmpty()); } void test_SetProgress() { BasicTask t; int current = 42; int total = 207; t.setProgress(current, total); QCOMPARE(t.getProgress(), current); QCOMPARE(t.getTotalProgress(), total); } void test_basicRun() { BasicTask t; connect(&t, &Task::finished, [&t] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); }); t.start(); QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_basicConcurrentRun() { auto t1 = makeShared(); auto t2 = makeShared(); auto t3 = makeShared(); ConcurrentTask t; t.addTask(t1); t.addTask(t2); t.addTask(t3); connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(t2->wasSuccessful()); QVERIFY(t3->wasSuccessful()); }); t.start(); QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } // Tests if starting new tasks after the 6 initial ones is working void test_moreConcurrentRun() { auto t1 = makeShared(); auto t2 = makeShared(); auto t3 = makeShared(); auto t4 = makeShared(); auto t5 = makeShared(); auto t6 = makeShared(); auto t7 = makeShared(); auto t8 = makeShared(); auto t9 = makeShared(); ConcurrentTask t; t.addTask(t1); t.addTask(t2); t.addTask(t3); t.addTask(t4); t.addTask(t5); t.addTask(t6); t.addTask(t7); t.addTask(t8); t.addTask(t9); connect(&t, &Task::finished, [&t, &t1, &t2, &t3, &t4, &t5, &t6, &t7, &t8, &t9] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(t2->wasSuccessful()); QVERIFY(t3->wasSuccessful()); QVERIFY(t4->wasSuccessful()); QVERIFY(t5->wasSuccessful()); QVERIFY(t6->wasSuccessful()); QVERIFY(t7->wasSuccessful()); QVERIFY(t8->wasSuccessful()); QVERIFY(t9->wasSuccessful()); }); t.start(); QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_basicSequentialRun() { auto t1 = makeShared(); auto t2 = makeShared(); auto t3 = makeShared(); SequentialTask t; t.addTask(t1); t.addTask(t2); t.addTask(t3); connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(t2->wasSuccessful()); QVERIFY(t3->wasSuccessful()); }); t.start(); QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_basicMultipleOptionsRun() { auto t1 = makeShared(); auto t2 = makeShared(); auto t3 = makeShared(); MultipleOptionsTask t; t.addTask(t1); t.addTask(t2); t.addTask(t3); connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(!t2->wasSuccessful()); QVERIFY(!t3->wasSuccessful()); }); t.start(); QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_stackOverflowInConcurrentTask() { QEventLoop loop; BigConcurrentTaskThread thread; connect(&thread, &BigConcurrentTaskThread::finished, &loop, &QEventLoop::quit); thread.start(); loop.exec(); QVERIFY(!thread.passed_the_deadline); } }; QTEST_GUILESS_MAIN(TaskTest) #include "Task_test.moc" PrismLauncher-11.0.3/tests/INIFile_test.cpp0000644000175100017510000001466615224505336020124 0ustar runnerrunner#include #include #include #include #include #include #include "FileSystem.h" #include class IniFileTest : public QObject { Q_OBJECT private slots: void initTestCase() {} void cleanupTestCase() {} void test_Escape_data() { QTest::addColumn("through"); QTest::newRow("unix path") << "/abc/def/ghi/jkl"; QTest::newRow("windows path") << "C:\\Program files\\terrible\\name\\of something\\"; QTest::newRow("Plain text") << "Lorem ipsum dolor sit amet."; QTest::newRow("Escape sequences") << "Lorem\n\t\n\\n\\tAAZ\nipsum dolor\n\nsit amet."; QTest::newRow("Escape sequences 2") << "\"\n\n\""; QTest::newRow("Hashtags") << "some data#something"; } void test_SaveLoad() { QString a = "a"; QString b = "a\nb\t\n\\\\\\C:\\Program files\\terrible\\name\\of something\\#thisIsNotAComment"; QString filename = "test_SaveLoad.ini"; // save INIFile f; f.set("a", a); f.set("b", b); f.saveFile(filename); // load INIFile f2; f2.loadFile(filename); QCOMPARE(f2.get("a", "NOT SET").toString(), a); QCOMPARE(f2.get("b", "NOT SET").toString(), b); } void test_SaveLoadLists() { QString slist_strings = "(\"a\",\"b\",\"c\")"; QStringList list_strings = { "a", "b", "c" }; QString slist_numbers = "(1,2,3,10)"; QList list_numbers = { 1, 2, 3, 10 }; QString filename = "test_SaveLoadLists.ini"; INIFile f; f.set("list_strings", list_strings); f.set("list_numbers", QVariantUtils::fromList(list_numbers)); f.saveFile(filename); // load INIFile f2; f2.loadFile(filename); QStringList out_list_strings = f2.get("list_strings", QStringList()).toStringList(); qDebug() << "OutStringList" << out_list_strings; QList out_list_numbers = QVariantUtils::toList(f2.get("list_numbers", QVariantUtils::fromList(QList()))); qDebug() << "OutNumbersList" << out_list_numbers; QCOMPARE(out_list_strings, list_strings); QCOMPARE(out_list_numbers, list_numbers); } void test_SaveAlreadyExistingFile() { QString fileContent = R"(InstanceType=OneSix iconKey=vanillia_icon name=Minecraft Vanillia OverrideCommands=true PreLaunchCommand="$INST_JAVA" -jar packwiz-installer-bootstrap.jar link Wrapperommand=)"; fileContent += "\""; fileContent += +R"(\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link =)"; fileContent += "\"\n"; #if defined(Q_OS_WIN) QString fileName = "test_SaveAlreadyExistingFile.ini"; QFile file(fileName); QCOMPARE(file.open(QFile::WriteOnly | QFile::Text), true); #else QTemporaryFile file; QCOMPARE(file.open(), true); QCOMPARE(file.fileName().isEmpty(), false); QString fileName = file.fileName(); #endif QTextStream stream(&file); stream << fileContent; file.close(); // load INIFile f1; f1.loadFile(fileName); QCOMPARE(f1.get("PreLaunchCommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link"); QCOMPARE(f1.get("Wrapperommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link ="); f1.saveFile(fileName); INIFile f2; f2.loadFile(fileName); QCOMPARE(f2.get("PreLaunchCommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link"); QCOMPARE(f2.get("Wrapperommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link ="); QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.3"); #if defined(Q_OS_WIN) FS::deletePath(fileName); #endif } void test_SaveAlreadyExistingFileWithSpecialChars() { #if defined(Q_OS_WIN) QString fileName = "test_SaveAlreadyExistingFileWithSpecialChars.ini"; #else QTemporaryFile file; QCOMPARE(file.open(), true); QCOMPARE(file.fileName().isEmpty(), false); QString fileName = file.fileName(); file.close(); #endif QSettings settings{ fileName, QSettings::Format::IniFormat }; settings.setFallbacksEnabled(false); settings.setValue("simple", "value1"); settings.setValue("withQuotes", R"("value2" with quotes)"); settings.setValue("withSpecialCharacters", "env mesa=true"); settings.setValue("withSpecialCharacters2", "1,2,3,4"); settings.setValue("withSpecialCharacters2", "1;2;3;4"); settings.setValue("withAll", "val=\"$INST_JAVA\" -jar; ls "); settings.sync(); QCOMPARE(settings.status(), QSettings::Status::NoError); // load INIFile f1; f1.loadFile(fileName); for (auto key : settings.allKeys()) QCOMPARE(f1.get(key, "NOT SET").toString(), settings.value(key).toString()); f1.saveFile(fileName); INIFile f2; f2.loadFile(fileName); for (auto key : settings.allKeys()) QCOMPARE(f2.get(key, "NOT SET").toString(), settings.value(key).toString()); QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.3"); #if defined(Q_OS_WIN) FS::deletePath(fileName); #endif } void test_SaveAlreadyExistingFileWithSpecialCharsV1() { QString fileContent = R"(InstanceType=OneSix ConfigVersion=1.1 iconKey=vanillia_icon name=Minecraft Vanillia OverrideCommands=true PreLaunchCommand=)"; fileContent += "\"\\\"env mesa=true\\\"\"\n"; #if defined(Q_OS_WIN) QString fileName = "test_SaveAlreadyExistingFileWithSpecialCharsV1.ini"; QFile file(fileName); QCOMPARE(file.open(QFile::WriteOnly | QFile::Text), true); #else QTemporaryFile file; QCOMPARE(file.open(), true); QCOMPARE(file.fileName().isEmpty(), false); QString fileName = file.fileName(); #endif QTextStream stream(&file); stream << fileContent; file.close(); // load INIFile f1; f1.loadFile(fileName); QCOMPARE(f1.get("PreLaunchCommand", "NOT SET").toString(), "env mesa=true"); QCOMPARE(f1.get("ConfigVersion", "NOT SET").toString(), "1.3"); #if defined(Q_OS_WIN) FS::deletePath(fileName); #endif } }; QTEST_GUILESS_MAIN(IniFileTest) #include "INIFile_test.moc" PrismLauncher-11.0.3/tests/MetaComponentParse_test.cpp0000644000175100017510000000604415224505336022440 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include class MetaComponentParseTest : public QObject { Q_OBJECT void doTest(QString name) { QString source = QFINDTESTDATA("testdata/MetaComponentParse"); QString comp_rp = FS::PathCombine(source, name); QFile file; file.setFileName(comp_rp); QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text)); QString data = file.readAll(); file.close(); QJsonDocument doc = QJsonDocument::fromJson(data.toUtf8()); QJsonObject obj = doc.object(); QJsonValue description_json = obj.value("description"); QJsonValue expected_json = obj.value("expected_output"); QVERIFY(!description_json.isUndefined()); QVERIFY(expected_json.isString()); QString expected = expected_json.toString(); QString processed = DataPackUtils::processComponent(description_json); QCOMPARE(processed, expected); } private slots: void test_parseComponentBasic() { doTest("component_basic.json"); } void test_parseComponentWithFormat() { doTest("component_with_format.json"); } void test_parseComponentWithExtra() { doTest("component_with_extra.json"); } void test_parseComponentWithLink() { doTest("component_with_link.json"); } void test_parseComponentWithMixed() { doTest("component_with_mixed.json"); } }; QTEST_GUILESS_MAIN(MetaComponentParseTest) #include "MetaComponentParse_test.moc" PrismLauncher-11.0.3/tests/ShaderPackParse_test.cpp0000644000175100017510000000440115224505336021667 0ustar runnerrunner // SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include class ShaderPackParseTest : public QObject { Q_OBJECT private slots: void test_parseZIP() { QString source = QFINDTESTDATA("testdata/ShaderPackParse"); QString zip_sp = FS::PathCombine(source, "shaderpack1.zip"); ShaderPack pack{ QFileInfo(zip_sp) }; bool valid = ShaderPackUtils::processZIP(pack); QVERIFY(pack.packFormat() == ShaderPackFormat::VALID); QVERIFY(valid == true); } void test_parseFolder() { QString source = QFINDTESTDATA("testdata/ShaderPackParse"); QString folder_sp = FS::PathCombine(source, "shaderpack2"); ShaderPack pack{ QFileInfo(folder_sp) }; bool valid = ShaderPackUtils::processFolder(pack); QVERIFY(pack.packFormat() == ShaderPackFormat::VALID); QVERIFY(valid == true); } void test_parseZIP2() { QString source = QFINDTESTDATA("testdata/ShaderPackParse"); QString folder_sp = FS::PathCombine(source, "shaderpack3.zip"); ShaderPack pack{ QFileInfo(folder_sp) }; bool valid = ShaderPackUtils::process(pack); QVERIFY(pack.packFormat() == ShaderPackFormat::INVALID); QVERIFY(valid == false); } }; QTEST_GUILESS_MAIN(ShaderPackParseTest) #include "ShaderPackParse_test.moc" PrismLauncher-11.0.3/CMakeLists.txt0000644000175100017510000005556115224505336016537 0ustar runnerrunnercmake_minimum_required(VERSION 3.25) # Required for features like `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT` project(Launcher LANGUAGES C CXX) if(APPLE) enable_language(OBJC OBJCXX) endif() string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD) if(IS_IN_SOURCE_BUILD) message(FATAL_ERROR "You are building the Launcher in-source. Please separate the build tree from the source tree.") endif() ##################################### Set CMake options ##################################### set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/") # Output all executables and shared libs in the main build folder, not in subfolders. set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) if(UNIX) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) endif() set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/jars) ######## Set compiler flags ######## set(CMAKE_CXX_STANDARD_REQUIRED true) set(CMAKE_C_STANDARD_REQUIRED true) set(CMAKE_CXX_STANDARD 20) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) add_compile_definitions($<$>:QT_NO_DEBUG>) add_compile_definitions(QT_WARN_DEPRECATED_UP_TO=0x060400) add_compile_definitions(QT_DISABLE_DEPRECATED_UP_TO=0x060400) if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") add_compile_options( # /GS Adds buffer security checks, default on but included anyway to mirror gcc's fstack-protector flag "$<$:/GS>" # /Gw helps reduce binary size # /Gy allows the compiler to package individual functions # /guard:cf enables control flow guard "$<$,$>:/Gw;/Gy;/guard:cf>" ) add_link_options( # /LTCG allows for linking wholy optimizated programs # /MANIFEST:NO disables generating a manifest file, we instead provide our own # /STACK sets the stack reserve size, ATL's pack list needs 3-4 MiB as of November 2022, provide 8 MiB "$<$:/LTCG;/MANIFEST:NO;/STACK:8388608>" ) # /GL enables whole program optimizations # NOTE: With Clang, this is implemented as regular LTO and only used during linking if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") add_compile_options("$<$,$>:/GL>") endif() # See https://github.com/ccache/ccache/issues/1040 # TODO(@getchoo): Is sccache affected by this? Would be nice to use `ProgramDatabase`.... set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>") if(CMAKE_MSVC_RUNTIME_LIBRARY STREQUAL "MultiThreadedDLL") set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release "") set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release "") endif() else() add_compile_options("$<$:-fstack-protector-strong;--param=ssp-buffer-size=4>") # Avoid re-defining _FORTIFY_SOURCE, as it can cause redefinition errors in setups that use it by default (i.e., package builds) if(NOT (CMAKE_C_FLAGS MATCHES "-D_FORTIFY_SOURCE" OR CMAKE_CXX_FLAGS MATCHES "-D_FORTIFY_SOURCE")) # NOTE: _FORTIFY_SOURCE requires optimizations in most newer versions of glibc add_compile_options("$<$,$>:-D_FORTIFY_SOURCE=2>") endif() # ATL's pack list needs more than the default 1 Mib stack on windows if(CMAKE_SYSTEM_NAME STREQUAL "Windows") add_link_options("$<$:-Wl,--stack,8388608>") # -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 add_compile_options("$<$,$>:-ffunction-sections;-fdata-sections;-mguard=cf>") endif() endif() # Export compile commands for debug builds if we can (useful in LSPs like clangd) # https://cmake.org/cmake/help/v3.31/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR MATCHES "^Ninja") set(CMAKE_EXPORT_COMPILE_COMMANDS ON) endif() option(USE_CLANG_TIDY "Enable the use of clang-tidy during compilation" OFF) if(USE_CLANG_TIDY) find_program(CLANG_TIDY clang-tidy OPTIONAL) if(CLANG_TIDY) message(STATUS "Using clang-tidy during compilation") set(CLANG_TIDY_COMMAND "${CLANG_TIDY}" "--config-file=${CMAKE_SOURCE_DIR}/.clang-tidy") set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_COMMAND}) else() message(WARNING "Unable to find `clang-tidy`. Not using during compilation") endif() endif() option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF) # If this is a Debug build turn on address sanitiser if (DEBUG_ADDRESS_SANITIZER) message(STATUS "Address Sanitizer enabled for Debug builds, Turn it off with -DDEBUG_ADDRESS_SANITIZER=off") set(USE_ASAN_COMPILE_OPTIONS $,$>) if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") message(STATUS "Using Address Sanitizer compile options for MSVC frontend") add_compile_options( $<${USE_ASAN_COMPILE_OPTIONS}:/fsanitize=address> $<${USE_ASAN_COMPILE_OPTIONS}:/Oy-> ) elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") message(STATUS "Using Address Sanitizer compile options for GCC/Clang") add_compile_options( $<${USE_ASAN_COMPILE_OPTIONS}:-fsanitize=address,undefined> $<${USE_ASAN_COMPILE_OPTIONS}:-fno-omit-frame-pointer> $<${USE_ASAN_COMPILE_OPTIONS}:-fno-sanitize-recover=null> ) link_libraries("asan" "ubsan") else() message(STATUS "Address Sanitizer not available on compiler ${CMAKE_CXX_COMPILER_ID}") endif() endif() option(ENABLE_LTO "Enable Link Time Optimization" off) if(ENABLE_LTO) include(CheckIPOSupported) check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error) if(ipo_supported) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_MINSIZEREL TRUE) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO TRUE) if(CMAKE_BUILD_TYPE) if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") message(STATUS "IPO / LTO enabled") else() message(STATUS "Not enabling IPO / LTO on debug builds") endif() else() message(STATUS "IPO / LTO will only be enabled for release builds") endif() else() message(STATUS "IPO / LTO not supported: <${ipo_error}>") endif() endif() option(BUILD_TESTING "Build the testing tree." ON) find_package(ECM NO_MODULE REQUIRED) set(CMAKE_MODULE_PATH "${ECM_MODULE_PATH};${CMAKE_MODULE_PATH}") include(CTest) include(ECMAddTests) if(BUILD_TESTING) enable_testing() endif() ##################################### Set Application options ##################################### ######## Set URLs ######## set(Launcher_NEWS_RSS_URL "https://prismlauncher.org/feed/feed.xml" CACHE STRING "URL to fetch Prism Launcher's news RSS feed from.") set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL that gets opened when the user clicks 'More News'") set(Launcher_WIKI_URL "https://prismlauncher.org/wiki/" CACHE STRING "URL that gets opened when the user clicks 'Launcher Help'") set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help in a dialog window") set(Launcher_LOGIN_CALLBACK_URL "https://prismlauncher.org/successful-login" CACHE STRING "URL that gets opened when the user successfully logins.") set(Launcher_LEGACY_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for legacy (<=1.5.2) FML Libraries.") ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 11) set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_PATCH 3) set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}") set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.0") set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_PATCH},0") # Build platform. set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") # Github repo URL with releases for updater set(Launcher_UPDATER_GITHUB_REPO "https://github.com/PrismLauncher/PrismLauncher" CACHE STRING "Base github URL for the updater.") # Name to help updater identify valid artifacts set(Launcher_BUILD_ARTIFACT "" CACHE STRING "Artifact name to help the updater identify valid artifacts.") # The metadata server set(Launcher_META_URL "https://meta.prismlauncher.org/v1/" CACHE STRING "URL to fetch Launcher's meta files from.") # Imgur API Client ID set(Launcher_IMGUR_CLIENT_ID "5b97b0713fba4a3" CACHE STRING "Client ID you can get from Imgur when you register an application") # Bug tracker URL set(Launcher_BUG_TRACKER_URL "https://github.com/PrismLauncher/PrismLauncher/issues" CACHE STRING "URL for the bug tracker.") # Translations Platform URL set(Launcher_TRANSLATIONS_URL "https://hosted.weblate.org/projects/prismlauncher/launcher/" CACHE STRING "URL for the translations platform.") set(Launcher_TRANSLATION_FILES_URL "https://i18n.prismlauncher.org/" CACHE STRING "URL for the translations files.") # Matrix Space set(Launcher_MATRIX_URL "https://prismlauncher.org/matrix" CACHE STRING "URL to the Matrix Space") # Discord URL set(Launcher_DISCORD_URL "https://prismlauncher.org/discord" CACHE STRING "URL for the Discord guild.") # Subreddit URL set(Launcher_SUBREDDIT_URL "https://prismlauncher.org/reddit" CACHE STRING "URL for the subreddit.") # Builds set(Launcher_QT_VERSION_MAJOR "6" CACHE STRING "Major Qt version to build against") option(Launcher_USE_PCH "Use precompiled headers where possible" ON) # Java downloader set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT ON) # Although we recommend enabling this, we cannot guarantee binary compatibility on # differing Linux/BSD/etc distributions. Downstream packagers should be explicitly opt-ing into this # feature if they know it will work with their distribution. if(UNIX AND NOT APPLE) set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) endif() # Java downloader option(Launcher_ENABLE_JAVA_DOWNLOADER "Build the java downloader feature" ${Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT}) # Native libraries if(UNIX AND APPLE) set(Launcher_GLFW_LIBRARY_NAME "libglfw.dylib" CACHE STRING "Name of native glfw library") set(Launcher_OPENAL_LIBRARY_NAME "libopenal.dylib" CACHE STRING "Name of native openal library") elseif(UNIX) set(Launcher_GLFW_LIBRARY_NAME "libglfw.so" CACHE STRING "Name of native glfw library") set(Launcher_OPENAL_LIBRARY_NAME "libopenal.so" CACHE STRING "Name of native openal library") elseif(WIN32) set(Launcher_GLFW_LIBRARY_NAME "glfw.dll" CACHE STRING "Name of native glfw library") set(Launcher_OPENAL_LIBRARY_NAME "OpenAL.dll" CACHE STRING "Name of native openal library") endif() # API Keys # NOTE: These API keys are here for convenience. If you rebrand this software or intend to break the terms of service # of these platforms, please change these API keys beforehand. # Be aware that if you were to use these API keys for malicious purposes they might get revoked, which might cause # breakage to thousands of users. # If you don't plan to use these features of this software, you can just remove these values. # By using this key in your builds you accept the terms of use laid down in # https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use set(Launcher_MSA_CLIENT_ID "c36a9fb6-4f2a-41ff-90bd-ae7cc92031eb" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application") # By using this key in your builds you accept the terms and conditions laid down in # https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions # NOTE: CurseForge requires you to change this if you make any kind of derivative work. # This key was issued specifically for Prism Launcher set(Launcher_CURSEFORGE_API_KEY "$2a$10$wuAJuNZuted3NORVmpgUC.m8sI.pv1tOPKZyBgLFGjxFp/br0lZCC" CACHE STRING "API key for the CurseForge platform") set(Launcher_COMPILER_NAME ${CMAKE_CXX_COMPILER_ID}) set(Launcher_COMPILER_VERSION ${CMAKE_CXX_COMPILER_VERSION}) set(Launcher_COMPILER_TARGET_SYSTEM ${CMAKE_SYSTEM_NAME}) set(Launcher_COMPILER_TARGET_SYSTEM_VERSION ${CMAKE_SYSTEM_VERSION}) set(Launcher_COMPILER_TARGET_PROCESSOR ${CMAKE_SYSTEM_PROCESSOR}) #### Check the current Git commit and branch include(GetGitRevisionDescription) git_get_exact_tag(Launcher_GIT_TAG) get_git_head_revision(Launcher_GIT_REFSPEC Launcher_GIT_COMMIT) message(STATUS "Git commit: ${Launcher_GIT_COMMIT}") message(STATUS "Git tag: ${Launcher_GIT_TAG}") message(STATUS "Git refspec: ${Launcher_GIT_REFSPEC}") string(TIMESTAMP TODAY "%Y-%m-%d") set(Launcher_BUILD_TIMESTAMP "${TODAY}") ################################ 3rd Party Libs ################################ # Find the required Qt parts if(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_VERSION_MAJOR 6) find_package(Qt6 6.4 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml NetworkAuth OpenGL) find_package(Qt6 COMPONENTS DBus) list(APPEND Launcher_QT_DBUS Qt6::DBus) else() message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported") endif() if(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_PLUGINS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_PLUGINS}) set(QT_LIBS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBS}) set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS}) endif() find_package(cmark REQUIRED) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") find_package(PkgConfig REQUIRED) pkg_check_modules(gamemode REQUIRED IMPORTED_TARGET gamemode) endif() # Find libqrencode ## NOTE(@getchoo): Never use pkg-config with MSVC since the vcpkg port makes our install bundle fail to find the dll if(MSVC) find_path(LIBQRENCODE_INCLUDE_DIR qrencode.h REQUIRED) find_library(LIBQRENCODE_LIBRARY_RELEASE qrencode REQUIRED) find_library(LIBQRENCODE_LIBRARY_DEBUG qrencoded) set(LIBQRENCODE_LIBRARIES optimized ${LIBQRENCODE_LIBRARY_RELEASE} debug ${LIBQRENCODE_LIBRARY_DEBUG}) else() find_package(PkgConfig REQUIRED) pkg_check_modules(libqrencode REQUIRED IMPORTED_TARGET libqrencode) endif() # Find libarchive through CMake packages, mainly for vcpkg find_package(LibArchive) # CMake packages aren't available in most distributions of libarchive, so fallback to pkg-config if(NOT LibArchive_FOUND) find_package(PkgConfig REQUIRED) pkg_check_modules(libarchive REQUIRED IMPORTED_TARGET libarchive) endif() find_package(tomlplusplus 3.2.0) # fallback to pkgconfig, important especially as many distros package toml++ built with meson if(NOT tomlplusplus_FOUND) find_package(PkgConfig REQUIRED) pkg_check_modules(tomlplusplus REQUIRED IMPORTED_TARGET tomlplusplus>=3.2.0) endif() find_package(ZLIB REQUIRED) include(ECMQtDeclareLoggingCategory) ####################################### Program Info ####################################### set(Launcher_APP_BINARY_NAME "prismlauncher" CACHE STRING "Name of the Launcher binary") add_subdirectory(program_info) ####################################### Install layout ####################################### set(Launcher_ENABLE_UPDATER NO) set(Launcher_BUILD_UPDATER NO) if (NOT APPLE AND (NOT Launcher_UPDATER_GITHUB_REPO STREQUAL "" AND NOT Launcher_BUILD_ARTIFACT STREQUAL "")) set(Launcher_BUILD_UPDATER YES) endif() if(NOT (UNIX AND APPLE)) # Install "portable.txt" if selected component is "portable" install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Portable_File}" DESTINATION "." COMPONENT portable EXCLUDE_FROM_ALL) endif() if(UNIX AND APPLE) set(BINARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") set(LIBRARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") set(PLUGIN_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") set(FRAMEWORK_DEST_DIR "${Launcher_Name}.app/Contents/Frameworks") set(RESOURCES_DEST_DIR "${Launcher_Name}.app/Contents/Resources") set(JARS_DEST_DIR "${Launcher_Name}.app/Contents/MacOS/jars") # Mac bundle settings set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_DisplayName}") set(MACOSX_BUNDLE_INFO_STRING "${Launcher_DisplayName}: A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.") set(MACOSX_BUNDLE_GUI_IDENTIFIER "${Launcher_AppID}") set(MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) set(MACOSX_BUNDLE_COPYRIGHT "${Launcher_Copyright_Mac}") set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed") set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed") set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.8.0/Sparkle-2.8.0.tar.xz" CACHE STRING "URL to Sparkle release archive") set(MACOSX_SPARKLE_SHA256 "fd5681ee92bf238aaac2d08214ceaf0cc8976e452d7f882d80bac1e61581f3b1" CACHE STRING "SHA256 checksum for Sparkle release archive") set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle") if(NOT MACOSX_SPARKLE_UPDATE_PUBLIC_KEY STREQUAL "" AND NOT MACOSX_SPARKLE_UPDATE_FEED_URL STREQUAL "") set(Launcher_ENABLE_UPDATER YES) endif() # Add the icon install(FILES ${Launcher_Branding_ICNS} DESTINATION ${RESOURCES_DEST_DIR} RENAME ${Launcher_Name}.icns) find_program(ACTOOL_EXE actool DOC "Path to the apple asset catalog compiler") if(ACTOOL_EXE) execute_process( COMMAND xcodebuild -version OUTPUT_VARIABLE XCODE_VERSION_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE ) string(REGEX MATCH "Xcode ([0-9]+\.[0-9]+)" XCODE_VERSION_MATCH "${XCODE_VERSION_OUTPUT}") if(XCODE_VERSION_MATCH) set(XCODE_VERSION ${CMAKE_MATCH_1}) else() set(XCODE_VERSION 0.0) endif() if(XCODE_VERSION VERSION_GREATER_EQUAL 26.0) set(ASSETS_OUT_DIR "${CMAKE_BINARY_DIR}/program_info") set(GENERATED_ASSETS_CAR "${ASSETS_OUT_DIR}/Assets.car") set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Branding_MAC_ICON}") add_custom_command( OUTPUT "${GENERATED_ASSETS_CAR}" COMMAND ${ACTOOL_EXE} "${ICON_SOURCE}" --compile "${ASSETS_OUT_DIR}" --output-partial-info-plist /dev/null --app-icon ${Launcher_Name} --enable-on-demand-resources NO --target-device mac --minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET} --platform macosx DEPENDS "${ICON_SOURCE}" COMMENT "Compiling asset catalog (${ICON_SOURCE})" VERBATIM ) add_custom_target(compile_assets ALL DEPENDS "${GENERATED_ASSETS_CAR}") install(FILES "${GENERATED_ASSETS_CAR}" DESTINATION "${RESOURCES_DEST_DIR}") else() message(WARNING "Xcode ${XCODE_VERSION} is too old. Minimum required version is 26.0. Not compiling liquid glass icons.") endif() else() message(WARNING "actool not found. Cannot compile macOS app icons.\n" "Install Xcode command line tools: 'xcode-select --install'") endif() elseif(UNIX) include(KDEInstallDirs) set(BINARY_DEST_DIR "bin") set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}") set(JARS_DEST_DIR "share/${Launcher_Name}") # Set RPATH SET(Launcher_BINARY_RPATH "$ORIGIN/") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps") install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_PNG_256} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/256x256/apps" RENAME "${Launcher_AppID}.png") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}") set(PLUGIN_DEST_DIR "plugins") set(BUNDLE_DEST_DIR ".") set(RESOURCES_DEST_DIR ".") if(Launcher_ManPage) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6") endif() # Install basic runner script if component "portable" is selected configure_file(launcher/Launcher.in "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" @ONLY) install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" DESTINATION "." RENAME ${Launcher_Name} COMPONENT portable EXCLUDE_FROM_ALL) elseif(WIN32) set(BINARY_DEST_DIR ".") set(LIBRARY_DEST_DIR ".") set(PLUGIN_DEST_DIR ".") set(RESOURCES_DEST_DIR ".") set(JARS_DEST_DIR "jars") else() message(FATAL_ERROR "Platform not supported") endif() ################################ Included Libs ################################ include(ExternalProject) set_directory_properties(PROPERTIES EP_BASE External) option(NBT_BUILD_SHARED "Build NBT shared library" OFF) option(NBT_USE_ZLIB "Build NBT library with zlib support" OFF) option(NBT_BUILD_TESTS "Build NBT library tests" OFF) #FIXME: fix unit tests. add_subdirectory(libraries/libnbtplusplus) add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/javacheck) # java compatibility checker add_subdirectory(libraries/rainbow) # Qt extension for colors add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API add_subdirectory(libraries/qdcss) # css parser ############################### Built Artifacts ############################### add_subdirectory(buildconfig) if(BUILD_TESTING) add_subdirectory(tests) endif() # NOTE: this must always be last to appease the CMake deity of quirky install command evaluation order. add_subdirectory(launcher) PrismLauncher-11.0.3/COPYING.md0000644000175100017510000004754615224505336015435 0ustar runnerrunner## Prism Launcher Prism Launcher - Minecraft Launcher Copyright (C) 2022-2026 Prism Launcher Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . This file incorporates work covered by the following copyright and permission notice: 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. ## PolyMC PolyMC - Minecraft Launcher Copyright (C) 2021-2022 PolyMC Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . This file incorporates work covered by the following copyright and permission notice: 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. ## MinGW-w64 runtime (Windows) Copyright (c) 2009, 2010, 2011, 2012, 2013 by the mingw-w64 project This license has been certified as open source. It has also been designated as GPL compatible by the Free Software Foundation (FSF). Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions in source code must retain the accompanying copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the accompanying copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Names of the copyright holders must not be used to endorse or promote products derived from this software without prior written permission from the copyright holders. 4. The right to distribute this software or to use it for any purpose does not give you the right to use Servicemarks (sm) or Trademarks (tm) of the copyright holders. Use of them is covered by separate agreement with the copyright holders. 5. If any files are modified, you must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. Disclaimer THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Information on third party licenses used in MinGW-w64 can be found in its COPYING.MinGW-w64-runtime.txt. ## Qt 6 Copyright (C) 2022 The Qt Company Ltd and other contributors. Contact: https://www.qt.io/licensing Licensed under LGPL v3 ## libnbt++ libnbt++ - A library for the Minecraft Named Binary Tag format. Copyright (C) 2013, 2015 ljfa-ag libnbt++ is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. libnbt++ is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with libnbt++. If not, see . ## rainbow (KGuiAddons) Copyright (C) 2007 Matthew Woehlke Copyright (C) 2007 Olaf Schmidt Copyright (C) 2007 Thomas Zander Copyright (C) 2007 Zack Rusin Copyright (C) 2015 Petr Mrazek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ## cmark Copyright (c) 2014, John MacFarlane All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Batch icon set You are free to use Batch (the "icon set") or any part thereof (the "icons") in any personal, open-source or commercial work without obligation of payment (monetary or otherwise) or attribution. Do not sell the icon set, host the icon set or rent the icon set (either in existing or modified form). While attribution is optional, it is always appreciated. Intellectual property rights are not transferred with the download of the icons. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL ADAM WHITCROFT BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THE USE OF THE ICONS, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. ## Material Design Icons Copyright (c) 2014, Austin Andrews (http://materialdesignicons.com/), with Reserved Font Name Material Design Icons. Copyright (c) 2014, Google (http://www.google.com/design/) uses the license at https://github.com/google/material-design-icons/blob/master/LICENSE This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ## launcher (`libraries/launcher`) PolyMC - Minecraft Launcher Copyright (C) 2021-2022 PolyMC Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obliged to do so. If you do not wish to do so, delete this exception statement from your version. You should have received a copy of the GNU General Public License along with this program. If not, see . This file incorporates work covered by the following copyright and permission notice: 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. ## lionshead Code has been taken from https://github.com/natefoo/lionshead and loosely translated to C++ laced with Qt. MIT License Copyright (c) 2017 Nate Coraor Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## tomlplusplus MIT License Copyright (c) Mark Gillard Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## Gamemode Copyright (c) 2017-2022, Feral Interactive All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Feral Interactive nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Breeze icons Copyright (C) 2014 Uri Herrera and others This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . ## Oxygen Icons The Oxygen Icon Theme Copyright (C) 2007 Nuno Pinheiro Copyright (C) 2007 David Vignoni Copyright (C) 2007 David Miller Copyright (C) 2007 Johann Ollivier Lapeyre Copyright (C) 2007 Kenneth Wimer Copyright (C) 2007 Riccardo Iaconelli and others This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . ## libqrencode (`fukuchi/libqrencode`) Copyright (C) 2020 libqrencode Authors This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA ## vcpkg (`cmake/vcpkg-ports`) MIT License Copyright (c) Microsoft Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PrismLauncher-11.0.3/cmake/0000755000175100017510000000000015224505336015043 5ustar runnerrunnerPrismLauncher-11.0.3/cmake/GetGitRevisionDescription.cmake0000644000175100017510000002245715224505336023165 0ustar runnerrunner# - Returns a version string from Git # # These functions force a re-configure on each git commit so that you can # trust the values of the variables in your build system. # # get_git_head_revision( [ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR]) # # Returns the refspec and sha hash of the current head revision # # git_describe( [ ...]) # # Returns the results of git describe on the source tree, and adjusting # the output so that it tests false if an error occurs. # # git_describe_working_tree( [ ...]) # # Returns the results of git describe on the working tree (--dirty option), # and adjusting the output so that it tests false if an error occurs. # # git_get_exact_tag( [ ...]) # # Returns the results of git describe --exact-match on the source tree, # and adjusting the output so that it tests false if there was no exact # matching tag. # # git_local_changes() # # Returns either "CLEAN" or "DIRTY" with respect to uncommitted changes. # Uses the return code of "git diff-index --quiet HEAD --". # Does not regard untracked files. # # Requires CMake 2.6 or newer (uses the 'function' command) # # Original Author: # 2009-2020 Ryan Pavlik # http://academic.cleardefinition.com # # Copyright 2009-2013, Iowa State University. # Copyright 2013-2020, Ryan Pavlik # Copyright 2013-2020, Contributors # SPDX-License-Identifier: BSL-1.0 # Distributed under the Boost Software License, Version 1.0. # (See accompanying file LICENSE_1_0.txt or copy at # http://www.boost.org/LICENSE_1_0.txt) if(__get_git_revision_description) return() endif() set(__get_git_revision_description YES) # We must run the following at "include" time, not at function call time, # to find the path to this module rather than the path to a calling list file get_filename_component(_gitdescmoddir ${CMAKE_CURRENT_LIST_FILE} PATH) # Function _git_find_closest_git_dir finds the next closest .git directory # that is part of any directory in the path defined by _start_dir. # The result is returned in the parent scope variable whose name is passed # as variable _git_dir_var. If no .git directory can be found, the # function returns an empty string via _git_dir_var. # # Example: Given a path C:/bla/foo/bar and assuming C:/bla/.git exists and # neither foo nor bar contain a file/directory .git. This wil return # C:/bla/.git # function(_git_find_closest_git_dir _start_dir _git_dir_var) set(cur_dir "${_start_dir}") set(git_dir "${_start_dir}/.git") while(NOT EXISTS "${git_dir}") # .git dir not found, search parent directories set(git_previous_parent "${cur_dir}") get_filename_component(cur_dir "${cur_dir}" DIRECTORY) if(cur_dir STREQUAL git_previous_parent) # We have reached the root directory, we are not in git set(${_git_dir_var} "" PARENT_SCOPE) return() endif() set(git_dir "${cur_dir}/.git") endwhile() set(${_git_dir_var} "${git_dir}" PARENT_SCOPE) endfunction() function(get_git_head_revision _refspecvar _hashvar) _git_find_closest_git_dir("${CMAKE_CURRENT_SOURCE_DIR}" GIT_DIR) if("${ARGN}" STREQUAL "ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR") set(ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR TRUE) else() set(ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR FALSE) endif() if(NOT "${GIT_DIR}" STREQUAL "") file(RELATIVE_PATH _relative_to_source_dir "${CMAKE_SOURCE_DIR}" "${GIT_DIR}") if("${_relative_to_source_dir}" MATCHES "[.][.]" AND NOT ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR) # We've gone above the CMake root dir. set(GIT_DIR "") endif() endif() if("${GIT_DIR}" STREQUAL "") set(${_refspecvar} "GITDIR-NOTFOUND" PARENT_SCOPE) set(${_hashvar} "GITDIR-NOTFOUND" PARENT_SCOPE) return() endif() # Check if the current source dir is a git submodule or a worktree. # In both cases .git is a file instead of a directory. # if(NOT IS_DIRECTORY ${GIT_DIR}) # The following git command will return a non empty string that # points to the super project working tree if the current # source dir is inside a git submodule. # Otherwise the command will return an empty string. # execute_process( COMMAND "${GIT_EXECUTABLE}" rev-parse --show-superproject-working-tree WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" OUTPUT_VARIABLE out ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) if(NOT "${out}" STREQUAL "") # If out is empty, GIT_DIR/CMAKE_CURRENT_SOURCE_DIR is in a submodule file(READ ${GIT_DIR} submodule) string(REGEX REPLACE "gitdir: (.*)$" "\\1" GIT_DIR_RELATIVE ${submodule}) string(STRIP ${GIT_DIR_RELATIVE} GIT_DIR_RELATIVE) get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH) get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} ABSOLUTE) set(HEAD_SOURCE_FILE "${GIT_DIR}/HEAD") else() # GIT_DIR/CMAKE_CURRENT_SOURCE_DIR is in a worktree file(READ ${GIT_DIR} worktree_ref) # The .git directory contains a path to the worktree information directory # inside the parent git repo of the worktree. # string(REGEX REPLACE "gitdir: (.*)$" "\\1" git_worktree_dir ${worktree_ref}) string(STRIP ${git_worktree_dir} git_worktree_dir) _git_find_closest_git_dir("${git_worktree_dir}" GIT_DIR) set(HEAD_SOURCE_FILE "${git_worktree_dir}/HEAD") endif() else() set(HEAD_SOURCE_FILE "${GIT_DIR}/HEAD") endif() set(GIT_DATA "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/git-data") if(NOT EXISTS "${GIT_DATA}") file(MAKE_DIRECTORY "${GIT_DATA}") endif() if(NOT EXISTS "${HEAD_SOURCE_FILE}") return() endif() set(HEAD_FILE "${GIT_DATA}/HEAD") configure_file("${HEAD_SOURCE_FILE}" "${HEAD_FILE}" COPYONLY) configure_file("${_gitdescmoddir}/GetGitRevisionDescription.cmake.in" "${GIT_DATA}/grabRef.cmake" @ONLY) include("${GIT_DATA}/grabRef.cmake") set(${_refspecvar} "${HEAD_REF}" PARENT_SCOPE) set(${_hashvar} "${HEAD_HASH}" PARENT_SCOPE) endfunction() function(git_describe _var) if(NOT GIT_FOUND) find_package(Git QUIET) endif() get_git_head_revision(refspec hash) if(NOT GIT_FOUND) set(${_var} "GIT-NOTFOUND" PARENT_SCOPE) return() endif() if(NOT hash) set(${_var} "HEAD-HASH-NOTFOUND" PARENT_SCOPE) return() endif() # TODO sanitize #if((${ARGN}" MATCHES "&&") OR # (ARGN MATCHES "||") OR # (ARGN MATCHES "\\;")) # message("Please report the following error to the project!") # message(FATAL_ERROR "Looks like someone's doing something nefarious with git_describe! Passed arguments ${ARGN}") #endif() #message(STATUS "Arguments to execute_process: ${ARGN}") execute_process( COMMAND "${GIT_EXECUTABLE}" describe --tags --always ${hash} ${ARGN} WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" RESULT_VARIABLE res OUTPUT_VARIABLE out ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) if(NOT res EQUAL 0) set(out "${out}-${res}-NOTFOUND") endif() set(${_var} "${out}" PARENT_SCOPE) endfunction() function(git_describe_working_tree _var) if(NOT GIT_FOUND) find_package(Git QUIET) endif() if(NOT GIT_FOUND) set(${_var} "GIT-NOTFOUND" PARENT_SCOPE) return() endif() execute_process( COMMAND "${GIT_EXECUTABLE}" describe --dirty ${ARGN} WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" RESULT_VARIABLE res OUTPUT_VARIABLE out ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) if(NOT res EQUAL 0) set(out "${out}-${res}-NOTFOUND") endif() set(${_var} "${out}" PARENT_SCOPE) endfunction() function(git_get_exact_tag _var) git_describe(out --exact-match ${ARGN}) set(${_var} "${out}" PARENT_SCOPE) endfunction() function(git_local_changes _var) if(NOT GIT_FOUND) find_package(Git QUIET) endif() get_git_head_revision(refspec hash) if(NOT GIT_FOUND) set(${_var} "GIT-NOTFOUND" PARENT_SCOPE) return() endif() if(NOT hash) set(${_var} "HEAD-HASH-NOTFOUND" PARENT_SCOPE) return() endif() execute_process( COMMAND "${GIT_EXECUTABLE}" diff-index --quiet HEAD -- WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" RESULT_VARIABLE res OUTPUT_VARIABLE out ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) if(res EQUAL 0) set(${_var} "CLEAN" PARENT_SCOPE) else() set(${_var} "DIRTY" PARENT_SCOPE) endif() endfunction() PrismLauncher-11.0.3/cmake/GitFunctions.cmake0000644000175100017510000000211415224505336020457 0ustar runnerrunnerif(__GITFUNCTIONS_CMAKE__) return() endif() set(__GITFUNCTIONS_CMAKE__ TRUE) find_package(Git QUIET) include(CMakeParseArguments) if(GIT_FOUND) function(git_run) set(oneValueArgs OUTPUT_VAR DEFAULT) set(multiValueArgs COMMAND) cmake_parse_arguments(GIT_RUN "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) execute_process(COMMAND ${GIT_EXECUTABLE} ${GIT_RUN_COMMAND} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} RESULT_VARIABLE GIT_RESULTVAR OUTPUT_VARIABLE GIT_OUTVAR OUTPUT_STRIP_TRAILING_WHITESPACE ) if(GIT_RESULTVAR EQUAL 0) set(${GIT_RUN_OUTPUT_VAR} "${GIT_OUTVAR}" PARENT_SCOPE) else() set(${GIT_RUN_OUTPUT_VAR} ${GIT_RUN_DEFAULT}) message(STATUS "Failed to run Git: ${GIT_OUTVAR}") endif() endfunction() else() function(git_run) set(oneValueArgs OUTPUT_VAR DEFAULT) set(multiValueArgs COMMAND) cmake_parse_arguments(GIT_RUN "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) set(${GIT_RUN_OUTPUT_VAR} ${GIT_RUN_DEFAULT}) endfunction(git_run) endif() PrismLauncher-11.0.3/cmake/vcpkg-ports/0000755000175100017510000000000015224505336017322 5ustar runnerrunnerPrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/0000755000175100017510000000000015224505336022526 5ustar runnerrunnerPrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake0000644000175100017510000000616715224505336027603 0ustar runnerrunnerfunction(vcpkg_install_meson) cmake_parse_arguments(PARSE_ARGV 0 arg "ADD_BIN_TO_PATH" "" "") vcpkg_find_acquire_program(NINJA) unset(ENV{DESTDIR}) # installation directory was already specified with '--prefix' option if(VCPKG_TARGET_IS_OSX) vcpkg_backup_env_variables(VARS SDKROOT MACOSX_DEPLOYMENT_TARGET) set(ENV{SDKROOT} "${VCPKG_DETECTED_CMAKE_OSX_SYSROOT}") set(ENV{MACOSX_DEPLOYMENT_TARGET} "${VCPKG_DETECTED_CMAKE_OSX_DEPLOYMENT_TARGET}") endif() foreach(buildtype IN ITEMS "debug" "release") if(DEFINED VCPKG_BUILD_TYPE AND NOT VCPKG_BUILD_TYPE STREQUAL buildtype) continue() endif() if(buildtype STREQUAL "debug") set(short_buildtype "dbg") else() set(short_buildtype "rel") endif() message(STATUS "Package ${TARGET_TRIPLET}-${short_buildtype}") if(arg_ADD_BIN_TO_PATH) vcpkg_backup_env_variables(VARS PATH) if(buildtype STREQUAL "debug") vcpkg_add_to_path(PREPEND "${CURRENT_INSTALLED_DIR}/debug/bin") else() vcpkg_add_to_path(PREPEND "${CURRENT_INSTALLED_DIR}/bin") endif() endif() vcpkg_execute_required_process( COMMAND "${NINJA}" install -v WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${short_buildtype}" LOGNAME package-${TARGET_TRIPLET}-${short_buildtype} ) if(arg_ADD_BIN_TO_PATH) vcpkg_restore_env_variables(VARS PATH) endif() endforeach() vcpkg_list(SET renamed_libs) if(VCPKG_TARGET_IS_WINDOWS AND VCPKG_LIBRARY_LINKAGE STREQUAL static AND NOT VCPKG_TARGET_IS_MINGW) # Meson names all static libraries lib.a which basically breaks the world file(GLOB_RECURSE gen_libraries "${CURRENT_PACKAGES_DIR}*/**/lib*.a") foreach(gen_library IN LISTS gen_libraries) get_filename_component(libdir "${gen_library}" DIRECTORY) get_filename_component(libname "${gen_library}" NAME) string(REGEX REPLACE ".a$" ".lib" fixed_librawname "${libname}") string(REGEX REPLACE "^lib" "" fixed_librawname "${fixed_librawname}") file(RENAME "${gen_library}" "${libdir}/${fixed_librawname}") # For cmake fixes. string(REGEX REPLACE ".a$" "" origin_librawname "${libname}") string(REGEX REPLACE ".lib$" "" fixed_librawname "${fixed_librawname}") vcpkg_list(APPEND renamed_libs ${fixed_librawname}) set(${librawname}_old ${origin_librawname}) set(${librawname}_new ${fixed_librawname}) endforeach() file(GLOB_RECURSE cmake_files "${CURRENT_PACKAGES_DIR}*/*.cmake") foreach(cmake_file IN LISTS cmake_files) foreach(current_lib IN LISTS renamed_libs) vcpkg_replace_string("${cmake_file}" "${${current_lib}_old}" "${${current_lib}_new}" IGNORE_UNCHANGED) endforeach() endforeach() endif() if(VCPKG_TARGET_IS_OSX) vcpkg_restore_env_variables(VARS SDKROOT MACOSX_DEPLOYMENT_TARGET) endif() endfunction() PrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake0000644000175100017510000000474315224505336027077 0ustar runnerrunnerinclude("${CURRENT_HOST_INSTALLED_DIR}/share/vcpkg-cmake-get-vars/vcpkg-port-config.cmake") # Overwrite builtin scripts include("${CMAKE_CURRENT_LIST_DIR}/vcpkg_configure_meson.cmake") include("${CMAKE_CURRENT_LIST_DIR}/vcpkg_install_meson.cmake") set(meson_short_hash @MESON_SHORT_HASH@) # Setup meson: set(program MESON) set(program_version @VERSION@) set(program_name meson) set(search_names meson meson.py) set(ref "${program_version}") set(path_to_search "${DOWNLOADS}/tools/meson-${program_version}-${meson_short_hash}") set(download_urls "https://github.com/mesonbuild/meson/archive/${ref}.tar.gz") set(download_filename "meson-${ref}.tar.gz") set(download_sha512 bd2e65f0863d9cb974e659ff502d773e937b8a60aaddfd7d81e34cd2c296c8e82bf214d790ac089ba441543059dfc2677ba95ed51f676df9da420859f404a907) find_program(SCRIPT_MESON NAMES ${search_names} PATHS "${path_to_search}" NO_DEFAULT_PATH) # NO_DEFAULT_PATH due top patching if(NOT SCRIPT_MESON) vcpkg_download_distfile(archive_path URLS ${download_urls} SHA512 "${download_sha512}" FILENAME "${download_filename}" ) file(REMOVE_RECURSE "${path_to_search}") file(REMOVE_RECURSE "${path_to_search}-tmp") file(MAKE_DIRECTORY "${path_to_search}-tmp") file(ARCHIVE_EXTRACT INPUT "${archive_path}" DESTINATION "${path_to_search}-tmp" #PATTERNS "**/mesonbuild/*" "**/*.py" ) z_vcpkg_apply_patches( SOURCE_PATH "${path_to_search}-tmp/meson-${ref}" PATCHES @PATCHES@ ) file(MAKE_DIRECTORY "${path_to_search}") file(RENAME "${path_to_search}-tmp/meson-${ref}/meson.py" "${path_to_search}/meson.py") file(RENAME "${path_to_search}-tmp/meson-${ref}/mesonbuild" "${path_to_search}/mesonbuild") file(REMOVE_RECURSE "${path_to_search}-tmp") set(SCRIPT_MESON "${path_to_search}/meson.py") endif() # Check required python version vcpkg_find_acquire_program(PYTHON3) vcpkg_execute_in_download_mode( COMMAND "${PYTHON3}" --version OUTPUT_VARIABLE version_contents WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}" ) string(REGEX MATCH [[[0-9]+\.[0-9]+\.[0-9]+]] python_ver "${version_contents}") set(min_required 3.7) if(python_ver VERSION_LESS "${min_required}") message(FATAL_ERROR "Found Python version '${python_ver} at ${PYTHON3}' is insufficient for meson. meson requires at least version '${min_required}'") else() message(STATUS "Found Python version '${python_ver} at ${PYTHON3}'") endif() message(STATUS "Using meson: ${SCRIPT_MESON}") PrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake0000644000175100017510000005202415224505336030107 0ustar runnerrunnerfunction(z_vcpkg_meson_set_proglist_variables config_type) if(VCPKG_TARGET_IS_WINDOWS) set(proglist MT AR) else() set(proglist AR RANLIB STRIP NM OBJDUMP DLLTOOL MT) endif() foreach(prog IN LISTS proglist) if(VCPKG_DETECTED_CMAKE_${prog}) if(meson_${prog}) string(TOUPPER "MESON_${meson_${prog}}" var_to_set) set("${var_to_set}" "${meson_${prog}} = ['${VCPKG_DETECTED_CMAKE_${prog}}']" PARENT_SCOPE) elseif(${prog} STREQUAL AR AND VCPKG_COMBINED_STATIC_LINKER_FLAGS_${config_type}) # Probably need to move AR somewhere else string(TOLOWER "${prog}" proglower) z_vcpkg_meson_convert_compiler_flags_to_list(ar_flags "${VCPKG_COMBINED_STATIC_LINKER_FLAGS_${config_type}}") list(PREPEND ar_flags "${VCPKG_DETECTED_CMAKE_${prog}}") z_vcpkg_meson_convert_list_to_python_array(ar_flags ${ar_flags}) set("MESON_AR" "${proglower} = ${ar_flags}" PARENT_SCOPE) else() string(TOUPPER "MESON_${prog}" var_to_set) string(TOLOWER "${prog}" proglower) set("${var_to_set}" "${proglower} = ['${VCPKG_DETECTED_CMAKE_${prog}}']" PARENT_SCOPE) endif() endif() endforeach() set(compilers "${arg_LANGUAGES}") if(VCPKG_TARGET_IS_WINDOWS) list(APPEND compilers RC) endif() set(meson_RC windres) set(meson_Fortran fortran) set(meson_CXX cpp) foreach(prog IN LISTS compilers) if(VCPKG_DETECTED_CMAKE_${prog}_COMPILER) string(TOUPPER "MESON_${prog}" var_to_set) if(meson_${prog}) if(VCPKG_COMBINED_${prog}_FLAGS_${config_type}) # Need compiler flags in prog vars for sanity check. z_vcpkg_meson_convert_compiler_flags_to_list(${prog}flags "${VCPKG_COMBINED_${prog}_FLAGS_${config_type}}") endif() list(PREPEND ${prog}flags "${VCPKG_DETECTED_CMAKE_${prog}_COMPILER}") list(FILTER ${prog}flags EXCLUDE REGEX "(-|/)nologo") # Breaks compiler detection otherwise z_vcpkg_meson_convert_list_to_python_array(${prog}flags ${${prog}flags}) set("${var_to_set}" "${meson_${prog}} = ${${prog}flags}" PARENT_SCOPE) if (DEFINED VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID AND NOT VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID MATCHES "^(GNU|Intel)$" AND VCPKG_DETECTED_CMAKE_LINKER) string(TOUPPER "MESON_${prog}_LD" var_to_set) set(${var_to_set} "${meson_${prog}}_ld = ['${VCPKG_DETECTED_CMAKE_LINKER}']" PARENT_SCOPE) endif() else() if(VCPKG_COMBINED_${prog}_FLAGS_${config_type}) # Need compiler flags in prog vars for sanity check. z_vcpkg_meson_convert_compiler_flags_to_list(${prog}flags "${VCPKG_COMBINED_${prog}_FLAGS_${config_type}}") endif() list(PREPEND ${prog}flags "${VCPKG_DETECTED_CMAKE_${prog}_COMPILER}") list(FILTER ${prog}flags EXCLUDE REGEX "(-|/)nologo") # Breaks compiler detection otherwise z_vcpkg_meson_convert_list_to_python_array(${prog}flags ${${prog}flags}) string(TOLOWER "${prog}" proglower) set("${var_to_set}" "${proglower} = ${${prog}flags}" PARENT_SCOPE) if (DEFINED VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID AND NOT VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID MATCHES "^(GNU|Intel)$" AND VCPKG_DETECTED_CMAKE_LINKER) string(TOUPPER "MESON_${prog}_LD" var_to_set) set(${var_to_set} "${proglower}_ld = ['${VCPKG_DETECTED_CMAKE_LINKER}']" PARENT_SCOPE) endif() endif() endif() endforeach() endfunction() function(z_vcpkg_meson_convert_compiler_flags_to_list out_var compiler_flags) separate_arguments(cmake_list NATIVE_COMMAND "${compiler_flags}") list(TRANSFORM cmake_list REPLACE ";" [[\\;]]) set("${out_var}" "${cmake_list}" PARENT_SCOPE) endfunction() function(z_vcpkg_meson_convert_list_to_python_array out_var) z_vcpkg_function_arguments(flag_list 1) vcpkg_list(REMOVE_ITEM flag_list "") # remove empty elements if any vcpkg_list(JOIN flag_list "', '" flag_list) set("${out_var}" "['${flag_list}']" PARENT_SCOPE) endfunction() # Generates the required compiler properties for meson function(z_vcpkg_meson_set_flags_variables config_type) if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) set(libpath_flag /LIBPATH:) else() set(libpath_flag -L) endif() if(config_type STREQUAL "DEBUG") set(path_suffix "/debug") else() set(path_suffix "") endif() set(includepath "-I${CURRENT_INSTALLED_DIR}/include") set(libpath "${libpath_flag}${CURRENT_INSTALLED_DIR}${path_suffix}/lib") foreach(lang IN LISTS arg_LANGUAGES) z_vcpkg_meson_convert_compiler_flags_to_list(${lang}flags "${VCPKG_COMBINED_${lang}_FLAGS_${config_type}}") if(lang MATCHES "^(C|CXX)$") vcpkg_list(APPEND ${lang}flags "${includepath}") endif() z_vcpkg_meson_convert_list_to_python_array(${lang}flags ${${lang}flags}) set(lang_mapping "${lang}") if(lang STREQUAL "Fortran") set(lang_mapping "FC") endif() string(TOLOWER "${lang_mapping}" langlower) if(lang STREQUAL "CXX") set(langlower cpp) endif() set(MESON_${lang_mapping}FLAGS "${langlower}_args = ${${lang}flags}\n") set(linker_flags "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_${config_type}}") z_vcpkg_meson_convert_compiler_flags_to_list(linker_flags "${linker_flags}") vcpkg_list(APPEND linker_flags "${libpath}") z_vcpkg_meson_convert_list_to_python_array(linker_flags ${linker_flags}) string(APPEND MESON_${lang_mapping}FLAGS "${langlower}_link_args = ${linker_flags}\n") set(MESON_${lang_mapping}FLAGS "${MESON_${lang_mapping}FLAGS}" PARENT_SCOPE) endforeach() endfunction() function(z_vcpkg_get_build_and_host_system build_system host_system is_cross) #https://mesonbuild.com/Cross-compilation.html set(build_unknown FALSE) if(CMAKE_HOST_WIN32) if(DEFINED ENV{PROCESSOR_ARCHITEW6432}) set(build_arch $ENV{PROCESSOR_ARCHITEW6432}) else() set(build_arch $ENV{PROCESSOR_ARCHITECTURE}) endif() if(build_arch MATCHES "(amd|AMD)64") set(build_cpu_fam x86_64) set(build_cpu x86_64) elseif(build_arch MATCHES "(x|X)86") set(build_cpu_fam x86) set(build_cpu i686) elseif(build_arch MATCHES "^(ARM|arm)64$") set(build_cpu_fam aarch64) set(build_cpu armv8) elseif(build_arch MATCHES "^(ARM|arm)$") set(build_cpu_fam arm) set(build_cpu armv7hl) else() if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) message(WARNING "Unsupported build architecture ${build_arch}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") endif() set(build_unknown TRUE) endif() elseif(CMAKE_HOST_UNIX) # at this stage, CMAKE_HOST_SYSTEM_PROCESSOR is not defined execute_process( COMMAND uname -m OUTPUT_VARIABLE MACHINE OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY) # Show real machine architecture to visually understand whether we are in a native Apple Silicon terminal or running under Rosetta emulation debug_message("Machine: ${MACHINE}") if(MACHINE MATCHES "arm64|aarch64") set(build_cpu_fam aarch64) set(build_cpu armv8) elseif(MACHINE MATCHES "armv7h?l") set(build_cpu_fam arm) set(build_cpu ${MACHINE}) elseif(MACHINE MATCHES "x86_64|amd64") set(build_cpu_fam x86_64) set(build_cpu x86_64) elseif(MACHINE MATCHES "x86|i686") set(build_cpu_fam x86) set(build_cpu i686) elseif(MACHINE MATCHES "i386") set(build_cpu_fam x86) set(build_cpu i386) elseif(MACHINE MATCHES "loongarch64") set(build_cpu_fam loongarch64) set(build_cpu loongarch64) else() # https://github.com/mesonbuild/meson/blob/master/docs/markdown/Reference-tables.md#cpu-families if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) message(WARNING "Unhandled machine: ${MACHINE}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") endif() set(build_unknown TRUE) endif() else() if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) message(WARNING "Failed to detect the build architecture! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") endif() set(build_unknown TRUE) endif() set(build "[build_machine]\n") # Machine the build is performed on string(APPEND build "endian = 'little'\n") if(CMAKE_HOST_WIN32) string(APPEND build "system = 'windows'\n") elseif(CMAKE_HOST_APPLE) string(APPEND build "system = 'darwin'\n") elseif(CYGWIN) string(APPEND build "system = 'cygwin'\n") elseif(CMAKE_HOST_UNIX) string(APPEND build "system = 'linux'\n") else() set(build_unknown TRUE) endif() if(DEFINED build_cpu_fam) string(APPEND build "cpu_family = '${build_cpu_fam}'\n") endif() if(DEFINED build_cpu) string(APPEND build "cpu = '${build_cpu}'") endif() if(NOT build_unknown) set(${build_system} "${build}" PARENT_SCOPE) endif() set(host_unkown FALSE) if(VCPKG_TARGET_ARCHITECTURE MATCHES "(amd|AMD|x|X)64") set(host_cpu_fam x86_64) set(host_cpu x86_64) elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "(x|X)86") set(host_cpu_fam x86) set(host_cpu i686) elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "^(ARM|arm)64$") set(host_cpu_fam aarch64) set(host_cpu armv8) elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "^(ARM|arm)$") set(host_cpu_fam arm) set(host_cpu armv7hl) elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "loongarch64") set(host_cpu_fam loongarch64) set(host_cpu loongarch64) elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "wasm32") set(host_cpu_fam wasm32) set(host_cpu wasm32) else() if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) message(WARNING "Unsupported target architecture ${VCPKG_TARGET_ARCHITECTURE}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the host_machine entry!" ) endif() set(host_unkown TRUE) endif() set(host "[host_machine]\n") # host=target in vcpkg. string(APPEND host "endian = 'little'\n") if(NOT VCPKG_CMAKE_SYSTEM_NAME OR VCPKG_TARGET_IS_MINGW OR VCPKG_TARGET_IS_UWP) set(meson_system_name "windows") else() string(TOLOWER "${VCPKG_CMAKE_SYSTEM_NAME}" meson_system_name) endif() string(APPEND host "system = '${meson_system_name}'\n") string(APPEND host "cpu_family = '${host_cpu_fam}'\n") string(APPEND host "cpu = '${host_cpu}'") if(NOT host_unkown) set(${host_system} "${host}" PARENT_SCOPE) endif() if(NOT build_cpu_fam MATCHES "${host_cpu_fam}" OR VCPKG_TARGET_IS_ANDROID OR VCPKG_TARGET_IS_IOS OR VCPKG_TARGET_IS_UWP OR (VCPKG_TARGET_IS_MINGW AND NOT CMAKE_HOST_WIN32)) set(${is_cross} TRUE PARENT_SCOPE) endif() endfunction() function(z_vcpkg_meson_setup_extra_windows_variables config_type) ## b_vscrt if(VCPKG_CRT_LINKAGE STREQUAL "static") set(crt_type "mt") else() set(crt_type "md") endif() if(config_type STREQUAL "DEBUG") set(crt_type "${crt_type}d") endif() set(MESON_VSCRT_LINKAGE "b_vscrt = '${crt_type}'" PARENT_SCOPE) ## winlibs separate_arguments(c_winlibs NATIVE_COMMAND "${VCPKG_DETECTED_CMAKE_C_STANDARD_LIBRARIES}") separate_arguments(cpp_winlibs NATIVE_COMMAND "${VCPKG_DETECTED_CMAKE_CXX_STANDARD_LIBRARIES}") z_vcpkg_meson_convert_list_to_python_array(c_winlibs ${c_winlibs}) z_vcpkg_meson_convert_list_to_python_array(cpp_winlibs ${cpp_winlibs}) set(MESON_WINLIBS "c_winlibs = ${c_winlibs}\n") string(APPEND MESON_WINLIBS "cpp_winlibs = ${cpp_winlibs}") set(MESON_WINLIBS "${MESON_WINLIBS}" PARENT_SCOPE) endfunction() function(z_vcpkg_meson_setup_variables config_type) set(meson_var_list VSCRT_LINKAGE WINLIBS MT AR RC C C_LD CXX CXX_LD OBJC OBJC_LD OBJCXX OBJCXX_LD FC FC_LD WINDRES CFLAGS CXXFLAGS OBJCFLAGS OBJCXXFLAGS FCFLAGS SHARED_LINKER_FLAGS) foreach(var IN LISTS meson_var_list) set(MESON_${var} "") endforeach() if(VCPKG_TARGET_IS_WINDOWS) z_vcpkg_meson_setup_extra_windows_variables("${config_type}") endif() z_vcpkg_meson_set_proglist_variables("${config_type}") z_vcpkg_meson_set_flags_variables("${config_type}") foreach(var IN LISTS meson_var_list) set(MESON_${var} "${MESON_${var}}" PARENT_SCOPE) endforeach() endfunction() function(vcpkg_generate_meson_cmd_args) cmake_parse_arguments(PARSE_ARGV 0 arg "" "OUTPUT;CONFIG" "OPTIONS;LANGUAGES;ADDITIONAL_BINARIES;ADDITIONAL_PROPERTIES" ) if(NOT arg_LANGUAGES) set(arg_LANGUAGES C CXX) endif() vcpkg_list(JOIN arg_ADDITIONAL_BINARIES "\n" MESON_ADDITIONAL_BINARIES) vcpkg_list(JOIN arg_ADDITIONAL_PROPERTIES "\n" MESON_ADDITIONAL_PROPERTIES) set(buildtype "${arg_CONFIG}") if(NOT VCPKG_CHAINLOAD_TOOLCHAIN_FILE) z_vcpkg_select_default_vcpkg_chainload_toolchain() endif() vcpkg_list(APPEND VCPKG_CMAKE_CONFIGURE_OPTIONS "-DVCPKG_LANGUAGES=${arg_LANGUAGES}") vcpkg_cmake_get_vars(cmake_vars_file) debug_message("Including cmake vars from: ${cmake_vars_file}") include("${cmake_vars_file}") vcpkg_list(APPEND arg_OPTIONS --backend ninja --wrap-mode nodownload -Doptimization=plain) z_vcpkg_get_build_and_host_system(MESON_HOST_MACHINE MESON_BUILD_MACHINE IS_CROSS) if(arg_CONFIG STREQUAL "DEBUG") set(suffix "dbg") else() string(SUBSTRING "${arg_CONFIG}" 0 3 suffix) string(TOLOWER "${suffix}" suffix) endif() set(meson_input_file_${buildtype} "${CURRENT_BUILDTREES_DIR}/meson-${TARGET_TRIPLET}-${suffix}.log") if(IS_CROSS) # VCPKG_CROSSCOMPILING is not used since it regresses a lot of ports in x64-windows-x triplets # For consistency this should proably be changed in the future? vcpkg_list(APPEND arg_OPTIONS --native "${SCRIPTS}/buildsystems/meson/none.txt") vcpkg_list(APPEND arg_OPTIONS --cross "${meson_input_file_${buildtype}}") else() vcpkg_list(APPEND arg_OPTIONS --native "${meson_input_file_${buildtype}}") endif() # User provided cross/native files if(VCPKG_MESON_NATIVE_FILE) vcpkg_list(APPEND arg_OPTIONS --native "${VCPKG_MESON_NATIVE_FILE}") endif() if(VCPKG_MESON_NATIVE_FILE_${buildtype}) vcpkg_list(APPEND arg_OPTIONS --native "${VCPKG_MESON_NATIVE_FILE_${buildtype}}") endif() if(VCPKG_MESON_CROSS_FILE) vcpkg_list(APPEND arg_OPTIONS --cross "${VCPKG_MESON_CROSS_FILE}") endif() if(VCPKG_MESON_CROSS_FILE_${buildtype}) vcpkg_list(APPEND arg_OPTIONS --cross "${VCPKG_MESON_CROSS_FILE_${buildtype}}") endif() vcpkg_list(APPEND arg_OPTIONS --libdir lib) # else meson install into an architecture describing folder vcpkg_list(APPEND arg_OPTIONS --pkgconfig.relocatable) if(arg_CONFIG STREQUAL "RELEASE") vcpkg_list(APPEND arg_OPTIONS -Ddebug=false --prefix "${CURRENT_PACKAGES_DIR}") vcpkg_list(APPEND arg_OPTIONS "--pkg-config-path;['${CURRENT_INSTALLED_DIR}/lib/pkgconfig','${CURRENT_INSTALLED_DIR}/share/pkgconfig']") if(VCPKG_TARGET_IS_WINDOWS) vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}/share']") else() vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/debug']") endif() elseif(arg_CONFIG STREQUAL "DEBUG") vcpkg_list(APPEND arg_OPTIONS -Ddebug=true --prefix "${CURRENT_PACKAGES_DIR}/debug" --includedir ../include) vcpkg_list(APPEND arg_OPTIONS "--pkg-config-path;['${CURRENT_INSTALLED_DIR}/debug/lib/pkgconfig','${CURRENT_INSTALLED_DIR}/share/pkgconfig']") if(VCPKG_TARGET_IS_WINDOWS) vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/share']") else() vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}']") endif() else() message(FATAL_ERROR "Unknown configuration. Only DEBUG and RELEASE are valid values.") endif() # Allow overrides / additional configuration variables from triplets if(DEFINED VCPKG_MESON_CONFIGURE_OPTIONS) vcpkg_list(APPEND arg_OPTIONS ${VCPKG_MESON_CONFIGURE_OPTIONS}) endif() if(DEFINED VCPKG_MESON_CONFIGURE_OPTIONS_${buildtype}) vcpkg_list(APPEND arg_OPTIONS ${VCPKG_MESON_CONFIGURE_OPTIONS_${buildtype}}) endif() if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic") set(MESON_DEFAULT_LIBRARY shared) else() set(MESON_DEFAULT_LIBRARY static) endif() set(MESON_CMAKE_BUILD_TYPE "${cmake_build_type_${buildtype}}") z_vcpkg_meson_setup_variables(${buildtype}) configure_file("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/meson.template.in" "${meson_input_file_${buildtype}}" @ONLY) set("${arg_OUTPUT}" ${arg_OPTIONS} PARENT_SCOPE) endfunction() function(vcpkg_configure_meson) # parse parameters such that semicolons in options arguments to COMMAND don't get erased cmake_parse_arguments(PARSE_ARGV 0 arg "NO_PKG_CONFIG" "SOURCE_PATH" "OPTIONS;OPTIONS_DEBUG;OPTIONS_RELEASE;LANGUAGES;ADDITIONAL_BINARIES;ADDITIONAL_NATIVE_BINARIES;ADDITIONAL_CROSS_BINARIES;ADDITIONAL_PROPERTIES" ) if(DEFINED arg_ADDITIONAL_NATIVE_BINARIES OR DEFINED arg_ADDITIONAL_CROSS_BINARIES) message(WARNING "Options ADDITIONAL_(NATIVE|CROSS)_BINARIES have been deprecated. Only use ADDITIONAL_BINARIES!") endif() vcpkg_list(APPEND arg_ADDITIONAL_BINARIES ${arg_ADDITIONAL_NATIVE_BINARIES} ${arg_ADDITIONAL_CROSS_BINARIES}) vcpkg_list(REMOVE_DUPLICATES arg_ADDITIONAL_BINARIES) file(REMOVE_RECURSE "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel") file(REMOVE_RECURSE "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg") vcpkg_find_acquire_program(MESON) get_filename_component(CMAKE_PATH "${CMAKE_COMMAND}" DIRECTORY) vcpkg_add_to_path("${CMAKE_PATH}") # Make CMake invokeable for Meson vcpkg_find_acquire_program(NINJA) if(NOT arg_NO_PKG_CONFIG) vcpkg_find_acquire_program(PKGCONFIG) set(ENV{PKG_CONFIG} "${PKGCONFIG}") endif() vcpkg_find_acquire_program(PYTHON3) get_filename_component(PYTHON3_DIR "${PYTHON3}" DIRECTORY) vcpkg_add_to_path(PREPEND "${PYTHON3_DIR}") set(buildtypes "") if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") set(buildname "DEBUG") set(cmake_build_type_${buildname} "Debug") vcpkg_list(APPEND buildtypes "${buildname}") set(path_suffix_${buildname} "debug/") set(suffix_${buildname} "dbg") endif() if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") set(buildname "RELEASE") set(cmake_build_type_${buildname} "Release") vcpkg_list(APPEND buildtypes "${buildname}") set(path_suffix_${buildname} "") set(suffix_${buildname} "rel") endif() # configure build foreach(buildtype IN LISTS buildtypes) message(STATUS "Configuring ${TARGET_TRIPLET}-${suffix_${buildtype}}") file(MAKE_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${suffix_${buildtype}}") vcpkg_generate_meson_cmd_args( OUTPUT cmd_args CONFIG ${buildtype} LANGUAGES ${arg_LANGUAGES} OPTIONS ${arg_OPTIONS} ${arg_OPTIONS_${buildtype}} ADDITIONAL_BINARIES ${arg_ADDITIONAL_BINARIES} ADDITIONAL_PROPERTIES ${arg_ADDITIONAL_PROPERTIES} ) vcpkg_execute_required_process( COMMAND ${MESON} setup ${cmd_args} ${arg_SOURCE_PATH} WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${suffix_${buildtype}}" LOGNAME config-${TARGET_TRIPLET}-${suffix_${buildtype}} SAVE_LOG_FILES meson-logs/meson-log.txt meson-info/intro-dependencies.json meson-logs/install-log.txt ) message(STATUS "Configuring ${TARGET_TRIPLET}-${suffix_${buildtype}} done") endforeach() endfunction() PrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch0000644000175100017510000000155215224505336026371 0ustar runnerrunnerdiff --git a/mesonbuild/compilers/detect.py b/mesonbuild/compilers/detect.py index f57957f0b..a72e72a0b 100644 --- a/mesonbuild/compilers/detect.py +++ b/mesonbuild/compilers/detect.py @@ -1472,6 +1472,11 @@ def _get_clang_compiler_defines(compiler: T.List[str], lang: str) -> T.Dict[str, """ from .mixins.clang import clang_lang_map + # Filter out `-arch` flags passed to the compiler for Universal Binaries + # https://github.com/mesonbuild/meson/issues/5290 + # https://github.com/mesonbuild/meson/issues/8206 + compiler = [arg for i, arg in enumerate(compiler) if not (i > 0 and compiler[i - 1] == "-arch") and not arg == "-arch"] + def _try_obtain_compiler_defines(args: T.List[str]) -> str: mlog.debug(f'Running command: {join_args(args)}') p, output, error = Popen_safe(compiler + args, write='', stdin=subprocess.PIPE) PrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch0000644000175100017510000000312615224505336032732 0ustar runnerrunnerdiff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py index cc0450a52..13501466d 100644 --- a/mesonbuild/modules/pkgconfig.py +++ b/mesonbuild/modules/pkgconfig.py @@ -701,16 +701,8 @@ class PkgConfigModule(NewExtensionModule): pcfile = filebase + '.pc' pkgroot = pkgroot_name = kwargs['install_dir'] or default_install_dir if pkgroot is None: - m = state.environment.machines.host - if m.is_freebsd(): - pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('prefix'))), 'libdata', 'pkgconfig') - pkgroot_name = os.path.join('{prefix}', 'libdata', 'pkgconfig') - elif m.is_haiku(): - pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('prefix'))), 'develop', 'lib', 'pkgconfig') - pkgroot_name = os.path.join('{prefix}', 'develop', 'lib', 'pkgconfig') - else: - pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('libdir'))), 'pkgconfig') - pkgroot_name = os.path.join('{libdir}', 'pkgconfig') + pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('libdir'))), 'pkgconfig') + pkgroot_name = os.path.join('{libdir}', 'pkgconfig') relocatable = state.get_option('pkgconfig.relocatable') self._generate_pkgconfig_file(state, deps, subdirs, name, description, url, version, pcfile, conflicts, variables, PrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake0000644000175100017510000000325415224505336025360 0ustar runnerrunner# This port represents a dependency on the Meson build system. # In the future, it is expected that this port acquires and installs Meson. # Currently is used in ports that call vcpkg_find_acquire_program(MESON) in order to force rebuilds. set(VCPKG_POLICY_CMAKE_HELPER_PORT enabled) set(patches meson-intl.patch adjust-python-dep.patch adjust-args.patch remove-freebsd-pcfile-specialization.patch fix-libcpp-enable-assertions.patch # https://github.com/mesonbuild/meson/pull/14548, Remove in 1.8.3 universal-osx.patch # NOTE(@getchoo): THIS IS THE ONLY CHANGE NEEDED FOR PRISM ) set(scripts vcpkg-port-config.cmake vcpkg_configure_meson.cmake vcpkg_install_meson.cmake meson.template.in ) set(to_hash "${CMAKE_CURRENT_LIST_DIR}/vcpkg.json" "${CMAKE_CURRENT_LIST_DIR}/portfile.cmake" ) foreach(file IN LISTS patches scripts) set(filepath "${CMAKE_CURRENT_LIST_DIR}/${file}") list(APPEND to_hash "${filepath}") file(COPY "${filepath}" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") endforeach() set(meson_path_hash "") foreach(filepath IN LISTS to_hash) file(SHA1 "${filepath}" to_append) string(APPEND meson_path_hash "${to_append}") endforeach() string(SHA512 meson_path_hash "${meson_path_hash}") string(SUBSTRING "${meson_path_hash}" 0 6 MESON_SHORT_HASH) list(TRANSFORM patches REPLACE [[^(..*)$]] [["${CMAKE_CURRENT_LIST_DIR}/\0"]]) list(JOIN patches "\n " PATCHES) configure_file("${CMAKE_CURRENT_LIST_DIR}/vcpkg-port-config.cmake" "${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-port-config.cmake" @ONLY) vcpkg_install_copyright(FILE_LIST "${VCPKG_ROOT_DIR}/LICENSE.txt") include("${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-port-config.cmake") PrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch0000644000175100017510000000553615224505336027137 0ustar runnerrunnerdiff --git a/mesonbuild/dependencies/python.py b/mesonbuild/dependencies/python.py index 883a29a..d9a82af 100644 --- a/mesonbuild/dependencies/python.py +++ b/mesonbuild/dependencies/python.py @@ -232,8 +232,10 @@ class _PythonDependencyBase(_Base): else: if self.is_freethreaded: libpath = Path('libs') / f'python{vernum}t.lib' + libpath = Path('libs') / f'..' / f'..' / f'..' / f'lib' / f'python{vernum}t.lib' else: libpath = Path('libs') / f'python{vernum}.lib' + libpath = Path('libs') / f'..' / f'..' / f'..' / f'lib' / f'python{vernum}.lib' # For a debug build, pyconfig.h may force linking with # pythonX_d.lib (see meson#10776). This cannot be avoided # and won't work unless we also have a debug build of @@ -250,6 +252,8 @@ class _PythonDependencyBase(_Base): vscrt = self.env.coredata.optstore.get_value('b_vscrt') if vscrt in {'mdd', 'mtd', 'from_buildtype', 'static_from_buildtype'}: vscrt_debug = True + if is_debug_build: + libpath = Path('libs') / f'..' / f'..' / f'..' / f'debug/lib' / f'python{vernum}_d.lib' if is_debug_build and vscrt_debug and not self.variables.get('Py_DEBUG'): mlog.warning(textwrap.dedent('''\ Using a debug build type with MSVC or an MSVC-compatible compiler @@ -350,9 +354,10 @@ class PythonSystemDependency(SystemDependency, _PythonDependencyBase): self.is_found = True # compile args + verdot = self.variables.get('py_version_short') inc_paths = mesonlib.OrderedSet([ self.variables.get('INCLUDEPY'), - self.paths.get('include'), + self.paths.get('include') + f'/../../../include/python${verdot}', self.paths.get('platinclude')]) self.compile_args += ['-I' + path for path in inc_paths if path] @@ -416,7 +421,7 @@ def python_factory(env: 'Environment', for_machine: 'MachineChoice', candidates.append(functools.partial(wrap_in_pythons_pc_dir, pkg_name, env, kwargs, installation)) # We only need to check both, if a python install has a LIBPC. It might point to the wrong location, # e.g. relocated / cross compilation, but the presence of LIBPC indicates we should definitely look for something. - if pkg_libdir is not None: + if True or pkg_libdir is not None: candidates.append(functools.partial(PythonPkgConfigDependency, pkg_name, env, kwargs, installation)) else: candidates.append(functools.partial(PkgConfigDependency, 'python3', env, kwargs)) PrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json0000644000175100017510000000037215224505336024535 0ustar runnerrunner{ "name": "vcpkg-tool-meson", "version": "1.8.2", "description": "Meson build system", "homepage": "https://github.com/mesonbuild/meson", "license": "Apache-2.0", "supports": "native", "dependencies": [ "vcpkg-cmake-get-vars" ] } PrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in0000644000175100017510000000172115224505336026012 0ustar runnerrunner[binaries] cmake = ['@CMAKE_COMMAND@'] ninja = ['@NINJA@'] pkg-config = ['@PKGCONFIG@'] @MESON_MT@ @MESON_AR@ @MESON_RC@ @MESON_C@ @MESON_C_LD@ @MESON_CXX@ @MESON_CXX_LD@ @MESON_OBJC@ @MESON_OBJC_LD@ @MESON_OBJCPP@ @MESON_OBJCPP_LD@ @MESON_FC@ @MESON_FC_LD@ @MESON_WINDRES@ @MESON_ADDITIONAL_BINARIES@ [properties] cmake_toolchain_file = '@SCRIPTS@/buildsystems/vcpkg.cmake' @MESON_ADDITIONAL_PROPERTIES@ [cmake] CMAKE_BUILD_TYPE = '@MESON_CMAKE_BUILD_TYPE@' VCPKG_TARGET_TRIPLET = '@TARGET_TRIPLET@' VCPKG_HOST_TRIPLET = '@_HOST_TRIPLET@' VCPKG_CHAINLOAD_TOOLCHAIN_FILE = '@VCPKG_CHAINLOAD_TOOLCHAIN_FILE@' VCPKG_CRT_LINKAGE = '@VCPKG_CRT_LINKAGE@' _VCPKG_INSTALLED_DIR = '@_VCPKG_INSTALLED_DIR@' @MESON_HOST_MACHINE@ @MESON_BUILD_MACHINE@ [built-in options] default_library = '@MESON_DEFAULT_LIBRARY@' werror = false @MESON_CFLAGS@ @MESON_CXXFLAGS@ @MESON_FCFLAGS@ @MESON_OBJCFLAGS@ @MESON_OBJCPPFLAGS@ # b_vscrt @MESON_VSCRT_LINKAGE@ # c_winlibs/cpp_winlibs @MESON_WINLIBS@PrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake0000644000175100017510000000030315224505336025172 0ustar runnerrunnerfile(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/tools/meson") file(INSTALL "${SOURCE_PATH}/meson.py" "${SOURCE_PATH}/mesonbuild" DESTINATION "${CURRENT_PACKAGES_DIR}/tools/meson" ) PrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch0000644000175100017510000000452615224505336031227 0ustar runnerrunnerFrom a16ec8b0fb6d7035b669a13edd4d97ff0c307a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20D=C3=B8rum?= Date: Fri, 2 May 2025 10:56:28 +0200 Subject: [PATCH] cpp: fix _LIBCPP_ENABLE_ASSERTIONS warning libc++ deprecated _LIBCPP_ENABLE_ASSERTIONS from version 18. However, the libc++ shipped with Apple Clang backported that deprecation in version 17 already, which is the version which Apple currently ships for macOS. This PR changes the _LIBCPP_ENABLE_ASSERTIONS deprecation check to use version ">=17" on Apple Clang. --- mesonbuild/compilers/cpp.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/mesonbuild/compilers/cpp.py b/mesonbuild/compilers/cpp.py index 01b9bb9fa34f..f7dc150e8608 100644 --- a/mesonbuild/compilers/cpp.py +++ b/mesonbuild/compilers/cpp.py @@ -311,6 +311,9 @@ def get_option_link_args(self, target: 'BuildTarget', env: 'Environment', subpro return libs return [] + def is_libcpp_enable_assertions_deprecated(self) -> bool: + return version_compare(self.version, ">=18") + def get_assert_args(self, disable: bool, env: 'Environment') -> T.List[str]: if disable: return ['-DNDEBUG'] @@ -323,7 +326,7 @@ def get_assert_args(self, disable: bool, env: 'Environment') -> T.List[str]: if self.language_stdlib_provider(env) == 'stdc++': return ['-D_GLIBCXX_ASSERTIONS=1'] else: - if version_compare(self.version, '>=18'): + if self.is_libcpp_enable_assertions_deprecated(): return ['-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST'] elif version_compare(self.version, '>=15'): return ['-D_LIBCPP_ENABLE_ASSERTIONS=1'] @@ -343,7 +346,12 @@ class ArmLtdClangCPPCompiler(ClangCPPCompiler): class AppleClangCPPCompiler(AppleCompilerMixin, AppleCPPStdsMixin, ClangCPPCompiler): - pass + def is_libcpp_enable_assertions_deprecated(self) -> bool: + # Upstream libc++ deprecated _LIBCPP_ENABLE_ASSERTIONS + # in favor of _LIBCPP_HARDENING_MODE from version 18 onwards, + # but Apple Clang 17's libc++ has back-ported that change. + # See: https://github.com/mesonbuild/meson/issues/14440 + return version_compare(self.version, ">=17") class EmscriptenCPPCompiler(EmscriptenMixin, ClangCPPCompiler): PrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/README.md0000644000175100017510000000026415224505336024007 0ustar runnerrunnerThe only difference between this and the upstream vcpkg port is the addition of `universal-osx.patch`. It's very annoying we need to bundle this entire tree to do that. -@getchoo PrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch0000644000175100017510000000102515224505336025632 0ustar runnerrunnerdiff --git a/mesonbuild/dependencies/misc.py b/mesonbuild/dependencies/misc.py --- a/mesonbuild/dependencies/misc.py +++ b/mesonbuild/dependencies/misc.py @@ -593,7 +593,8 @@ iconv_factory = DependencyFactory( packages['intl'] = intl_factory = DependencyFactory( 'intl', + [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM, DependencyMethods.CMAKE], + cmake_name='Intl', - [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM], builtin_class=IntlBuiltinDependency, system_class=IntlSystemDependency, ) PrismLauncher-11.0.3/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch0000644000175100017510000000117115224505336025773 0ustar runnerrunnerdiff --git a/mesonbuild/cmake/toolchain.py b/mesonbuild/cmake/toolchain.py index 11a00be5d..89ae490ff 100644 --- a/mesonbuild/cmake/toolchain.py +++ b/mesonbuild/cmake/toolchain.py @@ -202,7 +202,7 @@ class CMakeToolchain: @staticmethod def is_cmdline_option(compiler: 'Compiler', arg: str) -> bool: if compiler.get_argument_syntax() == 'msvc': - return arg.startswith('/') + return arg.startswith(('/','-')) else: if os.path.basename(compiler.get_exe()) == 'zig' and arg in {'ar', 'cc', 'c++', 'dlltool', 'lib', 'ranlib', 'objcopy', 'rc'}: return True PrismLauncher-11.0.3/cmake/GetGitRevisionDescription.cmake.in0000644000175100017510000000251215224505336023560 0ustar runnerrunner# # Internal file for GetGitRevisionDescription.cmake # # Requires CMake 2.6 or newer (uses the 'function' command) # # Original Author: # 2009-2010 Ryan Pavlik # http://academic.cleardefinition.com # Iowa State University HCI Graduate Program/VRAC # # Copyright 2009-2012, Iowa State University # Copyright 2011-2015, Contributors # Distributed under the Boost Software License, Version 1.0. # (See accompanying file LICENSE_1_0.txt or copy at # http://www.boost.org/LICENSE_1_0.txt) # SPDX-License-Identifier: BSL-1.0 set(HEAD_HASH) file(READ "@HEAD_FILE@" HEAD_CONTENTS LIMIT 1024) string(STRIP "${HEAD_CONTENTS}" HEAD_CONTENTS) if(HEAD_CONTENTS MATCHES "ref") # named branch string(REPLACE "ref: " "" HEAD_REF "${HEAD_CONTENTS}") if(EXISTS "@GIT_DIR@/${HEAD_REF}") configure_file("@GIT_DIR@/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY) else() configure_file("@GIT_DIR@/packed-refs" "@GIT_DATA@/packed-refs" COPYONLY) file(READ "@GIT_DATA@/packed-refs" PACKED_REFS) if(${PACKED_REFS} MATCHES "([0-9a-z]*) ${HEAD_REF}") set(HEAD_HASH "${CMAKE_MATCH_1}") endif() endif() else() # detached HEAD configure_file("@GIT_DIR@/HEAD" "@GIT_DATA@/head-ref" COPYONLY) endif() if(NOT HEAD_HASH) file(READ "@GIT_DATA@/head-ref" HEAD_HASH LIMIT 1024) string(STRIP "${HEAD_HASH}" HEAD_HASH) endif() PrismLauncher-11.0.3/cmake/vcpkg-triplets/0000755000175100017510000000000015224505336020021 5ustar runnerrunnerPrismLauncher-11.0.3/cmake/vcpkg-triplets/universal-osx.cmake0000644000175100017510000000045615224505336023647 0ustar runnerrunner# See https://github.com/microsoft/vcpkg/discussions/19454 # NOTE: Try to keep in sync with default arm64-osx definition set(VCPKG_TARGET_ARCHITECTURE x64) set(VCPKG_CRT_LINKAGE dynamic) set(VCPKG_LIBRARY_LINKAGE static) set(VCPKG_CMAKE_SYSTEM_NAME Darwin) set(VCPKG_OSX_ARCHITECTURES "arm64;x86_64") PrismLauncher-11.0.3/cmake/MacOSXBundleInfo.plist.in0000644000175100017510000000735215224505336021574 0ustar runnerrunner NSCameraUsageDescription A Minecraft mod wants to access your camera. NSMicrophoneUsageDescription A Minecraft mod wants to access your microphone. NSDownloadsFolderUsageDescription ${Launcher_DisplayName} uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where ${Launcher_DisplayName} scans for downloaded mods in Settings or the prompt that appears. NSLocalNetworkUsageDescription Minecraft uses the local network to find and connect to LAN servers. NSPrincipalClass NSApplication NSHighResolutionCapable True CFBundleDevelopmentRegion English CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} CFBundleGetInfoString ${MACOSX_BUNDLE_INFO_STRING} CFBundleIconFile ${Launcher_Name} CFBundleIconName ${Launcher_Name} CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleLongVersionString ${MACOSX_BUNDLE_LONG_VERSION_STRING} CFBundleName ${MACOSX_BUNDLE_BUNDLE_NAME} CFBundlePackageType APPL CFBundleShortVersionString ${MACOSX_BUNDLE_SHORT_VERSION_STRING} CFBundleSignature ???? CFBundleVersion ${MACOSX_BUNDLE_BUNDLE_VERSION} CSResourcesFileMapped LSRequiresCarbon LSApplicationCategoryType public.app-category.games NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} SUPublicEDKey ${MACOSX_SPARKLE_UPDATE_PUBLIC_KEY} SUFeedURL ${MACOSX_SPARKLE_UPDATE_FEED_URL} CFBundleDocumentTypes CFBundleTypeExtensions zip mrpack CFBundleTypeName ${Launcher_DisplayName} instance CFBundleTypeOSTypes TEXT utxt TUTX **** CFBundleTypeRole Viewer LSHandlerRank Alternate CFBundleURLTypes CFBundleURLName Curseforge CFBundleURLSchemes curseforge CFBundleURLName ${Launcher_Name} CFBundleURLSchemes prismlauncher ${MACOSX_BUNDLE_EXECUTABLE_NAME} PrismLauncher-11.0.3/.gitmodules0000644000175100017510000000020315224505336016133 0ustar runnerrunner[submodule "libraries/libnbtplusplus"] path = libraries/libnbtplusplus url = https://github.com/PrismLauncher/libnbtplusplus.git PrismLauncher-11.0.3/vcpkg.json0000644000175100017510000000066515224505336015777 0ustar runnerrunner{ "dependencies": [ { "name": "ecm", "host": true }, { "name": "libqrencode", "default-features": false }, { "name": "pkgconf", "host": true }, "cmark", { "name": "libarchive", "default-features": false, "features": [ "bzip2", "lz4", "lzma", "lzo", "zstd" ] }, "tomlplusplus", "zlib" ] } PrismLauncher-11.0.3/flake.lock0000644000175100017510000000214315224505336015717 0ustar runnerrunner{ "nodes": { "libnbtplusplus": { "flake": false, "locked": { "lastModified": 1772016279, "narHash": "sha256-7itkptyjoRcXfGLwg1/jxajetZ3a4mDc66+w4X6yW8s=", "owner": "PrismLauncher", "repo": "libnbtplusplus", "rev": "687e43031df0dc641984b4256bcca50d5b3f7de3", "type": "github" }, "original": { "owner": "PrismLauncher", "repo": "libnbtplusplus", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1774709303, "narHash": "sha256-D4ely1FsBcvtj/qSrNhSWpq+CUZKNiKwJIxpxnfy9o4=", "rev": "8110df5ad7abf5d4c0f6fb0f8f978390e77f9685", "type": "tarball", "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre971119.8110df5ad7ab/nixexprs.tar.xz" }, "original": { "type": "tarball", "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" } }, "root": { "inputs": { "libnbtplusplus": "libnbtplusplus", "nixpkgs": "nixpkgs" } } }, "root": "root", "version": 7 } PrismLauncher-11.0.3/shell.nix0000644000175100017510000000034215224505336015611 0ustar runnerrunner(import (fetchTarball { url = "https://github.com/edolstra/flake-compat/archive/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec.tar.gz"; sha256 = "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU="; }) { src = ./.; }).shellNix PrismLauncher-11.0.3/.editorconfig0000644000175100017510000000072615224505336016445 0ustar runnerrunner# EditorConfig specs and documentation: https://EditorConfig.org # top-most EditorConfig file root = true [*] indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true [*.{yml,nix}] indent_size = 2 # C++ Code Style settings [*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}] cpp_generate_documentation_comments = doxygen_slash_star [CMakeLists.txt] ij_continuation_indent_size = 4 PrismLauncher-11.0.3/LICENSE0000644000175100017510000010451515224505336014776 0ustar runnerrunner GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . PrismLauncher-11.0.3/CONTRIBUTING.md0000644000175100017510000001665415224505336016230 0ustar runnerrunner# Contributions Guidelines ## Restrictions on Generative AI Usage (AI Policy) > [!NOTE] > The following is adapted from [matplotlib's contributing guide](https://matplotlib.org/devdocs/devel/contribute.html#generative-ai) and the [Linux Kernel policy guide](https://www.kernel.org/doc./html/next/process/coding-assistants.html) We expect authentic engagement in our community. - Do not post output from Large Language Models or similar generative AI as comments on GitHub or our discord server, as such comments tend to be formulaic and low-quality content. - If you use generative AI tools as an aid in developing code or documentation changes, ensure that you fully understand the proposed changes and can explain why they are the correct approach. Make sure you have added value based on your personal competency to your contributions. Just taking some input, feeding it to an AI and posting the result is not of value to the project. To preserve precious core developer capacity, we reserve the right to rigorously reject seemingly AI generated low-value contributions. ### Signed-off-by and Developer Certificate of Origin AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer Certificate of Origin (DCO). The human submitter is responsible for: - Reviewing all AI-generated code - Ensuring compliance with licensing requirements - Adding their own Signed-off-by tag to certify the DCO - Taking full responsibility for the contribution See [Signing your work](#signing-your-work) for more information. ### Attribution When AI tools contribute to development, proper attribution helps track the evolving role of AI in the development process. Contributions should include an Assisted-by tag in the commit message with the following format: ```text Assisted-by: AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2] ``` Where: - `AGENT_NAME` is the name of the AI tool or framework - `MODEL_VERSION` is the specific model version used - `[TOOL1] [TOOL2]` are optional specialized analysis tools used (e.g., coccinelle, sparse, smatch, clang-tidy) Basic development tools (git, gcc, make, editors) should not be listed. Example: ```text Assisted-by: Claude:claude-3-opus coccinelle sparse ``` ## Code style All files are formatted with `clang-format` using the configuration in `.clang-format`. Ensure it is run on changed files before committing! Please also follow the project's conventions for C++: - Class and type names should be formatted as `PascalCase`: `MyClass`. - Private or protected class data members should be formatted as `camelCase` prefixed with `m_`: `m_myCounter`. - Private or protected `static` class data members should be formatted as `camelCase` prefixed with `s_`: `s_instance`. - Public class data members should be formatted as `camelCase` without the prefix: `dateOfBirth`. - Public, private or protected `static const` class data members should be formatted as `SCREAMING_SNAKE_CASE`: `MAX_VALUE`. - Class function members should be formatted as `camelCase` without a prefix: `incrementCounter`. - Global functions and non-`const` global variables should be formatted as `camelCase` without a prefix: `globalData`. - `const` global variables and macros should be formatted as `SCREAMING_SNAKE_CASE`: `LIGHT_GRAY`. - enum constants should be formatted as `PascalCase`: `CamelusBactrianus` - Avoid inventing acronyms or abbreviations especially for a name of multiple words - like `tp` for `texturePack`. - Avoid using `[[nodiscard]]` unless ignoring the return value is likely to cause a bug in cases such as: - A function allocates memory or another resource and the caller needs to clean it up. - A function has side effects and an error status is returned. - A function is likely be mistaken for having side effects. - A plain getter is unlikely to cause confusion and adding `[[nodiscard]]` can create clutter and inconsistency. Most of these rules are included in the `.clang-tidy` file, so you can run `clang-tidy` to check for any violations. Here is what these conventions with the formatting configuration look like: ```c++ #define AWESOMENESS 10 constexpr double PI = 3.14159; enum class PizzaToppings { HamAndPineapple, OreoAndKetchup }; struct Person { QString name; QDateTime dateOfBirth; long daysOld() const { return dateOfBirth.daysTo(QDateTime::currentDateTime()); } }; class ImportantClass { public: void incrementCounter() { if (m_counter + 1 > MAX_COUNTER_VALUE) throw std::runtime_error("Counter has reached limit!"); ++m_counter; } int counter() const { return m_counter; } private: static constexpr int MAX_COUNTER_VALUE = 100; int m_counter; }; ImportantClass importantClassInstance; ``` If you see any names which do not follow these conventions, it is preferred that you leave them be - renames increase the number of changes therefore make reviewing harder and make your PR more prone to conflicts. However, if you're refactoring a whole class anyway, it's fine. ## Signing your work In an effort to ensure that the code you contribute is actually compatible with the licenses in this codebase, we require you to sign-off all your contributions. This can be done by appending `-s` to your `git commit` call, or by manually appending the following text to your commit message: ```text Signed-off-by: Author name ``` By signing off your work, you agree to the terms below: ```text Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` These terms will be enforced once you create a pull request, and you will be informed automatically if any of your commits aren't signed-off by you. As a bonus, you can also [cryptographically sign your commits][gh-signing-commits] and enable [vigilant mode][gh-vigilant-mode] on GitHub. [gh-signing-commits]: https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits [gh-vigilant-mode]: https://docs.github.com/en/authentication/managing-commit-signature-verification/displaying-verification-statuses-for-all-of-your-commits ## Backporting to Release Branches We use [automated backports](https://github.com/PrismLauncher/PrismLauncher/blob/develop/.github/workflows/backport.yml) to merge specific contributions from develop into `release` branches. This is done when pull requests are merged and have labels such as `backport release-7.x` - which should be added along with the milestone for the release. PrismLauncher-11.0.3/.git-blame-ignore-revs0000644000175100017510000000043615224505336020066 0ustar runnerrunner# .git-blame-ignore-revs # tabs -> spaces bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9 # (nix) alejandra -> nixfmt 4c81d8c53d09196426568c4a31a4e752ed05397a # reformat codebase 1d468ac35ad88d8c77cc83f25e3704d9bd7df01b # format a part of codebase 5c8481a118c8fefbfe901001d7828eaf6866eac4 PrismLauncher-11.0.3/launcher/0000755000175100017510000000000015224505336015564 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/InstanceList.cpp0000644000175100017510000010255215224505336020675 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "InstanceList.h" #include #include #include #include #include #include #include #include #include #include #include #include "BaseInstance.h" #include "ExponentialSeries.h" #include "FileSystem.h" #include "InstanceTask.h" #include "NullInstance.h" #include "WatchLock.h" #include "minecraft/MinecraftInstance.h" #include "settings/INISettingsObject.h" #ifdef Q_OS_WIN32 #include #endif const static int GROUP_FILE_FORMAT_VERSION = 1; InstanceList::InstanceList(SettingsObject* settings, const QString& instDir, QObject* parent) : QAbstractListModel(parent), m_globalSettings(settings) { resumeWatch(); // Create aand normalize path if (!QDir::current().exists(instDir)) { QDir::current().mkpath(instDir); } connect(this, &InstanceList::instancesChanged, this, &InstanceList::providerUpdated); // NOTE: canonicalPath requires the path to exist. Do not move this above the creation block! m_instDir = QDir(instDir).canonicalPath(); m_watcher = new QFileSystemWatcher(this); connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &InstanceList::instanceDirContentsChanged); m_watcher->addPath(m_instDir); } InstanceList::~InstanceList() {} Qt::DropActions InstanceList::supportedDragActions() const { return Qt::MoveAction; } Qt::DropActions InstanceList::supportedDropActions() const { return Qt::MoveAction; } bool InstanceList::canDropMimeData(const QMimeData* data, [[maybe_unused]] Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex& parent) const { if (data && data->hasFormat("application/x-instanceid")) { return true; } return false; } bool InstanceList::dropMimeData(const QMimeData* data, [[maybe_unused]] Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex& parent) { if (data && data->hasFormat("application/x-instanceid")) { return true; } return false; } QStringList InstanceList::mimeTypes() const { auto types = QAbstractListModel::mimeTypes(); types.push_back("application/x-instanceid"); return types; } QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const { auto mimeData = QAbstractListModel::mimeData(indexes); if (indexes.size() == 1) { auto instanceId = data(indexes[0], InstanceIDRole).toString(); mimeData->setData("application/x-instanceid", instanceId.toUtf8()); } return mimeData; } QStringList InstanceList::getLinkedInstancesById(const QString& id) const { QStringList linkedInstances; for (auto& inst : m_instances) { if (inst->isLinkedToInstanceId(id)) linkedInstances.append(inst->id()); } return linkedInstances; } int InstanceList::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return count(); } QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) const { Q_UNUSED(parent); if (row < 0 || row >= count()) return QModelIndex(); return createIndex(row, column, m_instances.at(row).get()); } QVariant InstanceList::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } BaseInstance* pdata = static_cast(index.internalPointer()); switch (role) { case InstancePointerRole: { QVariant v = QVariant::fromValue((void*)pdata); return v; } case InstanceIDRole: { return pdata->id(); } case Qt::EditRole: case Qt::DisplayRole: { return pdata->name(); } case Qt::AccessibleTextRole: { return tr("%1 Instance").arg(pdata->name()); } case Qt::ToolTipRole: { return pdata->instanceRoot(); } case Qt::DecorationRole: { return pdata->iconKey(); } // HACK: see InstanceView.h in gui! case GroupRole: { return getInstanceGroup(pdata->id()); } default: break; } return QVariant(); } bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role) { if (!index.isValid()) { return false; } if (role != Qt::EditRole) { return false; } BaseInstance* pdata = static_cast(index.internalPointer()); auto newName = value.toString(); if (pdata->name() == newName) { return true; } pdata->setName(newName); return true; } Qt::ItemFlags InstanceList::flags(const QModelIndex& index) const { Qt::ItemFlags f; if (index.isValid()) { f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); } return f; } GroupId InstanceList::getInstanceGroup(const InstanceId& id) const { auto inst = getInstanceById(id); if (!inst) { return GroupId(); } auto iter = m_instanceGroupIndex.find(inst->id()); if (iter != m_instanceGroupIndex.end()) { return *iter; } return GroupId(); } void InstanceList::setInstanceGroup(const InstanceId& id, GroupId name) { if (name.isEmpty() && !name.isNull()) name = QString(); auto inst = getInstanceById(id); if (!inst) { qDebug() << "Attempt to set a null instance's group"; return; } bool changed = false; auto iter = m_instanceGroupIndex.find(inst->id()); if (iter != m_instanceGroupIndex.end()) { if (*iter != name) { decreaseGroupCount(*iter); *iter = name; changed = true; } } else { changed = true; m_instanceGroupIndex[id] = name; } if (changed) { increaseGroupCount(name); auto idx = getInstIndex(inst); emit dataChanged(index(idx), index(idx), { GroupRole }); saveGroupList(); } } QStringList InstanceList::getGroups() { return m_groupNameCache.keys(); } void InstanceList::deleteGroup(const GroupId& name) { m_groupNameCache.remove(name); m_collapsedGroups.remove(name); bool removed = false; qDebug() << "Delete group" << name; for (auto& instance : m_instances) { const QString& instID = instance->id(); const QString instGroupName = getInstanceGroup(instID); if (instGroupName == name) { m_instanceGroupIndex.remove(instID); qDebug() << "Remove" << instID << "from group" << name; removed = true; auto idx = getInstIndex(instance.get()); if (idx >= 0) emit dataChanged(index(idx), index(idx), { GroupRole }); } } if (removed) saveGroupList(); } void InstanceList::renameGroup(const QString& src, const QString& dst) { m_groupNameCache.remove(src); if (m_collapsedGroups.remove(src)) m_collapsedGroups.insert(dst); bool modified = false; qDebug() << "Rename group" << src << "to" << dst; for (auto& instance : m_instances) { const QString& instID = instance->id(); const QString instGroupName = getInstanceGroup(instID); if (instGroupName == src) { m_instanceGroupIndex[instID] = dst; increaseGroupCount(dst); qDebug() << "Set" << instID << "group to" << dst; modified = true; auto idx = getInstIndex(instance.get()); if (idx >= 0) emit dataChanged(index(idx), index(idx), { GroupRole }); } } if (modified) saveGroupList(); } bool InstanceList::isGroupCollapsed(const QString& group) { return m_collapsedGroups.contains(group); } bool InstanceList::trashInstance(const InstanceId& id) { auto inst = getInstanceById(id); if (!inst) { qWarning() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?)."; return false; } QString cachedGroupId = m_instanceGroupIndex[id]; qDebug() << "Will trash instance" << id; QString trashedLoc; if (m_instanceGroupIndex.remove(id)) { decreaseGroupCount(cachedGroupId); saveGroupList(); } if (!FS::trash(inst->instanceRoot(), &trashedLoc)) { qWarning() << "Trash of instance" << id << "has not been completely successful..."; return false; } qDebug() << "Instance" << id << "has been trashed by the launcher."; m_trashHistory.push({ id, inst->instanceRoot(), trashedLoc, cachedGroupId }); // Also trash all of its shortcuts; we remove the shortcuts if trash fails since it is invalid anyway for (const auto& [name, filePath, target] : inst->shortcuts()) { if (!FS::trash(filePath, &trashedLoc)) { qWarning() << "Trash of shortcut" << name << "at path" << filePath << "for instance" << id << "has not been successful, trying to delete it instead..."; if (!FS::deletePath(filePath)) { qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id << "has not been successful, given up..."; } else { qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been deleted by the launcher."; } continue; } qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been trashed by the launcher."; m_trashHistory.top().shortcuts.append({ { name, filePath, target }, trashedLoc }); } return true; } bool InstanceList::trashedSomething() const { return !m_trashHistory.empty(); } bool InstanceList::undoTrashInstance() { if (m_trashHistory.empty()) { qWarning() << "Nothing to recover from trash."; return true; } auto top = m_trashHistory.pop(); while (QDir(top.path).exists()) { top.id += "1"; top.path += "1"; } if (!QFile(top.trashPath).rename(top.path)) { qWarning() << "Moving" << top.trashPath << "back to" << top.path << "failed!"; return false; } qDebug() << "Moving" << top.trashPath << "back to" << top.path; bool ok = true; for (const auto& [data, trashPath] : top.shortcuts) { if (QDir(data.filePath).exists()) { // Don't try to append 1 here as the shortcut may have suffixes like .app, just warn and skip it qWarning() << "Shortcut" << trashPath << "original directory" << data.filePath << "already exists!"; ok = false; continue; } if (!QFile(trashPath).rename(data.filePath)) { qWarning() << "Moving shortcut from" << trashPath << "back to" << data.filePath << "failed!"; ok = false; continue; } qDebug() << "Moving shortcut from" << trashPath << "back to" << data.filePath; } m_instanceGroupIndex[top.id] = top.groupName; increaseGroupCount(top.groupName); saveGroupList(); emit instancesChanged(); return ok; } void InstanceList::deleteInstance(const InstanceId& id) { auto inst = getInstanceById(id); if (!inst) { qWarning() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; return; } QString cachedGroupId = m_instanceGroupIndex[id]; if (m_instanceGroupIndex.remove(id)) { decreaseGroupCount(cachedGroupId); saveGroupList(); } qDebug() << "Will delete instance" << id; if (!FS::deletePath(inst->instanceRoot())) { qWarning() << "Deletion of instance" << id << "has not been completely successful..."; return; } qDebug() << "Instance" << id << "has been deleted by the launcher."; for (const auto& [name, filePath, target] : inst->shortcuts()) { if (!FS::deletePath(filePath)) { qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id << "has not been successful..."; continue; } qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been deleted by the launcher."; } } static QMap getIdMapping(const std::vector>& list) { QMap out; int i = 0; for (auto& item : list) { auto id = item->id(); if (out.contains(id)) { qWarning() << "Duplicate ID" << id << "in instance list"; } out[id] = std::make_pair(item.get(), i); i++; } return out; } QList InstanceList::discoverInstances() { qInfo() << "Discovering instances in" << m_instDir; QList out; QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); while (iter.hasNext()) { QString subDir = iter.next(); QFileInfo dirInfo(subDir); if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists()) continue; // if it is a symlink, ignore it if it goes to the instance folder if (dirInfo.isSymLink()) { QFileInfo targetInfo(dirInfo.symLinkTarget()); QFileInfo instDirInfo(m_instDir); if (targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) { qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder"; continue; } } auto id = dirInfo.fileName(); out.append(id); qInfo() << "Found instance ID" << id; } instanceSet = QSet(out.begin(), out.end()); m_instancesProbed = true; return out; } InstanceList::InstListError InstanceList::loadList() { auto existingIds = getIdMapping(m_instances); std::vector> newList; for (auto& id : discoverInstances()) { if (existingIds.contains(id)) { existingIds.remove(id); qInfo() << "Should keep and soft-reload" << id; } else { std::unique_ptr instPtr = loadInstance(id); if (instPtr) { newList.push_back(std::move(instPtr)); } } } // TODO: looks like a general algorithm with a few specifics inserted. Do something about it. if (!existingIds.isEmpty()) { // get the list of removed instances and sort it by their original index, from last to first auto deadList = existingIds.values(); auto orderSortPredicate = [](const InstanceLocator& a, const InstanceLocator& b) -> bool { return a.second > b.second; }; std::sort(deadList.begin(), deadList.end(), orderSortPredicate); // remove the contiguous ranges of rows int front_bookmark = -1; int back_bookmark = -1; int currentItem = -1; auto removeNow = [this, &front_bookmark, &back_bookmark, ¤tItem]() { beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark); m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1); endRemoveRows(); front_bookmark = -1; back_bookmark = currentItem; }; for (auto& removedItem : deadList) { auto instPtr = removedItem.first; instPtr->invalidate(); currentItem = removedItem.second; if (back_bookmark == -1) { // no bookmark yet back_bookmark = currentItem; } else if (currentItem == front_bookmark - 1) { // part of contiguous sequence, continue } else { // seam between previous and current item removeNow(); } front_bookmark = currentItem; } if (back_bookmark != -1) { removeNow(); } } if (newList.size()) { add(newList); } m_dirty = false; updateTotalPlayTime(); return NoError; } void InstanceList::updateTotalPlayTime() { totalPlayTime = 0; for (const auto& itr : m_instances) { totalPlayTime += itr->totalTimePlayed(); } } void InstanceList::saveNow() { for (auto& item : m_instances) { item->saveNow(); } } void InstanceList::add(std::vector>& t) { beginInsertRows(QModelIndex(), count(), static_cast(count() + t.size() - 1)); for (auto& ptr : t) { m_instances.push_back(std::move(ptr)); connect(m_instances.back().get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); } endInsertRows(); } void InstanceList::resumeWatch() { if (m_watchLevel > 0) { qWarning() << "Bad suspend level resume in instance list"; return; } m_watchLevel++; if (m_watchLevel > 0 && m_dirty) { loadList(); } } void InstanceList::suspendWatch() { m_watchLevel--; } void InstanceList::providerUpdated() { m_dirty = true; if (m_watchLevel == 1) { loadList(); } } BaseInstance* InstanceList::getInstanceById(QString instId) const { if (instId.isEmpty()) return nullptr; for (auto& inst : m_instances) { if (inst->id() == instId) { return inst.get(); } } return nullptr; } BaseInstance* InstanceList::getInstanceByManagedName(const QString& managed_name) const { if (managed_name.isEmpty()) return {}; for (auto& instance : m_instances) { if (instance->getManagedPackName() == managed_name) return instance.get(); } return {}; } QModelIndex InstanceList::getInstanceIndexById(const QString& id) const { return index(getInstIndex(getInstanceById(id))); } int InstanceList::getInstIndex(BaseInstance* inst) const { int count = this->count(); for (int i = 0; i < count; i++) { if (inst == m_instances.at(i).get()) { return i; } } return -1; } void InstanceList::propertiesChanged(BaseInstance* inst) { int i = getInstIndex(inst); if (i != -1) { emit dataChanged(index(i), index(i)); updateTotalPlayTime(); } } std::unique_ptr InstanceList::loadInstance(const InstanceId& id) { if (!m_groupsLoaded) { loadGroupList(); } auto instanceRoot = FS::PathCombine(m_instDir, id); auto instanceSettings = std::make_unique(FS::PathCombine(instanceRoot, "instance.cfg")); std::unique_ptr inst; instanceSettings->registerSetting("InstanceType", ""); QString inst_type = instanceSettings->get("InstanceType").toString(); // NOTE: Some launcher versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a // OneSix instance if (inst_type == "OneSix" || inst_type.isEmpty()) { inst.reset(new MinecraftInstance(m_globalSettings, std::move(instanceSettings), instanceRoot)); } else { inst.reset(new NullInstance(m_globalSettings, std::move(instanceSettings), instanceRoot)); } qDebug() << "Loaded instance" << inst->name() << "from" << inst->instanceRoot(); auto shortcut = inst->shortcuts(); if (!shortcut.isEmpty()) qDebug() << "Loaded" << shortcut.size() << "shortcut(s) for instance" << inst->name(); return inst; } void InstanceList::increaseGroupCount(const QString& group) { if (group.isEmpty()) return; ++m_groupNameCache[group]; } void InstanceList::decreaseGroupCount(const QString& group) { if (group.isEmpty()) return; if (--m_groupNameCache[group] < 1) { m_groupNameCache.remove(group); m_collapsedGroups.remove(group); } } void InstanceList::saveGroupList() { qDebug() << "Will save group list now."; if (!m_instancesProbed) { qDebug() << "Group saving prevented because we don't know the full list of instances yet."; return; } WatchLock foo(m_watcher, m_instDir); QString groupFileName = m_instDir + "/instgroups.json"; QMap> reverseGroupMap; for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) { const QString& id = iter.key(); QString group = iter.value(); if (group.isEmpty()) continue; if (!instanceSet.contains(id)) { qDebug() << "Skipping saving missing instance" << id << "to groups list."; continue; } if (!reverseGroupMap.count(group)) { QSet set; set.insert(id); reverseGroupMap[group] = set; } else { QSet& set = reverseGroupMap[group]; set.insert(id); } } QJsonObject toplevel; toplevel.insert("formatVersion", QJsonValue(QString("1"))); QJsonObject groupsArr; for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++) { auto list = iter.value(); auto name = iter.key(); QJsonObject groupObj; QJsonArray instanceArr; groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name))); for (auto item : list) { instanceArr.append(QJsonValue(item)); } groupObj.insert("instances", instanceArr); groupsArr.insert(name, groupObj); } toplevel.insert("groups", groupsArr); // empty string represents ungrouped "group" if (m_collapsedGroups.contains("")) { QJsonObject ungrouped; ungrouped.insert("hidden", QJsonValue(true)); toplevel.insert("ungrouped", ungrouped); } QJsonDocument doc(toplevel); try { FS::write(groupFileName, doc.toJson()); qDebug() << "Group list saved."; } catch (const FS::FileSystemException& e) { qCritical() << "Failed to write instance group file :" << e.cause(); } } void InstanceList::loadGroupList() { qDebug() << "Will load group list now."; QString groupFileName = m_instDir + "/instgroups.json"; // if there's no group file, fail if (!QFileInfo(groupFileName).exists()) return; QByteArray jsonData; try { jsonData = FS::read(groupFileName); } catch (const FS::FileSystemException& e) { qCritical() << "Failed to read instance group file :" << e.cause(); return; } QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error); // if the json was bad, fail if (error.error != QJsonParseError::NoError) { qCritical() << QString("Failed to parse instance group file: %1 at offset %2") .arg(error.errorString(), QString::number(error.offset)) .toUtf8(); return; } // if the root of the json wasn't an object, fail if (!jsonDoc.isObject()) { qWarning() << "Invalid group file. Root entry should be an object."; return; } QJsonObject rootObj = jsonDoc.object(); // Make sure the format version matches, otherwise fail. if (rootObj.value("formatVersion").toVariant().toInt() != GROUP_FILE_FORMAT_VERSION) return; // Get the groups. if it's not an object, fail if (!rootObj.value("groups").isObject()) { qWarning() << "Invalid group list JSON: 'groups' should be an object."; return; } m_instanceGroupIndex.clear(); m_groupNameCache.clear(); // Iterate through all the groups. QJsonObject groupMapping = rootObj.value("groups").toObject(); for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) { QString groupName = iter.key(); if (iter.key().isEmpty()) { qWarning() << "Redundant empty group found"; continue; } // If not an object, complain and skip to the next one. if (!iter.value().isObject()) { qWarning() << QString("Group '%1' in the group list should be an object").arg(groupName).toUtf8(); continue; } QJsonObject groupObj = iter.value().toObject(); if (!groupObj.value("instances").isArray()) { qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.") .arg(groupName) .toUtf8(); continue; } auto hidden = groupObj.value("hidden").toBool(false); if (hidden) m_collapsedGroups.insert(groupName); // Iterate through the list of instances in the group. QJsonArray instancesArray = groupObj.value("instances").toArray(); for (auto value : instancesArray) { m_instanceGroupIndex[value.toString()] = groupName; increaseGroupCount(groupName); } } bool ungroupedHidden = false; if (rootObj.value("ungrouped").isObject()) { QJsonObject ungrouped = rootObj.value("ungrouped").toObject(); ungroupedHidden = ungrouped.value("hidden").toBool(false); } if (ungroupedHidden) { // empty string represents ungrouped "group" m_collapsedGroups.insert(""); } m_groupsLoaded = true; qDebug() << "Group list loaded."; } void InstanceList::instanceDirContentsChanged(const QString& path) { Q_UNUSED(path); emit instancesChanged(); } void InstanceList::on_InstFolderChanged([[maybe_unused]] const Setting& setting, QVariant value) { QString newInstDir = QDir(value.toString()).canonicalPath(); if (newInstDir != m_instDir) { if (m_groupsLoaded) { saveGroupList(); } m_instDir = newInstDir; m_groupsLoaded = false; beginRemoveRows(QModelIndex(), 0, count()); m_instances.erase(m_instances.begin(), m_instances.end()); endRemoveRows(); emit instancesChanged(); } } void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed) { qDebug() << "Group" << group << (collapsed ? "collapsed" : "expanded"); if (collapsed) { m_collapsedGroups.insert(group); } else { m_collapsedGroups.remove(group); } saveGroupList(); } class InstanceStaging : public Task { Q_OBJECT const unsigned minBackoff = 1; const unsigned maxBackoff = 16; public: InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObject* settings) : m_parent(parent), backoff(minBackoff, maxBackoff) { m_stagingPath = parent->getStagedInstancePath(); m_child.reset(child); m_child->setStagingPath(m_stagingPath); m_child->setParentSettings(settings); connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded); connect(child, &Task::failed, this, &InstanceStaging::childFailed); connect(child, &Task::aborted, this, &InstanceStaging::childAborted); connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable); connect(child, &Task::abortButtonTextChanged, this, &InstanceStaging::setAbortButtonText); connect(child, &Task::status, this, &InstanceStaging::setStatus); connect(child, &Task::details, this, &InstanceStaging::setDetails); connect(child, &Task::progress, this, &InstanceStaging::setProgress); connect(child, &Task::stepProgress, this, &InstanceStaging::propagateStepProgress); connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded); } ~InstanceStaging() override = default; // FIXME/TODO: add ability to abort during instance commit retries bool abort() override { if (!canAbort()) { return false; } return m_child->abort(); } bool canAbort() const override { return (m_child && m_child->canAbort()); } protected: void executeTask() override { if (m_stagingPath.isNull()) { emitFailed(tr("Could not create staging folder")); return; } m_child->start(); } QStringList warnings() const override { return m_child->warnings(); } private slots: void childSucceeded() { unsigned sleepTime = backoff(); if (m_parent->commitStagedInstance(m_stagingPath, *m_child, m_child->group(), *m_child)) { m_backoffTimer.stop(); emitSucceeded(); return; } // we actually failed, retry? if (sleepTime == maxBackoff) { m_backoffTimer.stop(); emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); return; } qDebug() << "Failed to commit instance" << m_child->name() << "Initiating backoff:" << sleepTime; m_backoffTimer.start(sleepTime * 500); } void childFailed(const QString& reason) { m_backoffTimer.stop(); m_parent->destroyStagingPath(m_stagingPath); emitFailed(reason); } void childAborted() { m_backoffTimer.stop(); m_parent->destroyStagingPath(m_stagingPath); emitAborted(); } private: InstanceList* m_parent; /* * WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows. * Basically, it starts messing things up while the launcher is extracting/creating instances * and causes that horrible failure that is NTFS to lock files in place because they are open. */ ExponentialSeries backoff; QString m_stagingPath; std::unique_ptr m_child; QTimer m_backoffTimer; }; Task* InstanceList::wrapInstanceTask(InstanceTask* task) { return new InstanceStaging(this, task, m_globalSettings); } QString InstanceList::getStagedInstancePath() { const QString tempRoot = FS::PathCombine(m_instDir, ".tmp"); QString result; int tries = 0; do { if (++tries > 256) return {}; const QString key = QUuid::createUuid().toString(QUuid::Id128).left(6); result = FS::PathCombine(tempRoot, key); } while (QFileInfo::exists(result)); if (!QDir::current().mkpath(result)) return {}; #ifdef Q_OS_WIN32 SetFileAttributesA(tempRoot.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); #endif return result; } bool InstanceList::commitStagedInstance(const QString& path, const InstanceName& instanceName, QString groupName, const InstanceTask& commiting) { if (groupName.isEmpty() && !groupName.isNull()) groupName = QString(); QString instID; auto should_override = commiting.shouldOverride(); if (should_override) { instID = commiting.originalInstanceID(); } else { instID = FS::DirNameFromString(instanceName.modifiedName(), m_instDir); } Q_ASSERT(!instID.isEmpty()); { WatchLock lock(m_watcher, m_instDir); QString destination = FS::PathCombine(m_instDir, instID); if (should_override) { if (!FS::overrideFolder(destination, path)) { qWarning() << "Failed to override" << path << "to" << destination; return false; } } else { if (!FS::move(path, destination)) { qWarning() << "Failed to move" << path << "to" << destination; return false; } m_instanceGroupIndex[instID] = groupName; increaseGroupCount(groupName); } instanceSet.insert(instID); emit instancesChanged(); emit instanceSelectRequest(instID); } saveGroupList(); return true; } bool InstanceList::destroyStagingPath(const QString& keyPath) { return FS::deletePath(keyPath); } int InstanceList::getTotalPlayTime() { updateTotalPlayTime(); return totalPlayTime; } #include "InstanceList.moc" PrismLauncher-11.0.3/launcher/icons/0000755000175100017510000000000015224505336016677 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/icons/MMCIcon.cpp0000644000175100017510000000730115224505336020631 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "MMCIcon.h" #include #include IconType operator--(IconType& t, int) { IconType temp = t; switch (t) { case IconType::Builtin: t = IconType::ToBeDeleted; break; case IconType::Transient: t = IconType::Builtin; break; case IconType::FileBased: t = IconType::Transient; break; default: break; } return temp; } IconType MMCIcon::type() const { return m_current_type; } QString MMCIcon::name() const { if (m_name.size()) return m_name; return m_key; } bool MMCIcon::has(IconType _type) const { return m_images[_type].present(); } QIcon MMCIcon::icon() const { if (m_current_type == IconType::ToBeDeleted) return QIcon(); auto& icon = m_images[m_current_type].icon; if (!icon.isNull()) return icon; // FIXME: inject this. return QIcon::fromTheme(m_images[m_current_type].key); } void MMCIcon::remove(IconType rm_type) { m_images[rm_type].filename = QString(); m_images[rm_type].icon = QIcon(); for (auto iter = rm_type; iter != IconType::ToBeDeleted; iter--) { if (m_images[iter].present()) { m_current_type = iter; return; } } m_current_type = IconType::ToBeDeleted; } void MMCIcon::replace(IconType new_type, QIcon icon, QString path) { if (new_type > m_current_type || m_current_type == IconType::ToBeDeleted) { m_current_type = new_type; } m_images[new_type].icon = icon; m_images[new_type].filename = path; m_images[new_type].key = QString(); } void MMCIcon::replace(IconType new_type, const QString& key) { if (new_type > m_current_type || m_current_type == IconType::ToBeDeleted) { m_current_type = new_type; } m_images[new_type].icon = QIcon(); m_images[new_type].filename = QString(); m_images[new_type].key = key; } QString MMCIcon::getFilePath() const { if (m_current_type == IconType::ToBeDeleted) { return QString(); } return m_images[m_current_type].filename; } bool MMCIcon::isBuiltIn() const { return m_current_type == IconType::Builtin; } PrismLauncher-11.0.3/launcher/icons/IconList.h0000644000175100017510000000751315224505336020602 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include "MMCIcon.h" #include "settings/Setting.h" #include "QObjectPtr.h" class QFileSystemWatcher; class IconList : public QAbstractListModel { Q_OBJECT public: explicit IconList(const QStringList& builtinPaths, const QString& path, QObject* parent = 0); virtual ~IconList() {}; QIcon getIcon(const QString& key) const; int getIconIndex(const QString& key) const; QString getDirectory() const; virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; virtual QStringList mimeTypes() const override; virtual Qt::DropActions supportedDropActions() const override; virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; virtual Qt::ItemFlags flags(const QModelIndex& index) const override; bool addThemeIcon(const QString& key); bool addIcon(const QString& key, const QString& name, const QString& path, IconType type); void saveIcon(const QString& key, const QString& path, const char* format) const; bool deleteIcon(const QString& key); bool trashIcon(const QString& key); bool iconFileExists(const QString& key) const; QString iconDirectory(const QString& key) const; void installIcons(const QStringList& iconFiles); void installIcon(const QString& file, const QString& name); const MMCIcon* icon(const QString& key) const; void startWatching(); void stopWatching(); signals: void iconUpdated(QString key); private: // hide copy constructor IconList(const IconList&) = delete; // hide assign op IconList& operator=(const IconList&) = delete; void reindex(); void sortIconList(); bool addPathRecursively(const QString& path); QStringList getIconFilePaths() const; public slots: void directoryChanged(const QString& path); protected slots: void fileChanged(const QString& path); void SettingChanged(const Setting& setting, const QVariant& value); private: shared_qobject_ptr m_watcher; bool m_isWatching; QMap m_nameIndex; QList m_icons; QDir m_dir; }; PrismLauncher-11.0.3/launcher/icons/IconList.cpp0000644000175100017510000003461515224505336021140 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "IconList.h" #include #include #include #include #include #include #include #include #include "icons/IconUtils.h" #define MAX_SIZE 1024 IconList::IconList(const QStringList& builtinPaths, const QString& path, QObject* parent) : QAbstractListModel(parent) { QSet builtinNames; // add builtin icons for (const auto& builtinPath : builtinPaths) { QDir instanceIcons(builtinPath); auto fileInfoList = instanceIcons.entryInfoList(QDir::Files, QDir::Name); for (const auto& fileInfo : fileInfoList) { builtinNames.insert(fileInfo.completeBaseName()); } } for (const auto& builtinName : builtinNames) { addThemeIcon(builtinName); } m_watcher.reset(new QFileSystemWatcher()); m_isWatching = false; connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged); connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged); directoryChanged(path); // Forces the UI to update, so that lengthy icon names are shown properly from the start emit iconUpdated({}); } void IconList::sortIconList() { qDebug() << "Sorting icon list..."; std::sort(m_icons.begin(), m_icons.end(), [](const MMCIcon& a, const MMCIcon& b) { bool aIsSubdir = a.m_key.contains(QDir::separator()); bool bIsSubdir = b.m_key.contains(QDir::separator()); if (aIsSubdir != bIsSubdir) { return !aIsSubdir; // root-level icons come first } return a.m_key.localeAwareCompare(b.m_key) < 0; }); reindex(); } // Helper function to add directories recursively bool IconList::addPathRecursively(const QString& path) { QDir dir(path); if (!dir.exists()) return false; // Add the directory itself bool watching = m_watcher->addPath(path); // Add all subdirectories QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QFileInfo& entry : entries) { if (addPathRecursively(entry.absoluteFilePath())) { watching = true; } } return watching; } QStringList IconList::getIconFilePaths() const { QStringList iconFiles{}; QStringList directories{ m_dir.absolutePath() }; while (!directories.isEmpty()) { QString first = directories.takeFirst(); QDir dir(first); for (QFileInfo& fileInfo : dir.entryInfoList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) { if (fileInfo.isDir()) directories.push_back(fileInfo.absoluteFilePath()); else iconFiles.push_back(fileInfo.absoluteFilePath()); } } return iconFiles; } QString formatName(const QDir& iconsDir, const QFileInfo& iconFile) { if (iconFile.dir() == iconsDir) return iconFile.completeBaseName(); constexpr auto delimiter = " » "; QString relativePathWithoutExtension = iconsDir.relativeFilePath(iconFile.dir().path()) + QDir::separator() + iconFile.completeBaseName(); return relativePathWithoutExtension.replace(QDir::separator(), delimiter); } /// Split into a separate function because the preprocessing impedes readability QSet toStringSet(const QList& list) { QSet set(list.begin(), list.end()); return set; } void IconList::directoryChanged(const QString& path) { QDir newDir(path); if (m_dir.absolutePath() != newDir.absolutePath()) { m_dir.setPath(path); m_dir.refresh(); if (m_isWatching) stopWatching(); startWatching(); } if (!m_dir.exists() && !FS::ensureFolderPathExists(m_dir.absolutePath())) return; m_dir.refresh(); const QStringList newFileNamesList = getIconFilePaths(); const QSet newSet = toStringSet(newFileNamesList); QSet currentSet; for (const MMCIcon& it : m_icons) { if (!it.has(IconType::FileBased)) continue; QFileInfo icon(it.getFilePath()); currentSet.insert(icon.absoluteFilePath()); } QSet toRemove = currentSet - newSet; QSet toAdd = newSet - currentSet; for (const QString& removedPath : toRemove) { qDebug() << "Removing icon" << removedPath; QFileInfo removedFile(removedPath); QString relativePath = m_dir.relativeFilePath(removedFile.absoluteFilePath()); QString key = QFileInfo(relativePath).completeBaseName(); int idx = getIconIndex(key); if (idx == -1) continue; m_icons[idx].remove(FileBased); if (m_icons[idx].type() == ToBeDeleted) { beginRemoveRows(QModelIndex(), idx, idx); m_icons.remove(idx); reindex(); endRemoveRows(); } else { dataChanged(index(idx), index(idx)); } m_watcher->removePath(removedPath); emit iconUpdated(key); } for (const QString& addedPath : toAdd) { qDebug() << "Adding icon" << addedPath; QFileInfo addfile(addedPath); QString relativePath = m_dir.relativeFilePath(addfile.absoluteFilePath()); QString key = QFileInfo(relativePath).completeBaseName(); QString name = formatName(m_dir, addfile); if (addIcon(key, name, addfile.filePath(), IconType::FileBased)) { m_watcher->addPath(addedPath); emit iconUpdated(key); } } sortIconList(); } void IconList::fileChanged(const QString& path) { qDebug() << "Checking icon" << path; QFileInfo checkfile(path); if (!checkfile.exists()) return; QString key = m_dir.relativeFilePath(checkfile.absoluteFilePath()); int idx = getIconIndex(key); if (idx == -1) return; QIcon icon; // special handling for jpg and jpeg to go through pixmap to keep the size constant if (path.endsWith(".jpg") || path.endsWith(".jpeg")) { icon.addPixmap(QPixmap(path)); } else { icon.addFile(path); } if (icon.availableSizes().empty()) return; m_icons[idx].m_images[IconType::FileBased].icon = icon; dataChanged(index(idx), index(idx)); emit iconUpdated(key); } void IconList::SettingChanged(const Setting& setting, const QVariant& value) { if (setting.id() != "IconsDir") return; directoryChanged(value.toString()); } void IconList::startWatching() { auto abs_path = m_dir.absolutePath(); FS::ensureFolderPathExists(abs_path); m_isWatching = addPathRecursively(abs_path); if (m_isWatching) { qDebug() << "Started watching" << abs_path; } else { qDebug() << "Failed to start watching" << abs_path; } } void IconList::stopWatching() { m_watcher->removePaths(m_watcher->files()); m_watcher->removePaths(m_watcher->directories()); m_isWatching = false; } QStringList IconList::mimeTypes() const { QStringList types; types << "text/uri-list"; return types; } Qt::DropActions IconList::supportedDropActions() const { return Qt::CopyAction; } bool IconList::dropMimeData(const QMimeData* data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex& parent) { if (action == Qt::IgnoreAction) return true; // check if the action is supported if (!data || !(action & supportedDropActions())) return false; // files dropped from outside? if (data->hasUrls()) { auto urls = data->urls(); QStringList iconFiles; for (const auto& url : urls) { // only local files may be dropped... if (!url.isLocalFile()) continue; iconFiles += url.toLocalFile(); } installIcons(iconFiles); return true; } return false; } Qt::ItemFlags IconList::flags(const QModelIndex& index) const { Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); return Qt::ItemIsDropEnabled | defaultFlags; } QVariant IconList::data(const QModelIndex& index, int role) const { if (!index.isValid()) return {}; int row = index.row(); if (row < 0 || row >= m_icons.size()) return {}; switch (role) { case Qt::DecorationRole: return m_icons[row].icon(); case Qt::DisplayRole: return m_icons[row].name(); case Qt::UserRole: return m_icons[row].m_key; default: return {}; } } int IconList::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_icons.size(); } void IconList::installIcons(const QStringList& iconFiles) { for (const QString& file : iconFiles) installIcon(file, {}); } void IconList::installIcon(const QString& file, const QString& name) { QFileInfo fileinfo(file); if (!fileinfo.isReadable() || !fileinfo.isFile()) return; if (!IconUtils::isIconSuffix(fileinfo.suffix())) return; QString target = FS::PathCombine(getDirectory(), name.isEmpty() ? fileinfo.fileName() : name); QFile::copy(file, target); } bool IconList::iconFileExists(const QString& key) const { auto iconEntry = icon(key); return iconEntry && iconEntry->has(IconType::FileBased); } /// Returns the icon with the given key or nullptr if it doesn't exist. const MMCIcon* IconList::icon(const QString& key) const { int iconIdx = getIconIndex(key); if (iconIdx == -1) return nullptr; return &m_icons[iconIdx]; } bool IconList::deleteIcon(const QString& key) { return iconFileExists(key) && FS::deletePath(icon(key)->getFilePath()); } bool IconList::trashIcon(const QString& key) { return iconFileExists(key) && FS::trash(icon(key)->getFilePath(), nullptr); } bool IconList::addThemeIcon(const QString& key) { auto iter = m_nameIndex.find(key); if (iter != m_nameIndex.end()) { auto& oldOne = m_icons[*iter]; oldOne.replace(Builtin, key); dataChanged(index(*iter), index(*iter)); return true; } // add a new icon beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size()); { MMCIcon mmc_icon; mmc_icon.m_name = key; mmc_icon.m_key = key; mmc_icon.replace(Builtin, key); m_icons.push_back(mmc_icon); m_nameIndex[key] = m_icons.size() - 1; } endInsertRows(); return true; } bool IconList::addIcon(const QString& key, const QString& name, const QString& path, const IconType type) { // replace the icon even? is the input valid? QIcon icon; // special handling for jpg and jpeg to go through pixmap to keep the size constant if (path.endsWith(".jpg") || path.endsWith(".jpeg")) { icon.addPixmap(QPixmap(path)); } else { icon.addFile(path); } if (icon.isNull()) return false; auto iter = m_nameIndex.find(key); if (iter != m_nameIndex.end()) { auto& oldOne = m_icons[*iter]; oldOne.replace(type, icon, path); dataChanged(index(*iter), index(*iter)); return true; } // add a new icon beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size()); { MMCIcon mmc_icon; mmc_icon.m_name = name; mmc_icon.m_key = key; mmc_icon.replace(type, icon, path); m_icons.push_back(mmc_icon); m_nameIndex[key] = m_icons.size() - 1; } endInsertRows(); return true; } void IconList::saveIcon(const QString& key, const QString& path, const char* format) const { auto icon = getIcon(key); auto pixmap = icon.pixmap(128, 128); pixmap.save(path, format); } void IconList::reindex() { m_nameIndex.clear(); for (int i = 0; i < m_icons.size(); i++) { m_nameIndex[m_icons[i].m_key] = i; emit iconUpdated(m_icons[i].m_key); // prevents incorrect indices with proxy model } } QIcon IconList::getIcon(const QString& key) const { int iconIndex = getIconIndex(key); if (iconIndex != -1) return m_icons[iconIndex].icon(); // Fallback for icons that don't exist.b iconIndex = getIconIndex("grass"); if (iconIndex != -1) return m_icons[iconIndex].icon(); return {}; } int IconList::getIconIndex(const QString& key) const { auto iter = m_nameIndex.find(key == "default" ? "grass" : key); if (iter != m_nameIndex.end()) return *iter; return -1; } QString IconList::getDirectory() const { return m_dir.absolutePath(); } /// Returns the directory of the icon with the given key or the default directory if it's a builtin icon. QString IconList::iconDirectory(const QString& key) const { for (const auto& mmcIcon : m_icons) { if (mmcIcon.m_key == key && mmcIcon.has(IconType::FileBased)) { QFileInfo iconFile(mmcIcon.getFilePath()); return iconFile.dir().path(); } } return getDirectory(); } PrismLauncher-11.0.3/launcher/icons/IconUtils.cpp0000644000175100017510000000452615224505336021323 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "IconUtils.h" #include #include "FileSystem.h" namespace { static const QStringList validIconExtensions = { { "svg", "png", "ico", "gif", "jpg", "jpeg", "webp" } }; } namespace IconUtils { QString findBestIconIn(const QString& folder, const QString& iconKey) { QString best_filename; QDirIterator it(folder, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::NoIteratorFlags); while (it.hasNext()) { it.next(); auto fileInfo = it.fileInfo(); if ((fileInfo.completeBaseName() == iconKey || fileInfo.fileName() == iconKey) && isIconSuffix(fileInfo.suffix())) return fileInfo.absoluteFilePath(); } return {}; } QString getIconFilter() { return "(*." + validIconExtensions.join(" *.") + ")"; } bool isIconSuffix(QString suffix) { return validIconExtensions.contains(suffix); } } // namespace IconUtils PrismLauncher-11.0.3/launcher/icons/IconUtils.h0000644000175100017510000000352215224505336020763 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 namespace IconUtils { // Given a folder and an icon key, find 'best' of the icons with the given key in there and return its path QString findBestIconIn(const QString& folder, const QString& iconKey); // Get icon file type filter for file browser dialogs QString getIconFilter(); bool isIconSuffix(QString suffix); } // namespace IconUtils PrismLauncher-11.0.3/launcher/icons/MMCIcon.h0000644000175100017510000000435015224505336020277 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include enum IconType : unsigned { Builtin, Transient, FileBased, ICONS_TOTAL, ToBeDeleted }; struct MMCImage { QIcon icon; QString key; QString filename; bool present() const { return !icon.isNull() || !key.isEmpty(); } }; struct MMCIcon { QString m_key; QString m_name; MMCImage m_images[ICONS_TOTAL]; IconType m_current_type = ToBeDeleted; IconType type() const; QString name() const; bool has(IconType _type) const; QIcon icon() const; void remove(IconType rm_type); void replace(IconType new_type, QIcon icon, QString path = QString()); void replace(IconType new_type, const QString& key); bool isBuiltIn() const; QString getFilePath() const; }; PrismLauncher-11.0.3/launcher/news/0000755000175100017510000000000015224505336016540 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/news/NewsEntry.cpp0000644000175100017510000000345715224505336021213 0ustar runnerrunner/* 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 "NewsEntry.h" #include #include NewsEntry::NewsEntry(QObject* parent) : QObject(parent) { this->title = tr("Untitled"); this->content = tr("No content."); this->link = ""; } NewsEntry::NewsEntry(const QString& title, const QString& content, const QString& link, QObject* parent) : QObject(parent) { this->title = title; this->content = content; this->link = link; } /*! * Gets the text content of the given child element as a QVariant. */ inline QString childValue(const QDomElement& element, const QString& childName, QString defaultVal = "") { QDomNodeList nodes = element.elementsByTagName(childName); if (nodes.count() > 0) { QDomElement elem = nodes.at(0).toElement(); return elem.text(); } else { return defaultVal; } } bool NewsEntry::fromXmlElement(const QDomElement& element, NewsEntry* entry, [[maybe_unused]] QString* errorMsg) { QString title = childValue(element, "title", tr("Untitled")); QString content = childValue(element, "content", tr("No content.")); QString link = childValue(element, "id"); entry->title = title; entry->content = content; entry->link = link; return true; } PrismLauncher-11.0.3/launcher/news/NewsChecker.h0000644000175100017510000000532515224505336021117 0ustar runnerrunner/* 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 #include #include #include #include "NewsEntry.h" class NewsChecker : public QObject { Q_OBJECT public: /*! * Constructs a news reader to read from the given RSS feed URL. */ NewsChecker(QNetworkAccessManager* network, const QString& feedUrl); /*! * Returns the error message for the last time the news was loaded. * Empty string if the last load was successful. */ QString getLastLoadErrorMsg() const; /*! * Returns true if the news has been loaded successfully. */ bool isNewsLoaded() const; //! True if the news is currently loading. If true, reloadNews() will do nothing. bool isLoadingNews() const; /*! * Returns a list of news entries. */ QList getNewsEntries() const; /*! * Reloads the news from the website's RSS feed. * If the news is already loading, this does nothing. */ void Q_SLOT reloadNews(); signals: /*! * Signal fired after the news has finished loading. */ void newsLoaded(); /*! * Signal fired after the news fails to load. */ void newsLoadingFailed(QString errorMsg); protected slots: void rssDownloadFinished(); void rssDownloadFailed(QString reason); protected: /* data */ //! The URL for the RSS feed to fetch. QString m_feedUrl; //! List of news entries. QList m_newsEntries; //! The network job to use to load the news. NetJob::Ptr m_newsNetJob; //! True if news has been loaded. bool m_loadedNews; //! The cache entry for the feed. MetaEntryPtr m_entry; /*! * Gets the error message that was given last time the news was loaded. * If the last news load succeeded, this will be an empty string. */ QString m_lastLoadError; QNetworkAccessManager* m_network; protected slots: /// Emits newsLoaded() and sets m_lastLoadError to empty string. void succeed(); /// Emits newsLoadingFailed() and sets m_lastLoadError to the given message. void fail(const QString& errorMsg); }; PrismLauncher-11.0.3/launcher/news/NewsChecker.cpp0000644000175100017510000001113615224505336021447 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "NewsChecker.h" #include #include #include #include "Application.h" NewsChecker::NewsChecker(QNetworkAccessManager* network, const QString& feedUrl) { m_network = network; m_feedUrl = feedUrl; } void NewsChecker::reloadNews() { // Start a netjob to download the RSS feed and call rssDownloadFinished() when it's done. if (isLoadingNews()) { qDebug() << "Ignored request to reload news. Currently reloading already."; return; } m_entry = APPLICATION->metacache()->resolveEntry("feed", "feed.xml"); qDebug() << "Reloading news."; NetJob::Ptr job{ new NetJob("News RSS Feed", m_network) }; job->addNetAction(Net::Download::makeCached(m_feedUrl, m_entry)); job->setAskRetry(false); connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed); m_newsNetJob.reset(job); job->start(); } void NewsChecker::rssDownloadFinished() { // Parse the XML file and process the RSS feed entries. qDebug() << "Finished loading RSS feed."; m_newsNetJob.reset(); QDomDocument doc; { // Stuff to store error info in. QString errorMsg = "Unknown error."; int errorLine = -1; int errorCol = -1; QFile feed(m_entry->getFullPath()); if (feed.open(QFile::ReadOnly | QFile::Text)) { QTextStream in(&feed); // Parse the XML. if (!doc.setContent(in.readAll(), false, &errorMsg, &errorLine, &errorCol)) { fail(QString("Error parsing RSS feed XML. %1 at %2:%3.").arg(errorMsg).arg(errorLine).arg(errorCol)); return; } } } // If the parsing succeeded, read it. QDomNodeList items = doc.elementsByTagName("entry"); m_newsEntries.clear(); for (int i = 0; i < items.length(); i++) { QDomElement element = items.at(i).toElement(); NewsEntryPtr entry; entry.reset(new NewsEntry()); QString errorMsg = "An unknown error occurred."; if (NewsEntry::fromXmlElement(element, entry.get(), &errorMsg)) { qDebug() << "Loaded news entry" << entry->title; m_newsEntries.append(entry); } else { qWarning() << "Failed to load news entry at index" << i << ":" << errorMsg; } } succeed(); } void NewsChecker::rssDownloadFailed(QString reason) { // Set an error message and fail. fail(tr("Failed to load news RSS feed:\n%1").arg(reason)); } QList NewsChecker::getNewsEntries() const { return m_newsEntries; } bool NewsChecker::isLoadingNews() const { return m_newsNetJob.get() != nullptr; } QString NewsChecker::getLastLoadErrorMsg() const { return m_lastLoadError; } void NewsChecker::succeed() { m_lastLoadError = ""; qDebug() << "News loading succeeded."; m_newsNetJob.reset(); emit newsLoaded(); } void NewsChecker::fail(const QString& errorMsg) { m_lastLoadError = errorMsg; qDebug() << "Failed to load news:" << errorMsg; m_newsNetJob.reset(); emit newsLoadingFailed(errorMsg); } PrismLauncher-11.0.3/launcher/news/NewsEntry.h0000644000175100017510000000306615224505336020654 0ustar runnerrunner/* 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 #include #include #include class NewsEntry : public QObject { Q_OBJECT public: /*! * Constructs an empty news entry. */ explicit NewsEntry(QObject* parent = 0); /*! * Constructs a new news entry. * Note that content may contain HTML. */ NewsEntry(const QString& title, const QString& content, const QString& link, QObject* parent = 0); /*! * Attempts to load information from the given XML element into the given news entry pointer. * If this fails, the function will return false and store an error message in the errorMsg pointer. */ static bool fromXmlElement(const QDomElement& element, NewsEntry* entry, QString* errorMsg = 0); //! The post title. QString title; //! The post's content. May contain HTML. QString content; //! URL to the post. QString link; }; using NewsEntryPtr = std::shared_ptr; PrismLauncher-11.0.3/launcher/meta/0000755000175100017510000000000015224505336016512 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/meta/VersionList.h0000644000175100017510000000647115224505336021154 0ustar runnerrunner/* Copyright 2015-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 #include #include "BaseEntity.h" #include "BaseVersionList.h" #include "meta/Version.h" namespace Meta { class VersionList : public BaseVersionList, public BaseEntity { Q_OBJECT Q_PROPERTY(QString uid READ uid CONSTANT) Q_PROPERTY(QString name READ name NOTIFY nameChanged) public: explicit VersionList(const QString& uid, QObject* parent = nullptr); virtual ~VersionList() = default; using Ptr = std::shared_ptr; enum Roles { UidRole = Qt::UserRole + 100, TimeRole, RequiresRole, VersionPtrRole }; bool isLoaded() override; Task::Ptr getLoadTask(bool forceReload = false) override; const BaseVersion::Ptr at(int i) const override; int count() const override; void sortVersions() override; BaseVersion::Ptr getRecommended() const override; Version::Ptr getRecommendedForParent(const QString& uid, const QString& version); Version::Ptr getLatestForParent(const QString& uid, const QString& version); QVariant data(const QModelIndex& index, int role) const override; RoleList providesRoles() const override; QHash roleNames() const override; void setProvidedRoles(RoleList roles); QString localFilename() const override; QString uid() const { return m_uid; } QString name() const { return m_name; } QString humanReadable() const; Version::Ptr getVersion(const QString& version); bool hasVersion(QString version) const; QList versions() const { return m_versions; } // this blocks until the version list is loaded void waitToLoad(); public: // for usage only by parsers void setName(const QString& name); void setVersions(const QList& versions); void merge(const VersionList::Ptr& other); void mergeFromIndex(const VersionList::Ptr& other); void parse(const QJsonObject& obj) override; void addExternalRecommends(const QStringList& recommends); void clearExternalRecommends(); signals: void nameChanged(const QString& name); protected slots: void updateListData(QList) override {} private: QList m_versions; QStringList m_externalRecommendsVersions; QHash m_lookup; QString m_uid; QString m_name; Version::Ptr m_recommended; RoleList m_provided_roles = { VersionPointerRole, VersionRole, VersionIdRole, ParentVersionRole, TypeRole, UidRole, TimeRole, RequiresRole, SortRole, RecommendedRole, LatestRole, VersionPtrRole }; void setupAddedVersion(int row, const Version::Ptr& version); }; } // namespace Meta Q_DECLARE_METATYPE(Meta::VersionList::Ptr) PrismLauncher-11.0.3/launcher/meta/Index.h0000644000175100017510000000433215224505336017734 0ustar runnerrunner/* Copyright 2015-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 #include "BaseEntity.h" #include "meta/VersionList.h" #include "net/Mode.h" class Task; namespace Meta { class Index : public QAbstractListModel, public BaseEntity { Q_OBJECT public: explicit Index(QObject* parent = nullptr); explicit Index(const QList& lists, QObject* parent = nullptr); virtual ~Index() = default; enum { UidRole = Qt::UserRole, NameRole, ListPtrRole }; QVariant data(const QModelIndex& index, int role) const override; int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; QString localFilename() const override { return "index.json"; } // queries VersionList::Ptr get(const QString& uid); Version::Ptr get(const QString& uid, const QString& version); bool hasUid(const QString& uid) const; QList lists() const { return m_lists; } Task::Ptr loadVersion(const QString& uid, const QString& version = {}, Net::Mode mode = Net::Mode::Online, bool force = false); // this blocks until the version is loaded Version::Ptr getLoadedVersion(const QString& uid, const QString& version); public: // for usage by parsers only void merge(const std::shared_ptr& other); protected: void parse(const QJsonObject& obj) override; private: QList m_lists; QHash m_uids; void connectVersionList(int row, const VersionList::Ptr& list); }; } // namespace Meta PrismLauncher-11.0.3/launcher/meta/JsonFormat.cpp0000644000175100017510000001447615224505336021314 0ustar runnerrunner/* Copyright 2015-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 "JsonFormat.h" // FIXME: remove this from here... somehow #include "Json.h" #include "minecraft/OneSixVersionFormat.h" #include "Index.h" #include "Version.h" #include "VersionList.h" using namespace Json; namespace Meta { MetadataVersion currentFormatVersion() { return MetadataVersion::InitialRelease; } // Index static std::shared_ptr parseIndexInternal(const QJsonObject& obj) { const QList objects = requireIsArrayOf(obj, "packages"); QList lists; lists.reserve(objects.size()); std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) { VersionList::Ptr list = std::make_shared(requireString(obj, "uid")); list->setName(obj["name"].toString()); list->setSha256(obj["sha256"].toString()); return list; }); return std::make_shared(lists); } // Version static Version::Ptr parseCommonVersion(const QString& uid, const QJsonObject& obj) { Version::Ptr version = std::make_shared(uid, requireString(obj, "version")); version->setTime(QDateTime::fromString(requireString(obj, "releaseTime"), Qt::ISODate).toMSecsSinceEpoch() / 1000); version->setType(obj["type"].toString()); version->setRecommended(obj["recommended"].toBool()); version->setVolatile(obj["volatile"].toBool()); RequireSet reqs, conflicts; parseRequires(obj, &reqs, "requires"); parseRequires(obj, &conflicts, "conflicts"); version->setRequires(reqs, conflicts); if (auto sha256 = obj["sha256"].toString(); !sha256.isEmpty()) { version->setSha256(sha256); } return version; } static Version::Ptr parseVersionInternal(const QJsonObject& obj) { Version::Ptr version = parseCommonVersion(requireString(obj, "uid"), obj); version->setData(OneSixVersionFormat::versionFileFromJson( QJsonDocument(obj), QString("%1/%2.json").arg(version->uid(), version->version()), obj.contains("order"))); return version; } // Version list / package static VersionList::Ptr parseVersionListInternal(const QJsonObject& obj) { const QString uid = requireString(obj, "uid"); const QList versionsRaw = requireIsArrayOf(obj, "versions"); QList versions; versions.reserve(versionsRaw.size()); std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [uid](const QJsonObject& vObj) { auto version = parseCommonVersion(uid, vObj); version->setProvidesRecommendations(); return version; }); VersionList::Ptr list = std::make_shared(uid); list->setName(obj["name"].toString()); list->setVersions(versions); return list; } MetadataVersion parseFormatVersion(const QJsonObject& obj, bool required) { if (!obj.contains("formatVersion")) { if (required) { return MetadataVersion::Invalid; } return MetadataVersion::InitialRelease; } if (!obj.value("formatVersion").isDouble()) { return MetadataVersion::Invalid; } switch (obj.value("formatVersion").toInt()) { case 0: case 1: return MetadataVersion::InitialRelease; default: return MetadataVersion::Invalid; } } void serializeFormatVersion(QJsonObject& obj, Meta::MetadataVersion version) { if (version == MetadataVersion::Invalid) { return; } obj.insert("formatVersion", int(version)); } void parseIndex(const QJsonObject& obj, Index* ptr) { const MetadataVersion version = parseFormatVersion(obj); switch (version) { case MetadataVersion::InitialRelease: ptr->merge(parseIndexInternal(obj)); break; case MetadataVersion::Invalid: throw ParseException(QObject::tr("Unknown format version!")); } } void parseVersionList(const QJsonObject& obj, VersionList* ptr) { const MetadataVersion version = parseFormatVersion(obj); switch (version) { case MetadataVersion::InitialRelease: ptr->merge(parseVersionListInternal(obj)); break; case MetadataVersion::Invalid: throw ParseException(QObject::tr("Unknown format version!")); } } void parseVersion(const QJsonObject& obj, Version* ptr) { const MetadataVersion version = parseFormatVersion(obj); switch (version) { case MetadataVersion::InitialRelease: ptr->merge(parseVersionInternal(obj)); break; case MetadataVersion::Invalid: throw ParseException(QObject::tr("Unknown format version!")); } } /* [ {"uid":"foo", "equals":"version"} ] */ void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char* keyName) { if (obj.contains(keyName)) { auto reqArray = requireArray(obj, keyName); auto iter = reqArray.begin(); while (iter != reqArray.end()) { auto reqObject = requireObject(*iter); auto uid = requireString(reqObject, "uid"); auto equals = reqObject["equals"].toString(); auto suggests = reqObject["suggests"].toString(); ptr->insert({ uid, equals, suggests }); iter++; } } } void serializeRequires(QJsonObject& obj, RequireSet* ptr, const char* keyName) { if (!ptr || ptr->empty()) { return; } QJsonArray arrOut; for (auto& iter : *ptr) { QJsonObject reqOut; reqOut.insert("uid", iter.uid); if (!iter.equalsVersion.isEmpty()) { reqOut.insert("equals", iter.equalsVersion); } if (!iter.suggests.isEmpty()) { reqOut.insert("suggests", iter.suggests); } arrOut.append(reqOut); } obj.insert(keyName, arrOut); } } // namespace Meta PrismLauncher-11.0.3/launcher/meta/BaseEntity.cpp0000644000175100017510000001626115224505336021273 0ustar runnerrunner/* Copyright 2015-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 "BaseEntity.h" #include "Exception.h" #include "FileSystem.h" #include "Json.h" #include "modplatform/helpers/HashUtils.h" #include "net/ApiDownload.h" #include "net/ChecksumValidator.h" #include "net/HttpMetaCache.h" #include "net/Mode.h" #include "net/NetJob.h" #include "Application.h" #include "settings/SettingsObject.h" #include "BuildConfig.h" #include "tasks/Task.h" namespace Meta { class ParsingValidator : public Net::Validator { public: /* con/des */ ParsingValidator(BaseEntity* entity) : m_entity(entity) {}; virtual ~ParsingValidator() = default; public: /* methods */ bool init(QNetworkRequest&) override { m_data.clear(); return true; } bool write(QByteArray& data) override { this->m_data.append(data); return true; } bool abort() override { m_data.clear(); return true; } bool validate(QNetworkReply&) override { auto fname = m_entity->localFilename(); try { auto doc = Json::requireDocument(m_data, fname); auto obj = Json::requireObject(doc, fname); m_entity->parse(obj); return true; } catch (const Exception& e) { qWarning() << "Unable to parse response:" << e.cause(); return false; } } private: /* data */ QByteArray m_data; BaseEntity* m_entity; }; QUrl BaseEntity::url() const { auto s = APPLICATION->settings(); QString metaOverride = s->get("MetaURLOverride").toString(); if (metaOverride.isEmpty()) { return QUrl(BuildConfig.META_URL).resolved(localFilename()); } return QUrl(metaOverride).resolved(localFilename()); } Task::Ptr BaseEntity::loadTask(Net::Mode mode, bool forceReload) { if (m_task && m_task->isRunning()) { return m_task; } m_task.reset(new BaseEntityLoadTask(this, mode, forceReload)); return m_task; } bool BaseEntity::isLoaded() const { // consider it loaded only if the main hash is either empty and was remote loadded or the hashes match and was loaded return m_sha256.isEmpty() ? m_load_status == LoadStatus::Remote : m_load_status != LoadStatus::NotLoaded && m_sha256 == m_file_sha256; } void BaseEntity::setSha256(QString sha256) { m_sha256 = sha256; } BaseEntity::LoadStatus BaseEntity::status() const { return m_load_status; } BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode, bool forceReload) : m_entity(parent), m_mode(mode), m_force_reload(forceReload) {} void BaseEntityLoadTask::executeTask() { const QString fname = QDir("meta").absoluteFilePath(m_entity->localFilename()); auto hashMatches = false; // the file exists on disk try to load it if (QFile::exists(fname)) { try { QByteArray fileData; // read local file if nothing is loaded yet if (m_entity->m_load_status == BaseEntity::LoadStatus::NotLoaded || m_entity->m_file_sha256.isEmpty()) { setStatus(tr("Loading local file")); fileData = FS::read(fname); m_entity->m_file_sha256 = Hashing::hash(fileData, Hashing::Algorithm::Sha256); } // on online the hash needs to match const auto& expected = m_entity->m_sha256; const auto& actual = m_entity->m_file_sha256; hashMatches = expected == actual; if (m_mode == Net::Mode::Online && !m_entity->m_sha256.isEmpty() && !hashMatches) { throw Exception(QString("Checksum mismatch, expected sha256: %1, got: %2").arg(expected, actual)); } // load local file if (m_entity->m_load_status == BaseEntity::LoadStatus::NotLoaded) { auto doc = Json::requireDocument(fileData, fname); auto obj = Json::requireObject(doc, fname); m_entity->parse(obj); m_entity->m_load_status = BaseEntity::LoadStatus::Local; } } catch (const Exception& e) { qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause()); // just make sure it's gone and we never consider it again. FS::deletePath(fname); m_entity->m_load_status = BaseEntity::LoadStatus::NotLoaded; } } // if we need remote update, run the update task auto wasLoadedOffline = m_entity->m_load_status != BaseEntity::LoadStatus::NotLoaded && m_mode == Net::Mode::Offline; // if has is not present allways fetch from remote(e.g. the main index file), else only fetch if hash doesn't match auto wasLoadedRemote = m_entity->m_sha256.isEmpty() ? m_entity->m_load_status == BaseEntity::LoadStatus::Remote : hashMatches; if (wasLoadedOffline || (wasLoadedRemote && !m_force_reload)) { emitSucceeded(); return; } m_task.reset(new NetJob(QObject::tr("Download of meta file %1").arg(m_entity->localFilename()), APPLICATION->network())); auto url = m_entity->url(); auto entry = APPLICATION->metacache()->resolveEntry("meta", m_entity->localFilename()); if (m_force_reload) { // clear validators so manual refreshes fetch a fresh body entry->setETag({}); entry->setRemoteChangedTimestamp({}); } entry->setStale(true); auto dl = Net::ApiDownload::makeCached(url, entry); /* * The validator parses the file and loads it into the object. * If that fails, the file is not written to storage. */ if (!m_entity->m_sha256.isEmpty()) dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha256, m_entity->m_sha256)); dl->addValidator(new ParsingValidator(m_entity)); m_task->addNetAction(dl); m_task->setAskRetry(false); connect(m_task.get(), &Task::failed, this, &BaseEntityLoadTask::emitFailed); connect(m_task.get(), &Task::succeeded, this, &BaseEntityLoadTask::emitSucceeded); connect(m_task.get(), &Task::succeeded, this, [this]() { m_entity->m_load_status = BaseEntity::LoadStatus::Remote; m_entity->m_file_sha256 = m_entity->m_sha256; }); connect(m_task.get(), &Task::progress, this, &Task::setProgress); connect(m_task.get(), &Task::stepProgress, this, &BaseEntityLoadTask::propagateStepProgress); connect(m_task.get(), &Task::status, this, &Task::setStatus); connect(m_task.get(), &Task::details, this, &Task::setDetails); m_task->start(); } bool BaseEntityLoadTask::canAbort() const { return m_task ? m_task->canAbort() : false; } bool BaseEntityLoadTask::abort() { if (m_task) { Task::abort(); return m_task->abort(); } return Task::abort(); } } // namespace Meta PrismLauncher-11.0.3/launcher/meta/JsonFormat.h0000644000175100017510000000372415224505336020753 0ustar runnerrunner/* Copyright 2015-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 #include #include "Exception.h" namespace Meta { class Index; class Version; class VersionList; enum class MetadataVersion { Invalid = -1, InitialRelease = 1 }; class ParseException : public Exception { public: using Exception::Exception; }; struct Require { bool operator==(const Require& rhs) const { return uid == rhs.uid; } bool operator<(const Require& rhs) const { return uid < rhs.uid; } bool deepEquals(const Require& rhs) const { return uid == rhs.uid && equalsVersion == rhs.equalsVersion && suggests == rhs.suggests; } QString uid; QString equalsVersion; QString suggests; }; using RequireSet = std::set; void parseIndex(const QJsonObject& obj, Index* ptr); void parseVersion(const QJsonObject& obj, Version* ptr); void parseVersionList(const QJsonObject& obj, VersionList* ptr); MetadataVersion parseFormatVersion(const QJsonObject& obj, bool required = true); void serializeFormatVersion(QJsonObject& obj, MetadataVersion version); // FIXME: this has a different shape than the others...FIX IT!? void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char* keyName = "requires"); void serializeRequires(QJsonObject& objOut, RequireSet* ptr, const char* keyName = "requires"); MetadataVersion currentFormatVersion(); } // namespace Meta Q_DECLARE_METATYPE(std::set) PrismLauncher-11.0.3/launcher/meta/Index.cpp0000644000175100017510000001136215224505336020270 0ustar runnerrunner/* Copyright 2015-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 "Index.h" #include "JsonFormat.h" #include "QObjectPtr.h" #include "VersionList.h" #include "meta/BaseEntity.h" #include "tasks/SequentialTask.h" namespace Meta { Index::Index(QObject* parent) : QAbstractListModel(parent) {} Index::Index(const QList& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists) { for (int i = 0; i < m_lists.size(); ++i) { m_uids.insert(m_lists.at(i)->uid(), m_lists.at(i)); connectVersionList(i, m_lists.at(i)); } } QVariant Index::data(const QModelIndex& index, int role) const { if (index.parent().isValid() || index.row() < 0 || index.row() >= m_lists.size()) { return QVariant(); } VersionList::Ptr list = m_lists.at(index.row()); switch (role) { case Qt::DisplayRole: if (index.column() == 0) { return list->humanReadable(); } else { break; } case UidRole: return list->uid(); case NameRole: return list->name(); case ListPtrRole: return QVariant::fromValue(list); } return QVariant(); } int Index::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_lists.size(); } int Index::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } QVariant Index::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0) { return tr("Name"); } else { return QVariant(); } } bool Index::hasUid(const QString& uid) const { return m_uids.contains(uid); } VersionList::Ptr Index::get(const QString& uid) { VersionList::Ptr out = m_uids.value(uid, nullptr); if (!out) { out = std::make_shared(uid); m_uids[uid] = out; m_lists.append(out); } return out; } Version::Ptr Index::get(const QString& uid, const QString& version) { auto list = get(uid); return list->getVersion(version); } void Index::parse(const QJsonObject& obj) { parseIndex(obj, this); } void Index::merge(const std::shared_ptr& other) { const QList lists = other->m_lists; // initial load, no need to merge if (m_lists.isEmpty()) { beginResetModel(); m_lists = lists; for (int i = 0; i < lists.size(); ++i) { m_uids.insert(lists.at(i)->uid(), lists.at(i)); connectVersionList(i, lists.at(i)); } endResetModel(); } else { for (const VersionList::Ptr& list : lists) { if (m_uids.contains(list->uid())) { m_uids[list->uid()]->mergeFromIndex(list); } else { beginInsertRows(QModelIndex(), m_lists.size(), m_lists.size()); connectVersionList(m_lists.size(), list); m_lists.append(list); m_uids.insert(list->uid(), list); endInsertRows(); } } } } void Index::connectVersionList(const int row, const VersionList::Ptr& list) { connect(list.get(), &VersionList::nameChanged, this, [this, row] { emit dataChanged(index(row), index(row), { Qt::DisplayRole }); }); } Task::Ptr Index::loadVersion(const QString& uid, const QString& version, Net::Mode mode, bool force) { if (mode == Net::Mode::Offline) { return get(uid, version)->loadTask(mode); } auto versionList = get(uid); auto loadTask = makeShared(tr("Load meta for %1:%2", "This is for the task name that loads the meta index.").arg(uid, version)); if (status() != BaseEntity::LoadStatus::Remote || force) { loadTask->addTask(this->loadTask(mode)); } loadTask->addTask(versionList->loadTask(mode)); loadTask->addTask(versionList->getVersion(version)->loadTask(mode)); return loadTask; } Version::Ptr Index::getLoadedVersion(const QString& uid, const QString& version) { QEventLoop ev; auto task = loadVersion(uid, version); connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); task->start(); ev.exec(); return get(uid, version); } } // namespace Meta PrismLauncher-11.0.3/launcher/meta/Version.h0000644000175100017510000000531215224505336020311 0ustar runnerrunner/* Copyright 2015-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 "../Version.h" #include "BaseVersion.h" #include #include #include #include #include "minecraft/VersionFile.h" #include "BaseEntity.h" #include "JsonFormat.h" namespace Meta { class Version : public QObject, public BaseVersion, public BaseEntity { Q_OBJECT public: using Ptr = std::shared_ptr; explicit Version(const QString& uid, const QString& version); virtual ~Version() = default; QString descriptor() const override; QString name() const override; QString typeString() const override; QString uid() const { return m_uid; } QString version() const { return m_version; } QString type() const { return m_type; } QDateTime time() const; qint64 rawTime() const { return m_time; } const Meta::RequireSet& requiredSet() const { return m_requires; } VersionFilePtr data() const { return m_data; } bool isRecommended() const { return m_recommended; } bool isLoaded() const { return m_data != nullptr && BaseEntity::isLoaded(); } void merge(const Version::Ptr& other); void mergeFromList(const Version::Ptr& other); void parse(const QJsonObject& obj) override; QString localFilename() const override; ::Version toComparableVersion() const; public: // for usage by format parsers only void setType(const QString& type); void setTime(qint64 time); void setRequires(const Meta::RequireSet& reqs, const Meta::RequireSet& conflicts); void setVolatile(bool volatile_); void setRecommended(bool recommended); void setProvidesRecommendations(); void setData(const VersionFilePtr& data); signals: void typeChanged(); void timeChanged(); void requiresChanged(); private: bool m_providesRecommendations = false; bool m_recommended = false; QString m_name; QString m_uid; QString m_version; QString m_type; qint64 m_time = 0; Meta::RequireSet m_requires; Meta::RequireSet m_conflicts; bool m_volatile = false; VersionFilePtr m_data; }; } // namespace Meta Q_DECLARE_METATYPE(Meta::Version::Ptr) PrismLauncher-11.0.3/launcher/meta/VersionList.cpp0000644000175100017510000002371215224505336021504 0ustar runnerrunner/* Copyright 2015-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 "VersionList.h" #include #include #include "Application.h" #include "Index.h" #include "JsonFormat.h" #include "Version.h" #include "meta/BaseEntity.h" #include "net/Mode.h" #include "tasks/SequentialTask.h" namespace Meta { VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList(parent), m_uid(uid) { setObjectName("Version list: " + uid); } Task::Ptr VersionList::getLoadTask(bool forceReload) { auto loadTask = makeShared(tr("Load meta for %1", "This is for the task name that loads the meta index.").arg(m_uid)); loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online, forceReload)); loadTask->addTask(this->loadTask(Net::Mode::Online, forceReload)); return loadTask; } bool VersionList::isLoaded() { return BaseEntity::isLoaded(); } const BaseVersion::Ptr VersionList::at(int i) const { return m_versions.at(i); } int VersionList::count() const { return m_versions.size(); } void VersionList::sortVersions() { beginResetModel(); std::sort(m_versions.begin(), m_versions.end(), [](const Version::Ptr& a, const Version::Ptr& b) { return *a.get() < *b.get(); }); endResetModel(); } QVariant VersionList::data(const QModelIndex& index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= m_versions.size() || index.parent().isValid()) { return QVariant(); } Version::Ptr version = m_versions.at(index.row()); switch (role) { case VersionPointerRole: return QVariant::fromValue(std::dynamic_pointer_cast(version)); case VersionRole: case VersionIdRole: return version->version(); case ParentVersionRole: { // FIXME: HACK: this should be generic and be replaced by something else. Anything that is a hard 'equals' dep is a 'parent // uid'. auto& reqs = version->requiredSet(); auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Require& req) { return req.uid == "net.minecraft"; }); if (iter != reqs.end()) { return (*iter).equalsVersion; } return QVariant(); } case TypeRole: return version->type(); case UidRole: return version->uid(); case TimeRole: return version->time(); case RequiresRole: return QVariant::fromValue(version->requiredSet()); case SortRole: return version->rawTime(); case VersionPtrRole: return QVariant::fromValue(version); case RecommendedRole: return version->isRecommended() || m_externalRecommendsVersions.contains(version->version()); case JavaMajorRole: { auto major = version->version(); if (major.startsWith("java")) { major = "Java " + major.mid(4); } return major; } // FIXME: this should be determined in whatever view/proxy is used... // case LatestRole: return version == getLatestStable(); default: return QVariant(); } } BaseVersionList::RoleList VersionList::providesRoles() const { return m_provided_roles; } void VersionList::setProvidedRoles(RoleList roles) { m_provided_roles = roles; }; QHash VersionList::roleNames() const { QHash roles = BaseVersionList::roleNames(); roles.insert(UidRole, "uid"); roles.insert(TimeRole, "time"); roles.insert(SortRole, "sort"); roles.insert(RequiresRole, "requires"); return roles; } QString VersionList::localFilename() const { return m_uid + "/index.json"; } QString VersionList::humanReadable() const { return m_name.isEmpty() ? m_uid : m_name; } Version::Ptr VersionList::getVersion(const QString& version) { Version::Ptr out = m_lookup.value(version, nullptr); if (!out) { out = std::make_shared(m_uid, version); m_lookup[version] = out; setupAddedVersion(m_versions.size(), out); m_versions.append(out); } return out; } bool VersionList::hasVersion(QString version) const { auto ver = std::find_if(m_versions.constBegin(), m_versions.constEnd(), [version](Meta::Version::Ptr const& a) { return a->version() == version; }); return (ver != m_versions.constEnd()); } void VersionList::setName(const QString& name) { m_name = name; emit nameChanged(name); } void VersionList::setVersions(const QList& versions) { beginResetModel(); m_versions = versions; std::sort(m_versions.begin(), m_versions.end(), [](const Version::Ptr& a, const Version::Ptr& b) { return a->rawTime() > b->rawTime(); }); for (int i = 0; i < m_versions.size(); ++i) { m_lookup.insert(m_versions.at(i)->version(), m_versions.at(i)); setupAddedVersion(i, m_versions.at(i)); } // FIXME: this is dumb, we have 'recommended' as part of the metadata already... auto recommendedIt = std::find_if(m_versions.constBegin(), m_versions.constEnd(), [](const Version::Ptr& ptr) { return ptr->type() == "release"; }); m_recommended = recommendedIt == m_versions.constEnd() ? nullptr : *recommendedIt; endResetModel(); } void VersionList::parse(const QJsonObject& obj) { parseVersionList(obj, this); } void VersionList::addExternalRecommends(const QStringList& recommends) { m_externalRecommendsVersions.append(recommends); } void VersionList::clearExternalRecommends() { m_externalRecommendsVersions.clear(); } // FIXME: this is dumb, we have 'recommended' as part of the metadata already... static const Meta::Version::Ptr& getBetterVersion(const Meta::Version::Ptr& a, const Meta::Version::Ptr& b) { if (!a) return b; if (!b) return a; if (a->type() == b->type()) { // newer of same type wins return (a->rawTime() > b->rawTime() ? a : b); } // 'release' type wins return (a->type() == "release" ? a : b); } void VersionList::mergeFromIndex(const VersionList::Ptr& other) { if (m_name != other->m_name) { setName(other->m_name); } if (!other->m_sha256.isEmpty()) { m_sha256 = other->m_sha256; } } void VersionList::merge(const VersionList::Ptr& other) { if (m_name != other->m_name) { setName(other->m_name); } if (!other->m_sha256.isEmpty()) { m_sha256 = other->m_sha256; } // TODO: do not reset the whole model. maybe? beginResetModel(); if (other->m_versions.isEmpty()) { qWarning() << "Empty list loaded ..."; } for (auto version : other->m_versions) { // we already have the version. merge the contents if (m_lookup.contains(version->version())) { auto existing = m_lookup.value(version->version()); existing->mergeFromList(version); version = existing; } else { m_lookup.insert(version->version(), version); // connect it. setupAddedVersion(m_versions.size(), version); m_versions.append(version); } m_recommended = getBetterVersion(m_recommended, version); } endResetModel(); } void VersionList::setupAddedVersion(const int row, const Version::Ptr& version) { disconnect(version.get(), &Version::requiresChanged, this, nullptr); disconnect(version.get(), &Version::timeChanged, this, nullptr); disconnect(version.get(), &Version::typeChanged, this, nullptr); connect(version.get(), &Version::requiresChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QList() << RequiresRole); }); connect(version.get(), &Version::timeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TimeRole, SortRole }); }); connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TypeRole }); }); } BaseVersion::Ptr VersionList::getRecommended() const { return m_recommended; } void VersionList::waitToLoad() { if (isLoaded()) return; QEventLoop ev; auto task = getLoadTask(); connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); task->start(); ev.exec(); } Version::Ptr VersionList::getRecommendedForParent(const QString& uid, const QString& version) { auto foundExplicit = std::find_if(m_versions.begin(), m_versions.end(), [uid, version](Version::Ptr ver) -> bool { auto& reqs = ver->requiredSet(); auto parentReq = std::find_if(reqs.begin(), reqs.end(), [uid, version](const Require& req) -> bool { return req.uid == uid && req.equalsVersion == version; }); return parentReq != reqs.end() && ver->isRecommended(); }); if (foundExplicit != m_versions.end()) { return *foundExplicit; } return nullptr; } Version::Ptr VersionList::getLatestForParent(const QString& uid, const QString& version) { Version::Ptr latestCompat = nullptr; for (auto ver : m_versions) { auto& reqs = ver->requiredSet(); auto parentReq = std::find_if(reqs.begin(), reqs.end(), [uid, version](const Require& req) -> bool { return req.uid == uid && req.equalsVersion == version; }); if (parentReq != reqs.end()) { latestCompat = getBetterVersion(latestCompat, ver); } } return latestCompat; } } // namespace Meta PrismLauncher-11.0.3/launcher/meta/BaseEntity.h0000644000175100017510000000374615224505336020744 0ustar runnerrunner/* Copyright 2015-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 #include #include "net/Mode.h" #include "net/NetJob.h" #include "tasks/Task.h" namespace Meta { class BaseEntityLoadTask; class BaseEntity { friend BaseEntityLoadTask; public: /* types */ using Ptr = std::shared_ptr; enum class LoadStatus { NotLoaded, Local, Remote }; public: virtual ~BaseEntity() = default; virtual QString localFilename() const = 0; virtual QUrl url() const; bool isLoaded() const; LoadStatus status() const; /* for parsers */ void setSha256(QString sha256); virtual void parse(const QJsonObject& obj) = 0; [[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online, bool forceReload = false); protected: QString m_sha256; // the expected sha256 QString m_file_sha256; // the file sha256 private: LoadStatus m_load_status = LoadStatus::NotLoaded; Task::Ptr m_task; }; class BaseEntityLoadTask : public Task { Q_OBJECT public: explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode, bool forceReload); ~BaseEntityLoadTask() override = default; virtual void executeTask() override; virtual bool canAbort() const override; virtual bool abort() override; private: BaseEntity* m_entity; Net::Mode m_mode; bool m_force_reload = false; NetJob::Ptr m_task; }; } // namespace Meta PrismLauncher-11.0.3/launcher/meta/Version.cpp0000644000175100017510000000567515224505336020660 0ustar runnerrunner/* Copyright 2015-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 "Version.h" #include #include "JsonFormat.h" Meta::Version::Version(const QString& uid, const QString& version) : BaseVersion(), m_uid(uid), m_version(version) {} QString Meta::Version::descriptor() const { return m_version; } QString Meta::Version::name() const { if (m_data) return m_data->name; return m_uid; } QString Meta::Version::typeString() const { return m_type; } QDateTime Meta::Version::time() const { return QDateTime::fromMSecsSinceEpoch(m_time * 1000, Qt::UTC); } void Meta::Version::parse(const QJsonObject& obj) { parseVersion(obj, this); } void Meta::Version::mergeFromList(const Meta::Version::Ptr& other) { if (other->m_providesRecommendations) { if (m_recommended != other->m_recommended) { setRecommended(other->m_recommended); } } if (m_type != other->m_type) { setType(other->m_type); } if (m_time != other->m_time) { setTime(other->m_time); } if (m_requires != other->m_requires) { m_requires = other->m_requires; } if (m_conflicts != other->m_conflicts) { m_conflicts = other->m_conflicts; } if (m_volatile != other->m_volatile) { setVolatile(other->m_volatile); } if (!other->m_sha256.isEmpty()) { m_sha256 = other->m_sha256; } } void Meta::Version::merge(const Version::Ptr& other) { mergeFromList(other); if (other->m_data) { setData(other->m_data); } } QString Meta::Version::localFilename() const { return m_uid + '/' + m_version + ".json"; } ::Version Meta::Version::toComparableVersion() const { return { descriptor() }; } void Meta::Version::setType(const QString& type) { m_type = type; emit typeChanged(); } void Meta::Version::setTime(const qint64 time) { m_time = time; emit timeChanged(); } void Meta::Version::setRequires(const Meta::RequireSet& reqs, const Meta::RequireSet& conflicts) { m_requires = reqs; m_conflicts = conflicts; emit requiresChanged(); } void Meta::Version::setVolatile(bool volatile_) { m_volatile = volatile_; } void Meta::Version::setData(const VersionFilePtr& data) { m_data = data; } void Meta::Version::setProvidesRecommendations() { m_providesRecommendations = true; } void Meta::Version::setRecommended(bool recommended) { m_recommended = recommended; } PrismLauncher-11.0.3/launcher/console/0000755000175100017510000000000015224505336017226 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/console/WindowsConsole.h0000644000175100017510000000231515224505336022355 0ustar runnerrunner// // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include namespace console { void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr); bool AttachWindowsConsole(); std::error_code EnableAnsiSupport(); void FreeWindowsConsole(); class WindowsConsoleGuard { public: WindowsConsoleGuard(); ~WindowsConsoleGuard(); private: bool m_consoleAttached; }; } // namespace console PrismLauncher-11.0.3/launcher/console/WindowsConsole.cpp0000644000175100017510000001470615224505336022717 0ustar runnerrunner/* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "WindowsConsole.h" #include #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include #include #include #include #include #include #include namespace console { void RedirectHandle(DWORD handle, FILE* stream, const char* mode) { HANDLE stdHandle = GetStdHandle(handle); if (stdHandle != INVALID_HANDLE_VALUE) { int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); if (fileDescriptor != -1) { FILE* file = _fdopen(fileDescriptor, mode); if (file != NULL) { int dup2Result = _dup2(_fileno(file), _fileno(stream)); if (dup2Result == 0) { setvbuf(stream, NULL, _IONBF, 0); } } } } } // taken from https://stackoverflow.com/a/25927081 // getting a proper output to console with redirection support on windows is apparently hell void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr) { // Re-initialize the C runtime "FILE" handles with clean handles bound to "nul". We do this because it has been // observed that the file number of our standard handle file objects can be assigned internally to a value of -2 // when not bound to a valid target, which represents some kind of unknown internal invalid state. In this state our // call to "_dup2" fails, as it specifically tests to ensure that the target file number isn't equal to this value // before allowing the operation to continue. We can resolve this issue by first "re-opening" the target files to // use the "nul" device, which will place them into a valid state, after which we can redirect them to our target // using the "_dup2" function. if (bindStdIn) { FILE* dummyFile; freopen_s(&dummyFile, "nul", "r", stdin); } if (bindStdOut) { FILE* dummyFile; freopen_s(&dummyFile, "nul", "w", stdout); } if (bindStdErr) { FILE* dummyFile; freopen_s(&dummyFile, "nul", "w", stderr); } // Redirect unbuffered stdin from the current standard input handle if (bindStdIn) { RedirectHandle(STD_INPUT_HANDLE, stdin, "r"); } // Redirect unbuffered stdout to the current standard output handle if (bindStdOut) { RedirectHandle(STD_OUTPUT_HANDLE, stdout, "w"); } // Redirect unbuffered stderr to the current standard error handle if (bindStdErr) { RedirectHandle(STD_ERROR_HANDLE, stderr, "w"); } // Clear the error state for each of the C++ standard stream objects. We need to do this, as attempts to access the // standard streams before they refer to a valid target will cause the iostream objects to enter an error state. In // versions of Visual Studio after 2005, this seems to always occur during startup regardless of whether anything // has been read from or written to the targets or not. if (bindStdIn) { std::wcin.clear(); std::cin.clear(); } if (bindStdOut) { std::wcout.clear(); std::cout.clear(); } if (bindStdErr) { std::wcerr.clear(); std::cerr.clear(); } } bool AttachWindowsConsole() { auto stdinType = GetFileType(GetStdHandle(STD_INPUT_HANDLE)); auto stdoutType = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)); auto stderrType = GetFileType(GetStdHandle(STD_ERROR_HANDLE)); bool bindStdIn = false; bool bindStdOut = false; bool bindStdErr = false; if (stdinType == FILE_TYPE_CHAR || stdinType == FILE_TYPE_UNKNOWN) { bindStdIn = true; } if (stdoutType == FILE_TYPE_CHAR || stdoutType == FILE_TYPE_UNKNOWN) { bindStdOut = true; } if (stderrType == FILE_TYPE_CHAR || stderrType == FILE_TYPE_UNKNOWN) { bindStdErr = true; } if (AttachConsole(ATTACH_PARENT_PROCESS)) { BindCrtHandlesToStdHandles(bindStdIn, bindStdOut, bindStdErr); return true; } return false; } std::error_code EnableAnsiSupport() { // ref: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew // Using `CreateFileW("CONOUT$", ...)` to retrieve the console handle works correctly even if STDOUT and/or STDERR are redirected HANDLE console_handle = CreateFileW(L"CONOUT$", FILE_GENERIC_READ | FILE_GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0); if (console_handle == INVALID_HANDLE_VALUE) { return std::error_code(GetLastError(), std::system_category()); } // ref: https://docs.microsoft.com/en-us/windows/console/getconsolemode DWORD console_mode; if (0 == GetConsoleMode(console_handle, &console_mode)) { return std::error_code(GetLastError(), std::system_category()); } // VT processing not already enabled? if ((console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0) { // https://docs.microsoft.com/en-us/windows/console/setconsolemode if (0 == SetConsoleMode(console_handle, console_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) { return std::error_code(GetLastError(), std::system_category()); } } return {}; } void FreeWindowsConsole() { fclose(stdout); fclose(stdin); fclose(stderr); FreeConsole(); } WindowsConsoleGuard::WindowsConsoleGuard() : m_consoleAttached(false) { if (console::AttachWindowsConsole()) { m_consoleAttached = true; if (auto err = console::EnableAnsiSupport(); err) { std::cout << "Error setting up ansi console" << err.message() << std::endl; } } } WindowsConsoleGuard::~WindowsConsoleGuard() { // Detach from Windows console if (m_consoleAttached) { console::FreeWindowsConsole(); } } } // namespace console PrismLauncher-11.0.3/launcher/console/Console.h0000644000175100017510000000107515224505336021004 0ustar runnerrunner#pragma once #include #include #if defined Q_OS_WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #else #include #include #endif namespace console { inline bool isConsole() { #if defined Q_OS_WIN32 DWORD procIDs[2]; DWORD maxCount = 2; DWORD result = GetConsoleProcessList((LPDWORD)procIDs, maxCount); return result > 1; #else if (isatty(fileno(stdout))) { return true; } return false; #endif } } // namespace console PrismLauncher-11.0.3/launcher/BaseVersionList.h0000644000175100017510000001047215224505336021015 0ustar runnerrunner/* 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 #include #include #include "BaseVersion.h" #include "QObjectPtr.h" #include "tasks/Task.h" /*! * \brief Class that each instance type's version list derives from. * Version lists are the lists that keep track of the available game versions * for that instance. This list will not be loaded on startup. It will be loaded * when the list's load function is called. Before using the version list, you * should check to see if it has been loaded yet and if not, load the list. * * Note that this class also inherits from QAbstractListModel. Methods from that * class determine how this version list shows up in a list view. Said methods * all have a default implementation, but they can be overridden by plugins to * change the behavior of the list. */ class BaseVersionList : public QAbstractListModel { Q_OBJECT public: enum ModelRoles { VersionPointerRole = Qt::UserRole, VersionRole, VersionIdRole, ParentVersionRole, RecommendedRole, LatestRole, TypeRole, BranchRole, PathRole, JavaNameRole, JavaMajorRole, CPUArchitectureRole, SortRole }; using RoleList = QList; explicit BaseVersionList(QObject* parent = 0); /*! * \brief Gets a task that will reload the version list. * Simply execute the task to load the list. * The task returned by this function should reset the model when it's done. * \return A pointer to a task that reloads the version list. */ virtual Task::Ptr getLoadTask(bool forceReload = false) = 0; //! Checks whether or not the list is loaded. If this returns false, the list should be // loaded. virtual bool isLoaded() = 0; //! Gets the version at the given index. virtual const BaseVersion::Ptr at(int i) const = 0; //! Returns the number of versions in the list. virtual int count() const = 0; //////// List Model Functions //////// QVariant data(const QModelIndex& index, int role) const override; int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; QHash roleNames() const override; //! which roles are provided by this version list? virtual RoleList providesRoles() const; /*! * \brief Finds a version by its descriptor. * \param descriptor The descriptor of the version to find. * \return A const pointer to the version with the given descriptor. NULL if * one doesn't exist. */ virtual BaseVersion::Ptr findVersion(const QString& descriptor); /*! * \brief Gets the recommended version from this list * If the list doesn't support recommended versions, this works exactly as getLatestStable */ virtual BaseVersion::Ptr getRecommended() const; /*! * Sorts the version list. */ virtual void sortVersions() = 0; protected slots: /*! * Updates this list with the given list of versions. * This is done by copying each version in the given list and inserting it * into this one. * We need to do this so that we can set the parents of the versions are set to this * version list. This can't be done in the load task, because the versions the load * task creates are on the load task's thread and Qt won't allow their parents * to be set to something created on another thread. * To get around that problem, we invoke this method on the GUI thread, which * then copies the versions and sets their parents correctly. * \param versions List of versions whose parents should be set. */ virtual void updateListData(QList versions) = 0; }; PrismLauncher-11.0.3/launcher/InstanceList.h0000644000175100017510000001631115224505336020337 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include "BaseInstance.h" class QFileSystemWatcher; class InstanceTask; struct InstanceName; using InstanceId = QString; using GroupId = QString; using InstanceLocator = std::pair; enum class InstCreateError { NoCreateError = 0, NoSuchVersion, UnknownCreateError, InstExists, CantCreateDir }; enum class GroupsState { NotLoaded, Steady, Dirty }; struct TrashShortcutItem { ShortcutData data; QString trashPath; }; struct TrashHistoryItem { QString id; QString path; QString trashPath; QString groupName; QList shortcuts; }; class InstanceList : public QAbstractListModel { Q_OBJECT public: explicit InstanceList(SettingsObject* settings, const QString& instDir, QObject* parent = 0); virtual ~InstanceList(); public: QModelIndex index(int row, int column = 0, const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role) const override; Qt::ItemFlags flags(const QModelIndex& index) const override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; enum AdditionalRoles { GroupRole = Qt::UserRole, InstancePointerRole = 0x34B1CB48, ///< Return pointer to real instance InstanceIDRole = 0x34B1CB49 ///< Return id if the instance }; /*! * \brief Error codes returned by functions in the InstanceList class. * NoError Indicates that no error occurred. * UnknownError indicates that an unspecified error occurred. */ enum InstListError { NoError = 0, UnknownError }; BaseInstance* at(int i) const { return m_instances.at(i).get(); } int count() const { return static_cast(m_instances.size()); } InstListError loadList(); void saveNow(); /* O(n) */ BaseInstance* getInstanceById(QString id) const; /* O(n) */ BaseInstance* getInstanceByManagedName(const QString& managed_name) const; QModelIndex getInstanceIndexById(const QString& id) const; QStringList getGroups(); bool isGroupCollapsed(const QString& groupName); GroupId getInstanceGroup(const InstanceId& id) const; void setInstanceGroup(const InstanceId& id, GroupId name); void deleteGroup(const GroupId& name); void renameGroup(const GroupId& src, const GroupId& dst); bool trashInstance(const InstanceId& id); bool trashedSomething() const; bool undoTrashInstance(); void deleteInstance(const InstanceId& id); // Wrap an instance creation task in some more task machinery and make it ready to be used Task* wrapInstanceTask(InstanceTask* task); /** * Create a new empty staging area for instance creation and @return a path/key top commit it later. * Used by instance manipulation tasks. */ QString getStagedInstancePath(); /** * Commit the staging area given by @keyPath to the provider - used when creation succeeds. * Used by instance manipulation tasks. * should_override is used when another similar instance already exists, and we want to override it * - for instance, when updating it. */ bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, QString groupName, const InstanceTask&); /** * Destroy a previously created staging area given by @keyPath - used when creation fails. * Used by instance manipulation tasks. */ bool destroyStagingPath(const QString& keyPath); int getTotalPlayTime(); Qt::DropActions supportedDragActions() const override; Qt::DropActions supportedDropActions() const override; bool canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const override; bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; QStringList mimeTypes() const override; QMimeData* mimeData(const QModelIndexList& indexes) const override; QStringList getLinkedInstancesById(const QString& id) const; signals: void dataIsInvalid(); void instancesChanged(); void instanceSelectRequest(QString instanceId); void groupsChanged(QSet groups); public slots: void on_InstFolderChanged(const Setting& setting, QVariant value); void on_GroupStateChanged(const QString& group, bool collapsed); private slots: void propertiesChanged(BaseInstance* inst); void providerUpdated(); void instanceDirContentsChanged(const QString& path); private: int getInstIndex(BaseInstance* inst) const; void updateTotalPlayTime(); void suspendWatch(); void resumeWatch(); void add(std::vector>& list); void loadGroupList(); void saveGroupList(); QList discoverInstances(); std::unique_ptr loadInstance(const InstanceId& id); void increaseGroupCount(const QString& group); void decreaseGroupCount(const QString& group); private: int m_watchLevel = 0; int totalPlayTime = 0; bool m_dirty = false; std::vector> m_instances; // id -> refs QMap m_groupNameCache; SettingsObject* m_globalSettings; QString m_instDir; QFileSystemWatcher* m_watcher; // FIXME: this is so inefficient that looking at it is almost painful. QSet m_collapsedGroups; QMap m_instanceGroupIndex; QSet instanceSet; bool m_groupsLoaded = false; bool m_instancesProbed = false; QStack m_trashHistory; }; PrismLauncher-11.0.3/launcher/VersionProxyModel.cpp0000644000175100017510000003562415224505336021752 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "VersionProxyModel.h" #include #include #include #include #include class VersionFilterModel : public QSortFilterProxyModel { Q_OBJECT public: VersionFilterModel(VersionProxyModel* parent) : QSortFilterProxyModel(parent) { m_parent = parent; setSortRole(BaseVersionList::SortRole); sort(0, Qt::DescendingOrder); } bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { const auto& filters = m_parent->filters(); const QString& search = m_parent->search(); const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); if (!search.isEmpty() && !sourceModel()->data(idx, BaseVersionList::VersionRole).toString().contains(search, Qt::CaseInsensitive)) return false; for (auto it = filters.begin(); it != filters.end(); ++it) { auto data = sourceModel()->data(idx, it.key()); auto match = data.toString(); if (!it.value()(match)) { return false; } } return true; } void filterChanged() { invalidateFilter(); } private: VersionProxyModel* m_parent; }; VersionProxyModel::VersionProxyModel(QObject* parent) : QAbstractProxyModel(parent) { filterModel = new VersionFilterModel(this); connect(filterModel, &QAbstractItemModel::dataChanged, this, &VersionProxyModel::sourceDataChanged); connect(filterModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &VersionProxyModel::sourceRowsAboutToBeInserted); connect(filterModel, &QAbstractItemModel::rowsInserted, this, &VersionProxyModel::sourceRowsInserted); connect(filterModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &VersionProxyModel::sourceRowsAboutToBeRemoved); connect(filterModel, &QAbstractItemModel::rowsRemoved, this, &VersionProxyModel::sourceRowsRemoved); // FIXME: implement when needed /* connect(replacing, &QAbstractItemModel::rowsAboutToBeMoved, this, &VersionProxyModel::sourceRowsAboutToBeMoved); connect(replacing, &QAbstractItemModel::rowsMoved, this, &VersionProxyModel::sourceRowsMoved); connect(replacing, &QAbstractItemModel::layoutAboutToBeChanged, this, &VersionProxyModel::sourceLayoutAboutToBeChanged); connect(replacing, &QAbstractItemModel::layoutChanged, this, &VersionProxyModel::sourceLayoutChanged); */ connect(filterModel, &QAbstractItemModel::modelAboutToBeReset, this, &VersionProxyModel::sourceAboutToBeReset); connect(filterModel, &QAbstractItemModel::modelReset, this, &VersionProxyModel::sourceReset); QAbstractProxyModel::setSourceModel(filterModel); } QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation, int role) const { if (section < 0 || section >= m_columns.size()) return QVariant(); if (orientation != Qt::Horizontal) return QVariant(); auto column = m_columns[section]; if (role == Qt::DisplayRole) { switch (column) { case Name: return tr("Version"); case ParentVersion: return tr("Minecraft"); // FIXME: this should come from metadata case Branch: return tr("Branch"); case Type: return tr("Type"); case CPUArchitecture: return tr("Architecture"); case Path: return tr("Path"); case JavaName: return tr("Java Name"); case JavaMajor: return tr("Major Version"); case Time: return tr("Released"); } } else if (role == Qt::ToolTipRole) { switch (column) { case Name: return tr("The name of the version."); case ParentVersion: return tr("Minecraft version"); // FIXME: this should come from metadata case Branch: return tr("The version's branch"); case Type: return tr("The version's type"); case CPUArchitecture: return tr("CPU Architecture"); case Path: return tr("Filesystem path to this version"); case JavaName: return tr("The alternative name of the Java version"); case JavaMajor: return tr("The Java major version"); case Time: return tr("Release date of this version"); } } return QVariant(); } QVariant VersionProxyModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } auto column = m_columns[index.column()]; auto parentIndex = mapToSource(index); switch (role) { case Qt::DisplayRole: { switch (column) { case Name: { QString version = sourceModel()->data(parentIndex, BaseVersionList::VersionRole).toString(); if (version == m_currentVersion) { return tr("%1 (installed)").arg(version); } return version; } case ParentVersion: return sourceModel()->data(parentIndex, BaseVersionList::ParentVersionRole); case Branch: return sourceModel()->data(parentIndex, BaseVersionList::BranchRole); case Type: return sourceModel()->data(parentIndex, BaseVersionList::TypeRole); case CPUArchitecture: return sourceModel()->data(parentIndex, BaseVersionList::CPUArchitectureRole); case Path: return sourceModel()->data(parentIndex, BaseVersionList::PathRole); case JavaName: return sourceModel()->data(parentIndex, BaseVersionList::JavaNameRole); case JavaMajor: return sourceModel()->data(parentIndex, BaseVersionList::JavaMajorRole); case Time: return sourceModel()->data(parentIndex, Meta::VersionList::TimeRole).toDate(); default: return QVariant(); } } case Qt::ToolTipRole: { if (column == Name && hasRecommended) { auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); if (value.toBool()) { return tr("Recommended"); } else if (hasLatest) { auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); if (latest.toBool()) { return tr("Latest"); } } } return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); } case Qt::DecorationRole: { if (column == Name && hasRecommended) { auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); if (recommenced.toBool()) { return QIcon::fromTheme("star"); } else if (hasLatest) { auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); if (latest.toBool()) { return QIcon::fromTheme("bug"); } } QPixmap pixmap; QPixmapCache::find("placeholder", &pixmap); if (!pixmap) { QPixmap px(16, 16); px.fill(Qt::transparent); QPixmapCache::insert("placeholder", px); return px; } return pixmap; } return QVariant(); } default: { if (roles.contains((BaseVersionList::ModelRoles)role)) { return sourceModel()->data(parentIndex, role); } return QVariant(); } } } QModelIndex VersionProxyModel::parent([[maybe_unused]] const QModelIndex& child) const { return QModelIndex(); } QModelIndex VersionProxyModel::mapFromSource(const QModelIndex& sourceIndex) const { if (sourceIndex.isValid()) { return index(sourceIndex.row(), 0); } return QModelIndex(); } QModelIndex VersionProxyModel::mapToSource(const QModelIndex& proxyIndex) const { if (proxyIndex.isValid()) { return sourceModel()->index(proxyIndex.row(), 0); } return QModelIndex(); } QModelIndex VersionProxyModel::index(int row, int column, const QModelIndex& parent) const { // no trees here... shoo if (parent.isValid()) { return QModelIndex(); } if (row < 0 || row >= sourceModel()->rowCount()) return QModelIndex(); if (column < 0 || column >= columnCount()) return QModelIndex(); return QAbstractItemModel::createIndex(row, column); } int VersionProxyModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_columns.size(); } int VersionProxyModel::rowCount(const QModelIndex& parent) const { if (sourceModel()) { return sourceModel()->rowCount(parent); } return 0; } void VersionProxyModel::sourceDataChanged(const QModelIndex& source_top_left, const QModelIndex& source_bottom_right) { if (source_top_left.parent() != source_bottom_right.parent()) return; // whole row is getting changed auto topLeft = createIndex(source_top_left.row(), 0); auto bottomRight = createIndex(source_bottom_right.row(), columnCount() - 1); emit dataChanged(topLeft, bottomRight); } void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) { auto replacing = dynamic_cast(replacingRaw); m_columns.clear(); if (!replacing) { roles.clear(); filterModel->setSourceModel(replacing); return; } roles = replacing->providesRoles(); if (roles.contains(BaseVersionList::VersionRole)) { m_columns.push_back(Name); } /* if(roles.contains(BaseVersionList::ParentVersionRole)) { m_columns.push_back(ParentVersion); } */ if (roles.contains(BaseVersionList::CPUArchitectureRole)) { m_columns.push_back(CPUArchitecture); } if (roles.contains(BaseVersionList::PathRole)) { m_columns.push_back(Path); } if (roles.contains(BaseVersionList::JavaNameRole)) { m_columns.push_back(JavaName); } if (roles.contains(BaseVersionList::JavaMajorRole)) { m_columns.push_back(JavaMajor); } if (roles.contains(Meta::VersionList::TimeRole)) { m_columns.push_back(Time); } if (roles.contains(BaseVersionList::BranchRole)) { m_columns.push_back(Branch); } if (roles.contains(BaseVersionList::TypeRole)) { m_columns.push_back(Type); } if (roles.contains(BaseVersionList::RecommendedRole)) { hasRecommended = true; } if (roles.contains(BaseVersionList::LatestRole)) { hasLatest = true; } filterModel->setSourceModel(replacing); } QModelIndex VersionProxyModel::getRecommended() const { if (!roles.contains(BaseVersionList::RecommendedRole)) { return index(0, 0); } int recommended = 0; for (int i = 0; i < rowCount(); i++) { auto value = sourceModel()->data(mapToSource(index(i, 0)), BaseVersionList::RecommendedRole); if (value.toBool()) { recommended = i; } } return index(recommended, 0); } QModelIndex VersionProxyModel::getVersion(const QString& version) const { int found = -1; for (int i = 0; i < rowCount(); i++) { auto value = sourceModel()->data(mapToSource(index(i, 0)), BaseVersionList::VersionRole); if (value.toString() == version) { found = i; } } if (found == -1) { return QModelIndex(); } return index(found, 0); } void VersionProxyModel::clearFilters() { m_filters.clear(); m_search.clear(); filterModel->filterChanged(); } void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filter f) { m_filters[column] = std::move(f); filterModel->filterChanged(); } void VersionProxyModel::setSearch(const QString& search) { m_search = search; filterModel->filterChanged(); } const VersionProxyModel::FilterMap& VersionProxyModel::filters() const { return m_filters; } const QString& VersionProxyModel::search() const { return m_search; } void VersionProxyModel::sourceAboutToBeReset() { beginResetModel(); } void VersionProxyModel::sourceReset() { endResetModel(); } void VersionProxyModel::sourceRowsAboutToBeInserted(const QModelIndex& parent, int first, int last) { beginInsertRows(parent, first, last); } void VersionProxyModel::sourceRowsInserted([[maybe_unused]] const QModelIndex& parent, [[maybe_unused]] int first, [[maybe_unused]] int last) { endInsertRows(); } void VersionProxyModel::sourceRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { beginRemoveRows(parent, first, last); } void VersionProxyModel::sourceRowsRemoved([[maybe_unused]] const QModelIndex& parent, [[maybe_unused]] int first, [[maybe_unused]] int last) { endRemoveRows(); } void VersionProxyModel::setCurrentVersion(const QString& version) { m_currentVersion = version; } #include "VersionProxyModel.moc" PrismLauncher-11.0.3/launcher/minecraft/0000755000175100017510000000000015224505336017534 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/minecraft/MinecraftInstance.h0000644000175100017510000001415415224505336023307 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include "BaseInstance.h" #include "minecraft/launch/MinecraftTarget.h" #include "minecraft/mod/Mod.h" class ModFolderModel; class ResourceFolderModel; class ResourcePackFolderModel; class ShaderPackFolderModel; class TexturePackFolderModel; class WorldList; class LaunchStep; class LaunchProfile; class PackProfile; class MinecraftInstance : public BaseInstance { Q_OBJECT public: MinecraftInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir); virtual ~MinecraftInstance(); virtual void saveNow() override; void loadSpecificSettings() override; // FIXME: remove QString typeName() const override; // FIXME: remove QSet traits() const override; bool canEdit() const override { return true; } bool canExport() const override { return true; } void populateLaunchMenu(QMenu* menu) override; ////// Directories and files ////// QString jarModsDir() const; QString resourcePacksDir() const; QString texturePacksDir() const; QString shaderPacksDir() const; QString modsRoot() const override; QString coreModsDir() const; QString nilModsDir() const; QString dataPacksDir(); QString modsCacheLocation() const; QString libDir() const; QString worldDir() const; QString resourcesDir() const; QDir jarmodsPath() const; QDir librariesPath() const; QDir versionsPath() const; QString instanceConfigFolder() const override; // Path to the instance's minecraft directory. QString gameRoot() const override; // Path to the instance's minecraft bin directory. QString binRoot() const; // where to put the natives during/before launch QString getNativePath() const; // where the instance-local libraries should be QString getLocalLibraryPath() const; /** Returns whether the instance, with its version, has support for demo mode. */ bool supportsDemo() const; void updateRuntimeContext() override; ////// Profile management ////// PackProfile* getPackProfile() const; ////// Mod Lists ////// ModFolderModel* loaderModList(); ModFolderModel* coreModList(); ModFolderModel* nilModList(); ResourcePackFolderModel* resourcePackList(); TexturePackFolderModel* texturePackList(); ShaderPackFolderModel* shaderPackList(); DataPackFolderModel* dataPackList(); QList resourceLists(); WorldList* worldList(); ////// Launch stuff ////// QList createUpdateTask() override; LaunchTask* createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) override; QStringList extraArguments() override; QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override; QList getJarMods() const; QString createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin); /// get arguments passed to java QStringList javaArguments(); QString getLauncher(); bool shouldApplyOnlineFixes(); /// get variables for launch command variable substitution/environment QMap getVariables() override; /// create an environment for launching processes QProcessEnvironment createEnvironment() override; QProcessEnvironment createLaunchEnvironment() override; QStringList getLogFileSearchPaths() override; QString getStatusbarDescription() override; // FIXME: remove virtual QStringList getClassPath(); // FIXME: remove virtual QStringList getNativeJars(); // FIXME: remove virtual QString getMainClass() const; // FIXME: remove virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) const; virtual JavaVersion getJavaVersion(); protected: QMap createCensorFilterFromSession(AuthSessionPtr session); QMap makeProfileVarMapping(std::shared_ptr profile) const; protected: // data std::unique_ptr m_components; std::unique_ptr m_loader_mod_list; std::unique_ptr m_core_mod_list; std::unique_ptr m_nil_mod_list; std::unique_ptr m_resource_pack_list; std::unique_ptr m_shader_pack_list; std::unique_ptr m_texture_pack_list; std::unique_ptr m_data_pack_list; std::unique_ptr m_world_list; }; PrismLauncher-11.0.3/launcher/minecraft/Rule.cpp0000644000175100017510000000521615224505336021153 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2025 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include "Rule.h" Rule Rule::fromJson(const QJsonObject& object) { Rule result; if (object["action"] == "allow") result.m_action = Allow; else if (object["action"] == "disallow") result.m_action = Disallow; if (auto os = object["os"]; os.isObject()) { if (auto name = os["name"].toString(); !name.isNull()) { result.m_os = OS{ name, os["version"].toString(), }; } } return result; } QJsonObject Rule::toJson() { QJsonObject result; if (m_action == Allow) result["action"] = "allow"; else if (m_action == Disallow) result["action"] = "disallow"; if (m_os.has_value()) { QJsonObject os; os["name"] = m_os->name; if (!m_os->version.isEmpty()) os["version"] = m_os->version; result["os"] = os; } return result; } Rule::Action Rule::apply(const RuntimeContext& runtimeContext) { if (m_os.has_value() && !runtimeContext.classifierMatches(m_os->name)) return Defer; return m_action; } PrismLauncher-11.0.3/launcher/minecraft/auth/0000755000175100017510000000000015224505336020475 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/minecraft/auth/MinecraftAccount.h0000644000175100017510000001205715224505336024100 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include #include "AccountData.h" #include "AuthSession.h" #include "QObjectPtr.h" #include "Usable.h" #include "minecraft/auth/AuthFlow.h" class Task; class MinecraftAccount; using MinecraftAccountPtr = shared_qobject_ptr; Q_DECLARE_METATYPE(MinecraftAccountPtr) /** * A profile within someone's Mojang account. * * Currently, the profile system has not been implemented by Mojang yet, * but we might as well add some things for it in Prism Launcher right now so * we don't have to rip the code to pieces to add it later. */ struct AccountProfile { QString id; QString name; bool legacy; }; /** * Object that stores information about a certain Mojang account. * * Said information may include things such as that account's username, client token, and access * token if the user chose to stay logged in. */ class MinecraftAccount : public QObject, public Usable { Q_OBJECT public: /* construction */ //! Do not copy accounts. ever. explicit MinecraftAccount(const MinecraftAccount& other, QObject* parent) = delete; //! Default constructor explicit MinecraftAccount(QObject* parent = 0); static MinecraftAccountPtr createBlankMSA(); static MinecraftAccountPtr createOffline(const QString& username); static MinecraftAccountPtr loadFromJsonV3(const QJsonObject& json); static QUuid uuidFromUsername(QString username); //! Saves a MinecraftAccount to a JSON object and returns it. QJsonObject saveToJson() const; public: /* manipulation */ shared_qobject_ptr login(bool useDeviceCode = false); shared_qobject_ptr refresh(); shared_qobject_ptr currentTask(); public: /* queries */ QString internalId() const { return data.internalId; } QString accessToken() const { return data.accessToken(); } QString profileId() const { return data.profileId(); } QString profileName() const { return data.profileName(); } QString displayName() const; bool isActive() const; AccountType accountType() const noexcept { return data.type; } bool ownsMinecraft() const { return data.type != AccountType::Offline && data.minecraftEntitlement.ownsMinecraft; } bool hasProfile() const { return data.profileId().size() != 0; } QString typeString() const { switch (data.type) { case AccountType::MSA: { return "msa"; } break; case AccountType::Offline: { return "offline"; } break; default: { return "unknown"; } } } QPixmap getFace(int width = 64, int height = 64) const; //! Returns the current state of the account AccountState accountState() const; AccountData* accountData() { return &data; } bool shouldRefresh() const; void fillSession(AuthSessionPtr session); QString lastError() const { return data.lastError(); } signals: /** * This signal is emitted when the account changes */ void changed(); void activityChanged(bool active); // TODO: better signalling for the various possible state changes - especially errors protected: /* variables */ AccountData data; // current task we are executing here shared_qobject_ptr m_currentTask; protected: /* methods */ void incrementUses() override; void decrementUses() override; private slots: void authSucceeded(); void authFailed(QString reason); }; PrismLauncher-11.0.3/launcher/minecraft/auth/steps/0000755000175100017510000000000015224505336021633 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp0000644000175100017510000001747115224505336026706 0ustar runnerrunner#include "XboxAuthorizationStep.h" #include #include #include #include "Application.h" #include "Logging.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" #include "net/RawHeaderProxy.h" #include "net/Upload.h" XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind) : AuthStep(data), m_token(token), m_relyingParty(std::move(relyingParty)), m_authorizationKind(std::move(authorizationKind)) {} QString XboxAuthorizationStep::describe() { return tr("Getting authorization to access %1 services.").arg(m_authorizationKind); } void XboxAuthorizationStep::perform() { const QString xboxAuthTemplate = R"XXX( { "Properties": { "SandboxId": "RETAIL", "UserTokens": [ "%1" ] }, "RelyingParty": "%2", "TokenType": "JWT" } )XXX"; const auto xboxAuthData = xboxAuthTemplate.arg(m_data->userToken.token, m_relyingParty); // http://xboxlive.com const QUrl url("https://xsts.auth.xboxlive.com/xsts/authorize"); auto headers = QList{ { .headerName = "Content-Type", .headerValue = "application/json" }, { .headerName = "Accept", .headerValue = "application/json" }, { .headerName = "x-xbl-contract-version", .headerValue = "1" } }; auto [request, response] = Net::Upload::makeByteArray(url, xboxAuthData.toUtf8()); m_request = request; m_request->addHeaderProxy(std::make_unique(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("XboxAuthorizationStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); qDebug() << "Getting authorization token for" << m_relyingParty; } void XboxAuthorizationStep::onRequestDone(QByteArray* response) { qCDebug(authCredentials()) << *response; if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Reply error:" << m_request->error(); if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) { if (processSTSError(*response)) { return; } emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, m_request->errorString())); } else { m_data->networkError = m_request->error(); emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, m_request->errorString())); } return; } Token temp; if (!Parsers::parseXTokenResponse(*response, temp, m_authorizationKind)) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Could not parse authorization response for access to %1 services.").arg(m_authorizationKind)); return; } if (temp.extra["uhs"] != m_data->userToken.extra["uhs"]) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Server has changed %1 authorization user hash in the reply. Something is wrong.").arg(m_authorizationKind)); return; } auto& token = *m_token; token = temp; emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization to access %1").arg(m_relyingParty)); } bool XboxAuthorizationStep::processSTSError(const QByteArray& response) { if (m_request->error() == QNetworkReply::AuthenticationRequiredError) { QJsonParseError jsonError; const QJsonDocument doc = QJsonDocument::fromJson(response, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qWarning() << "Cannot parse error XSTS response as JSON:" << jsonError.errorString(); emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Cannot parse %1 authorization error response as JSON: %2").arg(m_authorizationKind, jsonError.errorString())); return true; } int64_t errorCode = -1; auto obj = doc.object(); if (!Parsers::getNumber(obj.value("XErr"), errorCode)) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XErr element is missing from %1 authorization error response.").arg(m_authorizationKind)); return true; } switch (errorCode) { case 2148916233: { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account does not have an Xbox Live profile. Buy the game on %1 first.") .arg("minecraft.net")); return true; } case 2148916235: { // NOTE: this is the Grulovia error emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Xbox Live is not available in your country. You've been blocked.")); return true; } case 2148916238: { emit finished( AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.") .arg("help.minecraft.net")); return true; } // the following codes where copied from: https://github.com/PrismarineJS/prismarine-auth/pull/44 case 2148916236: { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account requires proof of age to play. Please login to %1 to provide proof of age.") .arg("login.live.com")); return true; } case 2148916237: emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account has reached its limit for playtime. This " "Microsoft account has been blocked from logging in.")); return true; case 2148916227: { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account was banned by Xbox for violating one or more " "Community Standards for Xbox and is unable to be used.")); return true; } case 2148916229: { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account is currently restricted and your guardian has not given you permission to play " "online. Login to %1 and have your guardian change your permissions.") .arg("account.microsoft.com")); return true; } case 2148916234: { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account has not accepted Xbox's Terms of Service. Please login and accept them.")); return true; } default: { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XSTS authentication ended with unrecognized error(s):\n\n%1").arg(errorCode)); return true; } } } return false; } PrismLauncher-11.0.3/launcher/minecraft/auth/steps/MinecraftProfileStep.h0000644000175100017510000000077215224505336026077 0ustar runnerrunner#pragma once #include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" #include "net/NetJob.h" class MinecraftProfileStep : public AuthStep { Q_OBJECT public: explicit MinecraftProfileStep(AccountData* data); virtual ~MinecraftProfileStep() noexcept = default; void perform() override; QString describe() override; private slots: void onRequestDone(QByteArray* response); private: Net::Download::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-11.0.3/launcher/minecraft/auth/steps/GetSkinStep.h0000644000175100017510000000073715224505336024213 0ustar runnerrunner#pragma once #include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" #include "net/NetJob.h" class GetSkinStep : public AuthStep { Q_OBJECT public: explicit GetSkinStep(AccountData* data); virtual ~GetSkinStep() noexcept = default; void perform() override; QString describe() override; private slots: void onRequestDone(QByteArray* response); private: Net::Download::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-11.0.3/launcher/minecraft/auth/steps/LauncherLoginStep.cpp0000644000175100017510000000505615224505336025733 0ustar runnerrunner#include "LauncherLoginStep.h" #include #include #include "Application.h" #include "Logging.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" #include "net/RawHeaderProxy.h" #include "net/Upload.h" LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {} QString LauncherLoginStep::describe() { return tr("Fetching Minecraft access token"); } void LauncherLoginStep::perform() { QUrl url("https://api.minecraftservices.com/launcher/login"); auto uhs = m_data->mojangservicesToken.extra["uhs"].toString(); auto xToken = m_data->mojangservicesToken.token; QString mc_auth_template = R"XXX( { "xtoken": "XBL3.0 x=%1;%2", "platform": "PC_LAUNCHER" } )XXX"; auto requestBody = mc_auth_template.arg(uhs, xToken); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, }; auto [request, response] = Net::Upload::makeByteArray(url, requestBody.toUtf8()); m_request = request; m_request->addHeaderProxy(std::make_unique(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("LauncherLoginStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); qDebug() << "Getting Minecraft access token..."; } void LauncherLoginStep::onRequestDone(QByteArray* response) { qCDebug(authCredentials()) << *response; if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Reply error:" << m_request->error(); if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); } else { m_data->networkError = m_request->error(); emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); } return; } if (!Parsers::parseMojangResponse(*response, m_data->yggdrasilToken)) { qWarning() << "Could not parse login_with_xbox response..."; emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse the Minecraft access token response.")); return; } emit finished(AccountTaskState::STATE_WORKING, tr("Got Minecraft access token")); } PrismLauncher-11.0.3/launcher/minecraft/auth/steps/MSAStep.h0000644000175100017510000000373315224505336023266 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include "minecraft/auth/AuthStep.h" #include class MSAStep : public AuthStep { Q_OBJECT public: explicit MSAStep(AccountData* data, bool silent = false); virtual ~MSAStep() noexcept = default; void perform() override; QString describe() override; signals: void authorizeWithBrowser(const QUrl& url); private: bool m_silent; QString m_clientId; QOAuth2AuthorizationCodeFlow m_oauth2; }; PrismLauncher-11.0.3/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp0000644000175100017510000002427115224505336025534 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "MSADeviceCodeStep.h" #include #include #include "Application.h" #include "Json.h" #include "net/RawHeaderProxy.h" // https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code MSADeviceCodeStep::MSADeviceCodeStep(AccountData* data) : AuthStep(data) { m_clientId = APPLICATION->getMSAClientID(); connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort); connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser); } QString MSADeviceCodeStep::describe() { return tr("Logging in with Microsoft account(device code)."); } void MSADeviceCodeStep::perform() { QUrlQuery data; data.addQueryItem("client_id", m_clientId); data.addQueryItem("scope", "XboxLive.SignIn XboxLive.offline_access"); auto payload = data.query(QUrl::FullyEncoded).toUtf8(); QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"); auto headers = QList{ { "Content-Type", "application/x-www-form-urlencoded" }, { "Accept", "application/json" }, }; auto [request, response] = Net::Upload::makeByteArray(url, payload); m_request = request; m_request->addHeaderProxy(std::make_unique(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("MSADeviceCodeStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, [this, response] { deviceAuthorizationFinished(response); }); m_task->start(); } struct DeviceAuthorizationResponse { QString device_code; QString user_code; QString verification_uri; int expires_in; int interval; QString error; QString error_description; }; DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& data) { QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { qWarning() << "Failed to parse device authorization response due to err:" << err.errorString(); return {}; } if (!doc.isObject()) { qWarning() << "Device authorization response is not an object"; return {}; } auto obj = doc.object(); return { obj["device_code"].toString(), obj["user_code"].toString(), obj["verification_uri"].toString(), obj["expires_in"].toInt(), obj["interval"].toInt(), obj["error"].toString(), obj["error_description"].toString(), }; } void MSADeviceCodeStep::deviceAuthorizationFinished(QByteArray* response) { if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) { qWarning() << "Device authorization failed:" << m_request->error() << m_request->errorString(); emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: %1").arg(m_request->errorString())); return; } auto rsp = parseDeviceAuthorizationResponse(*response); if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { qWarning() << "Device authorization failed:" << rsp.error; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); return; } if (rsp.device_code.isEmpty() || rsp.user_code.isEmpty() || rsp.verification_uri.isEmpty() || rsp.expires_in == 0) { qWarning() << "Device authorization failed: required fields missing"; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: required fields missing")); return; } if (rsp.interval != 0) { interval = rsp.interval; } m_device_code = rsp.device_code; emit authorizeWithBrowser(rsp.verification_uri, rsp.user_code, rsp.expires_in); m_expiration_timer.setTimerType(Qt::VeryCoarseTimer); m_expiration_timer.setInterval(rsp.expires_in * 1000); m_expiration_timer.setSingleShot(true); m_expiration_timer.start(); m_pool_timer.setTimerType(Qt::VeryCoarseTimer); m_pool_timer.setSingleShot(true); startPoolTimer(); } void MSADeviceCodeStep::abort() { m_expiration_timer.stop(); m_pool_timer.stop(); if (m_request) { m_request->abort(); } m_is_aborted = true; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Task aborted")); } void MSADeviceCodeStep::startPoolTimer() { if (m_is_aborted) { return; } if (m_expiration_timer.remainingTime() < interval * 1000) { perform(); return; } m_pool_timer.setInterval(interval * 1000); m_pool_timer.start(); } void MSADeviceCodeStep::authenticateUser() { QUrlQuery data; data.addQueryItem("client_id", m_clientId); data.addQueryItem("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); data.addQueryItem("device_code", m_device_code); auto payload = data.query(QUrl::FullyEncoded).toUtf8(); QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"); auto headers = QList{ { "Content-Type", "application/x-www-form-urlencoded" }, { "Accept", "application/json" }, }; auto [request, response] = Net::Upload::makeByteArray(url, payload); m_request = request; m_request->addHeaderProxy(std::make_unique(headers)); connect(m_request.get(), &Task::finished, this, [this, response] { authenticationFinished(response); }); m_request->setNetwork(APPLICATION->network()); m_request->start(); } struct AuthenticationResponse { QString access_token; QString token_type; QString refresh_token; int expires_in; QString error; QString error_description; QVariantMap extra; }; AuthenticationResponse parseAuthenticationResponse(const QByteArray& data) { QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { qWarning() << "Failed to parse device authorization response due to err:" << err.errorString(); return {}; } if (!doc.isObject()) { qWarning() << "Device authorization response is not an object"; return {}; } auto obj = doc.object(); return { obj["access_token"].toString(), obj["token_type"].toString(), obj["refresh_token"].toString(), obj["expires_in"].toInt(), obj["error"].toString(), obj["error_description"].toString(), obj.toVariantMap() }; } void MSADeviceCodeStep::authenticationFinished(QByteArray* response) { if (m_request->error() == QNetworkReply::TimeoutError) { // rfc8628#section-3.5 // "On encountering a connection timeout, clients MUST unilaterally // reduce their polling frequency before retrying. The use of an // exponential backoff algorithm to achieve this, such as doubling the // polling interval on each such connection timeout, is RECOMMENDED." interval *= 2; startPoolTimer(); return; } auto rsp = parseAuthenticationResponse(*response); if (rsp.error == "slow_down") { // rfc8628#section-3.5 // "A variant of 'authorization_pending', the authorization request is // still pending and polling should continue, but the interval MUST // be increased by 5 seconds for this and all subsequent requests." interval += 5; startPoolTimer(); return; } if (rsp.error == "authorization_pending") { // keep trying - rfc8628#section-3.5 // "The authorization request is still pending as the end user hasn't // yet completed the user-interaction steps (Section 3.3)." startPoolTimer(); return; } if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { qWarning() << "Device Access failed:" << rsp.error; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device Access failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); return; } if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) { startPoolTimer(); // it failed so just try again without increasing the interval return; } m_expiration_timer.stop(); m_data->msaClientID = m_clientId; m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc(); m_data->msaToken.notAfter = QDateTime::currentDateTime().addSecs(rsp.expires_in); m_data->msaToken.extra = rsp.extra; m_data->msaToken.refresh_token = rsp.refresh_token; m_data->msaToken.token = rsp.access_token; emit finished(AccountTaskState::STATE_WORKING, tr("Got MSA token")); } PrismLauncher-11.0.3/launcher/minecraft/auth/steps/EntitlementsStep.h0000644000175100017510000000102515224505336025311 0ustar runnerrunner#pragma once #include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" #include "net/NetJob.h" class EntitlementsStep : public AuthStep { Q_OBJECT public: explicit EntitlementsStep(AccountData* data); virtual ~EntitlementsStep() noexcept = default; void perform() override; QString describe() override; private slots: void onRequestDone(QByteArray* response); private: QString m_entitlements_request_id; Net::Download::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-11.0.3/launcher/minecraft/auth/steps/MSADeviceCodeStep.h0000644000175100017510000000457015224505336025201 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" #include "net/Upload.h" class MSADeviceCodeStep : public AuthStep { Q_OBJECT public: explicit MSADeviceCodeStep(AccountData* data); virtual ~MSADeviceCodeStep() noexcept = default; void perform() override; QString describe() override; public slots: void abort() override; signals: void authorizeWithBrowser(QString url, QString code, int expiresIn); private slots: void deviceAuthorizationFinished(QByteArray* response); void startPoolTimer(); void authenticateUser(); void authenticationFinished(QByteArray* response); private: QString m_clientId; QString m_device_code; bool m_is_aborted = false; int interval = 5; QTimer m_pool_timer; QTimer m_expiration_timer; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-11.0.3/launcher/minecraft/auth/steps/XboxUserStep.cpp0000644000175100017510000000526515224505336024762 0ustar runnerrunner#include "XboxUserStep.h" #include #include "Application.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" #include "net/RawHeaderProxy.h" XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) {} QString XboxUserStep::describe() { return tr("Logging in as an Xbox user."); } void XboxUserStep::perform() { QString xbox_auth_template = R"XXX( { "Properties": { "AuthMethod": "RPS", "SiteName": "user.auth.xboxlive.com", "RpsTicket": "d=%1" }, "RelyingParty": "http://auth.xboxlive.com", "TokenType": "JWT" } )XXX"; auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token); QUrl url("https://user.auth.xboxlive.com/user/authenticate"); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, // set contract-version header (prevent err 400 bad-request?) // https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders { "x-xbl-contract-version", "1" } }; auto [request, response] = Net::Upload::makeByteArray(url, xbox_auth_data.toUtf8()); m_request = request; m_request->addHeaderProxy(std::make_unique(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("XboxUserStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); qDebug() << "First layer of Xbox auth ... commencing."; } void XboxUserStep::onRequestDone(QByteArray* response) { if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Reply error:" << m_request->error(); if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Xbox user authentication failed: %1").arg(m_request->errorString())); } else { m_data->networkError = m_request->error(); emit finished(AccountTaskState::STATE_OFFLINE, tr("Xbox user authentication failed: %1").arg(m_request->errorString())); } return; } Token temp; if (!Parsers::parseXTokenResponse(*response, temp, "UToken")) { qWarning() << "Could not parse user authentication response..."; emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Xbox user authentication response could not be understood.")); return; } m_data->userToken = temp; emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox user token")); } PrismLauncher-11.0.3/launcher/minecraft/auth/steps/MSAStep.cpp0000644000175100017510000002014115224505336023611 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "MSAStep.h" #include #include #include #include #include #include "Application.h" #include "BuildConfig.h" #include "FileSystem.h" #include #include #include bool isSchemeHandlerRegistered() { #ifdef Q_OS_LINUX QProcess process; process.start("xdg-mime", { "query", "default", "x-scheme-handler/" + BuildConfig.LAUNCHER_APP_BINARY_NAME }); process.waitForFinished(); QString output = process.readAllStandardOutput().trimmed(); return output.contains(APPLICATION->desktopFileName()); #elif defined(Q_OS_WIN) QString regPath = QString("HKEY_CURRENT_USER\\Software\\Classes\\%1").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); QSettings settings(regPath, QSettings::NativeFormat); const QString registeredRunCommand = settings.value("shell/open/command/.").toString().replace("\\", "/"); return registeredRunCommand.contains(QCoreApplication::applicationFilePath()); #endif return true; } class CustomOAuthOobReplyHandler : public QOAuthOobReplyHandler { Q_OBJECT public: explicit CustomOAuthOobReplyHandler(QObject* parent = nullptr) : QOAuthOobReplyHandler(parent) { connect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived); } ~CustomOAuthOobReplyHandler() override { disconnect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived); } QString callback() const override { return BuildConfig.LAUNCHER_APP_BINARY_NAME + "://oauth/microsoft"; } protected: void networkReplyFinished(QNetworkReply* reply) override { if (reply->error() != QNetworkReply::NoError) { qWarning() << "OAuth2 request failed:" << reply->readAll(); } QOAuthOobReplyHandler::networkReplyFinished(reply); } }; class LoggingOAuthHttpServerReplyHandler final : public QOAuthHttpServerReplyHandler { Q_OBJECT public: explicit LoggingOAuthHttpServerReplyHandler(QObject* parent = nullptr) : QOAuthHttpServerReplyHandler(parent) {} protected: void networkReplyFinished(QNetworkReply* reply) override { if (reply->error() != QNetworkReply::NoError) { qWarning() << "OAuth2 request failed:" << reply->readAll(); } QOAuthHttpServerReplyHandler::networkReplyFinished(reply); } }; MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent) { m_clientId = APPLICATION->getMSAClientID(); if (QCoreApplication::applicationFilePath().startsWith("/tmp/.mount_") || APPLICATION->isPortable() || !isSchemeHandlerRegistered()) { auto replyHandler = new LoggingOAuthHttpServerReplyHandler(this); replyHandler->setCallbackText(QString(R"XXX( Login Successful, redirecting... )XXX") .arg(BuildConfig.LOGIN_CALLBACK_URL)); m_oauth2.setReplyHandler(replyHandler); } else { m_oauth2.setReplyHandler(new CustomOAuthOobReplyHandler(this)); } m_oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize")); m_oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")); m_oauth2.setScope("XboxLive.SignIn XboxLive.offline_access"); m_oauth2.setClientIdentifier(m_clientId); m_oauth2.setNetworkAccessManager(APPLICATION->network()); connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] { m_data->msaClientID = m_oauth2.clientIdentifier(); m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc(); 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 MSA token")); }); 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 (m_oauth2.status() == QAbstractOAuth::Status::Granted || silent) { if (err == QAbstractOAuth2::Error::NetworkError) { state = AccountTaskState::STATE_OFFLINE; } else { state = AccountTaskState::STATE_FAILED_SOFT; } } auto message = tr("Microsoft user authentication failed."); if (silent) { message = tr("Failed to refresh token."); } qWarning() << message; emit finished(state, message); }); 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(&m_oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this, [this](const QVariantMap& tokens) { m_data->msaToken.extra = tokens; }); connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::clientIdentifierChanged, this, [this](const QString& clientIdentifier) { m_data->msaClientID = clientIdentifier; }); } QString MSAStep::describe() { return tr("Logging in with Microsoft account."); } void MSAStep::perform() { if (m_silent) { if (m_data->msaClientID != m_clientId) { emit finished(AccountTaskState::STATE_DISABLED, tr("Microsoft user authentication failed - client identification has changed.")); return; } if (m_data->msaToken.refresh_token.isEmpty()) { emit finished(AccountTaskState::STATE_DISABLED, tr("Microsoft user authentication failed - refresh token is empty.")); return; } m_oauth2.setRefreshToken(m_data->msaToken.refresh_token); m_oauth2.refreshAccessToken(); } else { m_oauth2.setModifyParametersFunction( [](QAbstractOAuth::Stage stage, QMultiMap* map) { map->insert("prompt", "select_account"); }); *m_data = AccountData(); m_data->msaClientID = m_clientId; m_oauth2.grant(); } } #include "MSAStep.moc" PrismLauncher-11.0.3/launcher/minecraft/auth/steps/LauncherLoginStep.h0000644000175100017510000000075515224505336025401 0ustar runnerrunner#pragma once #include #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" #include "net/Upload.h" class LauncherLoginStep : public AuthStep { Q_OBJECT public: explicit LauncherLoginStep(AccountData* data); virtual ~LauncherLoginStep() noexcept = default; void perform() override; QString describe() override; private slots: void onRequestDone(QByteArray* response); private: Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-11.0.3/launcher/minecraft/auth/steps/XboxAuthorizationStep.h0000644000175100017510000000131515224505336026341 0ustar runnerrunner#pragma once #include #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" #include "net/Upload.h" class XboxAuthorizationStep : public AuthStep { Q_OBJECT public: explicit XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind); virtual ~XboxAuthorizationStep() noexcept = default; void perform() override; QString describe() override; private: bool processSTSError(const QByteArray& response); private slots: void onRequestDone(QByteArray* response); private: Token* m_token; QString m_relyingParty; QString m_authorizationKind; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-11.0.3/launcher/minecraft/auth/steps/XboxUserStep.h0000644000175100017510000000073615224505336024425 0ustar runnerrunner#pragma once #include #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" #include "net/Upload.h" class XboxUserStep : public AuthStep { Q_OBJECT public: explicit XboxUserStep(AccountData* data); virtual ~XboxUserStep() noexcept = default; void perform() override; QString describe() override; private slots: void onRequestDone(QByteArray* response); private: Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-11.0.3/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp0000644000175100017510000000562115224505336026430 0ustar runnerrunner#include "MinecraftProfileStep.h" #include #include "Application.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" #include "net/RawHeaderProxy.h" MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) {} QString MinecraftProfileStep::describe() { return tr("Fetching the Minecraft profile."); } void MinecraftProfileStep::perform() { QUrl url("https://api.minecraftservices.com/minecraft/profile"); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; auto [request, response] = Net::Download::makeByteArray(url); m_request = request; m_request->addHeaderProxy(std::make_unique(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("MinecraftProfileStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); } void MinecraftProfileStep::onRequestDone(QByteArray* response) { if (m_request->error() == QNetworkReply::ContentNotFoundError) { // NOTE: Succeed even if we do not have a profile. This is a valid account state. m_data->minecraftProfile = MinecraftProfile(); emit finished(AccountTaskState::STATE_WORKING, tr("Account has no Minecraft profile.")); return; } if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Error getting profile:"; qWarning() << " HTTP Status :" << m_request->replyStatusCode(); qWarning() << " Internal error no.:" << m_request->error(); qWarning() << " Error string :" << m_request->errorString(); qWarning() << " Response:"; qWarning() << QString::fromUtf8(*response); if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString())); } else { m_data->networkError = m_request->error(); emit finished(AccountTaskState::STATE_OFFLINE, tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString())); } return; } if (!Parsers::parseMinecraftProfile(*response, m_data->minecraftProfile)) { m_data->minecraftProfile = MinecraftProfile(); emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile response could not be parsed")); return; } emit finished(AccountTaskState::STATE_WORKING, tr("Got Minecraft profile")); } PrismLauncher-11.0.3/launcher/minecraft/auth/steps/EntitlementsStep.cpp0000644000175100017510000000352315224505336025651 0ustar runnerrunner#include "EntitlementsStep.h" #include #include #include #include #include #include "Application.h" #include "Logging.h" #include "minecraft/auth/Parsers.h" #include "net/Download.h" #include "net/NetJob.h" #include "net/RawHeaderProxy.h" #include "tasks/Task.h" EntitlementsStep::EntitlementsStep(AccountData* data) : AuthStep(data) {} QString EntitlementsStep::describe() { return tr("Determining game ownership."); } void EntitlementsStep::perform() { m_entitlements_request_id = QUuid::createUuid().toString(QUuid::WithoutBraces); QUrl url("https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlements_request_id); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; auto [request, response] = Net::Download::makeByteArray(url); m_request = request; m_request->addHeaderProxy(std::make_unique(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("EntitlementsStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); qDebug() << "Getting entitlements..."; } void EntitlementsStep::onRequestDone(QByteArray* response) { qCDebug(authCredentials()) << *response; // TODO: check presence of same entitlementsRequestId? // TODO: validate JWTs? Parsers::parseMinecraftEntitlements(*response, m_data->minecraftEntitlement); emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements")); } PrismLauncher-11.0.3/launcher/minecraft/auth/steps/GetSkinStep.cpp0000644000175100017510000000163215224505336024541 0ustar runnerrunner #include "GetSkinStep.h" #include #include "Application.h" GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) {} QString GetSkinStep::describe() { return tr("Getting skin."); } void GetSkinStep::perform() { QUrl url(m_data->minecraftProfile.skin.url); auto [request, response] = Net::Download::makeByteArray(url); m_request = request; m_request->enableAutoRetry(true); m_task.reset(new NetJob("GetSkinStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); } void GetSkinStep::onRequestDone(QByteArray* response) { if (m_request->error() == QNetworkReply::NoError) m_data->minecraftProfile.skin.data = *response; emit finished(AccountTaskState::STATE_WORKING, tr("Got skin")); } PrismLauncher-11.0.3/launcher/minecraft/auth/AccountList.cpp0000644000175100017510000005474015224505336023443 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "AccountList.h" #include "AccountData.h" #include "tasks/Task.h" #include #include #include #include #include #include #include #include #include #include #include #include #include enum AccountListVersion { MojangMSA = 3 }; AccountList::AccountList(QObject* parent) : QAbstractListModel(parent) { m_refreshTimer = new QTimer(this); m_refreshTimer->setSingleShot(true); connect(m_refreshTimer, &QTimer::timeout, this, &AccountList::fillQueue); m_nextTimer = new QTimer(this); m_nextTimer->setSingleShot(true); connect(m_nextTimer, &QTimer::timeout, this, &AccountList::tryNext); } AccountList::~AccountList() noexcept {} int AccountList::findAccountByProfileId(const QString& profileId) const { for (int i = 0; i < count(); i++) { MinecraftAccountPtr account = at(i); if (account->profileId() == profileId) { return i; } } return -1; } MinecraftAccountPtr AccountList::getAccountByProfileName(const QString& profileName) const { for (int i = 0; i < count(); i++) { MinecraftAccountPtr account = at(i); if (account->profileName() == profileName) { return account; } } return nullptr; } const MinecraftAccountPtr AccountList::at(int i) const { return MinecraftAccountPtr(m_accounts.at(i)); } QStringList AccountList::profileNames() const { QStringList out; for (auto& account : m_accounts) { auto profileName = account->profileName(); if (profileName.isEmpty()) { continue; } out.append(profileName); } return out; } void AccountList::addAccount(const MinecraftAccountPtr account) { // NOTE: Do not allow adding something that's already there. We shouldn't let it continue // because of the signal / slot connections after this. if (m_accounts.contains(account)) { qDebug() << "Tried to add account that's already on the accounts list!"; return; } // hook up notifications for changes in the account connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); // override/replace existing account with the same profileId auto profileId = account->profileId(); if (profileId.size()) { auto existingAccount = findAccountByProfileId(profileId); if (existingAccount != -1) { qDebug() << "Replacing old account with a new one with the same profile ID!"; MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount]; m_accounts[existingAccount] = account; if (m_defaultAccount == existingAccountPtr) { m_defaultAccount = account; } // disconnect notifications for changes in the account being replaced existingAccountPtr->disconnect(this); emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1)); onListChanged(); return; } } // if we don't have this profileId yet, add the account to the end int row = m_accounts.count(); qDebug() << "Inserting account at index" << row; beginInsertRows(QModelIndex(), row, row); m_accounts.append(account); endInsertRows(); onListChanged(); } void AccountList::removeAccount(QModelIndex index) { int row = index.row(); if (index.isValid() && row >= 0 && row < m_accounts.size()) { auto& account = m_accounts[row]; if (account == m_defaultAccount) { m_defaultAccount = nullptr; onDefaultAccountChanged(); } account->disconnect(this); beginRemoveRows(QModelIndex(), row, row); m_accounts.removeAt(index.row()); endRemoveRows(); onListChanged(); } } void AccountList::moveAccount(QModelIndex index, int delta) { const int row = index.row(); const int newRow = row + delta; if (index.isValid() && row < m_accounts.size() && newRow >= 0 && newRow < m_accounts.size()) { // Qt is stupid, https://doc.qt.io/qt-6/qabstractitemmodel.html#beginMoveRows const int modelDestinationRow = (newRow > row) ? newRow + 1 : newRow; if (beginMoveRows(QModelIndex(), row, row, QModelIndex(), modelDestinationRow)) { m_accounts.move(row, newRow); endMoveRows(); onListChanged(); } else { qCritical().noquote() << "AccountList: failed to move account from" << row << "to" << newRow << QString("(%1 accounts in total)").arg(this->count()); } } } MinecraftAccountPtr AccountList::defaultAccount() const { return m_defaultAccount; } void AccountList::setDefaultAccount(MinecraftAccountPtr newAccount) { if (!newAccount && m_defaultAccount) { int idx = 0; auto previousDefaultAccount = m_defaultAccount; m_defaultAccount = nullptr; for (MinecraftAccountPtr account : m_accounts) { if (account == previousDefaultAccount) { emit dataChanged(index(idx), index(idx, columnCount(QModelIndex()) - 1)); } idx++; } onDefaultAccountChanged(); } else { auto currentDefaultAccount = m_defaultAccount; int currentDefaultAccountIdx = -1; auto newDefaultAccount = m_defaultAccount; int newDefaultAccountIdx = -1; int idx = 0; for (MinecraftAccountPtr account : m_accounts) { if (account == newAccount) { newDefaultAccount = account; newDefaultAccountIdx = idx; } if (currentDefaultAccount == account) { currentDefaultAccountIdx = idx; } idx++; } if (currentDefaultAccount != newDefaultAccount) { emit dataChanged(index(currentDefaultAccountIdx), index(currentDefaultAccountIdx, columnCount(QModelIndex()) - 1)); emit dataChanged(index(newDefaultAccountIdx), index(newDefaultAccountIdx, columnCount(QModelIndex()) - 1)); m_defaultAccount = newDefaultAccount; onDefaultAccountChanged(); } } } void AccountList::accountChanged() { // the list changed. there is no doubt. onListChanged(); } void AccountList::accountActivityChanged(bool active) { MinecraftAccount* account = qobject_cast(sender()); bool found = false; for (int i = 0; i < count(); i++) { if (at(i).get() == account) { emit dataChanged(index(i), index(i, columnCount(QModelIndex()) - 1)); found = true; break; } } if (found) { emit listActivityChanged(); if (active) { beginActivity(); } else { endActivity(); } } } void AccountList::onListChanged() { if (m_autosave) // TODO: Alert the user if this fails. saveList(); emit listChanged(); } void AccountList::onDefaultAccountChanged() { if (m_autosave) saveList(); emit defaultAccountChanged(); } int AccountList::count() const { return m_accounts.count(); } QString getAccountStatus(AccountState status) { switch (status) { case AccountState::Unchecked: return QObject::tr("Unchecked", "Account status"); case AccountState::Offline: return QObject::tr("Offline", "Account status"); case AccountState::Online: return QObject::tr("Ready", "Account status"); case AccountState::Working: return QObject::tr("Working", "Account status"); case AccountState::Errored: return QObject::tr("Errored", "Account status"); case AccountState::Expired: return QObject::tr("Expired", "Account status"); case AccountState::Disabled: return QObject::tr("Disabled", "Account status"); case AccountState::Gone: return QObject::tr("Gone", "Account status"); default: return QObject::tr("Unknown", "Account status"); } } QVariant AccountList::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() > count()) return QVariant(); MinecraftAccountPtr account = at(index.row()); switch (role) { case Qt::SizeHintRole: if (index.column() == ProfileNameColumn) { return QSize(0, 30); } return QVariant(); case Qt::DecorationRole: if (index.column() == ProfileNameColumn) { auto face = account->getFace(24, 24); if (!face.isNull()) { return face; } else { return QIcon::fromTheme("noaccount").pixmap(24, 24); } } return QVariant(); case Qt::DisplayRole: switch (index.column()) { case ProfileNameColumn: return account->profileName(); case TypeColumn: { switch (account->accountType()) { case AccountType::MSA: { return tr("MSA", "Account type"); } case AccountType::Offline: { return tr("Offline", "Account type"); } } return tr("Unknown", "Account type"); } case StatusColumn: return getAccountStatus(account->accountState()); default: return QVariant(); } case PointerRole: return QVariant::fromValue(account); case Qt::CheckStateRole: if (index.column() == ProfileNameColumn) return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; return QVariant(); default: return QVariant(); } } QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case ProfileNameColumn: return tr("Username"); case TypeColumn: return tr("Type"); case StatusColumn: return tr("Status"); default: return QVariant(); } case Qt::ToolTipRole: switch (section) { case ProfileNameColumn: return tr("Minecraft username associated with the account."); case TypeColumn: return tr("Type of the account (MSA or Offline)"); case StatusColumn: return tr("Current status of the account."); default: return QVariant(); } default: return QVariant(); } } int AccountList::rowCount(const QModelIndex& parent) const { // Return count return parent.isValid() ? 0 : count(); } int AccountList::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : NUM_COLUMNS; } Qt::ItemFlags AccountList::flags(const QModelIndex& index) const { if (index.row() < 0 || index.row() >= rowCount(index.parent()) || !index.isValid()) { return Qt::NoItemFlags; } return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable; } bool AccountList::setData(const QModelIndex& idx, const QVariant& value, int role) { if (idx.row() < 0 || idx.row() >= rowCount(idx.parent()) || !idx.isValid()) { return false; } if (role == Qt::CheckStateRole) { if (value == Qt::Checked) { MinecraftAccountPtr account = at(idx.row()); setDefaultAccount(account); } else if (m_defaultAccount == at(idx.row())) setDefaultAccount(nullptr); } emit dataChanged(idx, index(idx.row(), columnCount(QModelIndex()) - 1)); return true; } bool AccountList::loadList() { if (m_listFilePath.isEmpty()) { qCritical() << "Can't load Mojang account list. No file path given and no default set."; return false; } QFile file(m_listFilePath); // Try to open the file and fail if we can't. // TODO: We should probably report this error to the user. if (!file.open(QIODevice::ReadOnly)) { qCritical() << QString("Failed to read the account list file %1 (%2).").arg(m_listFilePath).arg(file.errorString()).toUtf8(); return false; } // Read the file and close it. QByteArray jsonData = file.readAll(); file.close(); QJsonParseError parseError; QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); // Fail if the JSON is invalid. if (parseError.error != QJsonParseError::NoError) { qCritical() << QString("Failed to parse account list file: %1 at offset %2") .arg(parseError.errorString(), QString::number(parseError.offset)) .toUtf8(); return false; } // Make sure the root is an object. if (!jsonDoc.isObject()) { qCritical() << "Invalid account list JSON: Root should be an array."; return false; } QJsonObject root = jsonDoc.object(); // Make sure the format version matches. auto listVersion = root.value("formatVersion").toVariant().toInt(); if (listVersion == AccountListVersion::MojangMSA) return loadV3(root); QString newName = "accounts-old.json"; qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; // Attempt to rename the old version. file.rename(newName); return false; } bool AccountList::loadV3(QJsonObject& root) { beginResetModel(); QJsonArray accounts = root.value("accounts").toArray(); for (QJsonValue accountVal : accounts) { QJsonObject accountObj = accountVal.toObject(); MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV3(accountObj); if (account.get() != nullptr) { auto profileId = account->profileId(); if (profileId.size()) { if (findAccountByProfileId(profileId) != -1) { continue; } } connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); m_accounts.append(account); if (accountObj.value("active").toBool(false)) { m_defaultAccount = account; } } else { qWarning() << "Failed to load an account."; } } endResetModel(); return true; } bool AccountList::saveList() { if (m_listFilePath.isEmpty()) { qCritical() << "Can't save Mojang account list. No file path given and no default set."; return false; } // make sure the parent folder exists if (!FS::ensureFilePathExists(m_listFilePath)) return false; // make sure the file wasn't overwritten with a folder before (fixes a bug) QFileInfo finfo(m_listFilePath); if (finfo.isDir()) { QDir badDir(m_listFilePath); badDir.removeRecursively(); } qDebug() << "Writing account list to" << m_listFilePath; qDebug() << "Building JSON data structure."; // Build the JSON document to write to the list file. QJsonObject root; root.insert("formatVersion", AccountListVersion::MojangMSA); // Build a list of accounts. qDebug() << "Building account array."; QJsonArray accounts; for (MinecraftAccountPtr account : m_accounts) { QJsonObject accountObj = account->saveToJson(); if (m_defaultAccount == account) { accountObj["active"] = true; } accounts.append(accountObj); } // Insert the account list into the root object. root.insert("accounts", accounts); // Create a JSON document object to convert our JSON to bytes. QJsonDocument doc(root); // Now that we're done building the JSON object, we can write it to the file. qDebug() << "Writing account list to file."; QSaveFile file(m_listFilePath); // Try to open the file and fail if we can't. // TODO: We should probably report this error to the user. if (!file.open(QIODevice::WriteOnly)) { qCritical() << QString("Failed to save the account list file %1 (%2).").arg(m_listFilePath).arg(file.errorString()).toUtf8(); return false; } // Write the JSON to the file. file.write(doc.toJson()); file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser); if (file.commit()) { qDebug() << "Saved account list to" << m_listFilePath; return true; } else { qDebug() << "Failed to save accounts to" << m_listFilePath << "error:" << file.errorString(); return false; } } void AccountList::setListFilePath(QString path, bool autosave) { m_listFilePath = path; m_autosave = autosave; } bool AccountList::anyAccountIsValid() { for (auto account : m_accounts) { if (account->ownsMinecraft()) { return true; } } return false; } void AccountList::fillQueue() { if (m_defaultAccount && m_defaultAccount->shouldRefresh()) { auto idToRefresh = m_defaultAccount->internalId(); m_refreshQueue.push_back(idToRefresh); qDebug() << "AccountList: Queued default account with internal ID" << idToRefresh << "to refresh first"; } for (int i = 0; i < count(); i++) { auto account = at(i); if (account == m_defaultAccount) { continue; } if (account->shouldRefresh()) { auto idToRefresh = account->internalId(); queueRefresh(idToRefresh); } } tryNext(); } void AccountList::requestRefresh(QString accountId) { auto index = m_refreshQueue.indexOf(accountId); if (index != -1) { m_refreshQueue.removeAt(index); } m_refreshQueue.push_front(accountId); qDebug() << "AccountList: Pushed account with internal ID" << accountId << "to the front of the queue"; if (!isActive()) { tryNext(); } } void AccountList::queueRefresh(QString accountId) { if (m_refreshQueue.indexOf(accountId) != -1) { return; } m_refreshQueue.push_back(accountId); qDebug() << "AccountList: Queued account with internal ID" << accountId << "to refresh"; } void AccountList::tryNext() { while (m_refreshQueue.length()) { auto accountId = m_refreshQueue.front(); m_refreshQueue.pop_front(); bool found = false; for (int i = 0; i < count(); i++) { auto account = at(i); if (account->internalId() == accountId) { found = true; if (!account->shouldRefresh()) { // Account no longer needs refreshing, skip it. qDebug() << "RefreshSchedule: Skipping account" << account->profileName() << "with internal ID" << accountId << "(no longer needs refresh)"; break; } m_currentTask = account->refresh(); if (m_currentTask) { connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded); connect(m_currentTask.get(), &Task::failed, this, &AccountList::authFailed); m_currentTask->start(); qDebug() << "RefreshSchedule: Processing account" << account->profileName() << "with internal ID" << accountId; return; } break; } } if (!found) { qDebug() << "RefreshSchedule: Account with internal ID" << accountId << "not found."; } } // if we get here, no account needed refreshing. Schedule refresh in an hour. m_refreshTimer->start(1000 * 3600); } void AccountList::authSucceeded() { qDebug() << "RefreshSchedule: Background account refresh succeeded"; m_currentTask.reset(); m_nextTimer->start(1000 * 20); } void AccountList::authFailed(QString reason) { qDebug() << "RefreshSchedule: Background account refresh failed:" << reason; m_currentTask.reset(); m_nextTimer->start(1000 * 20); } bool AccountList::isActive() const { return m_activityCount != 0; } void AccountList::beginActivity() { bool activating = m_activityCount == 0; m_activityCount++; if (activating) { emit activityChanged(true); } } void AccountList::endActivity() { if (m_activityCount == 0) { qWarning() << "Activity count would become below zero"; return; } bool deactivating = m_activityCount == 1; m_activityCount--; if (deactivating) { emit activityChanged(false); } } PrismLauncher-11.0.3/launcher/minecraft/auth/Parsers.cpp0000644000175100017510000003526415224505336022632 0ustar runnerrunner#include "Parsers.h" #include "Json.h" #include "Logging.h" #include #include #include namespace Parsers { bool getDateTime(QJsonValue value, QDateTime& out) { if (!value.isString()) { return false; } out = QDateTime::fromString(value.toString(), Qt::ISODate); return out.isValid(); } bool getString(QJsonValue value, QString& out) { if (!value.isString()) { return false; } out = value.toString(); return true; } bool getNumber(QJsonValue value, double& out) { if (!value.isDouble()) { return false; } out = value.toDouble(); return true; } bool getNumber(QJsonValue value, int64_t& out) { if (!value.isDouble()) { return false; } out = (int64_t)value.toDouble(); return true; } bool getBool(QJsonValue value, bool& out) { if (!value.isBool()) { return false; } out = value.toBool(); return true; } /* { "IssueInstant":"2020-12-07T19:52:08.4463796Z", "NotAfter":"2020-12-21T19:52:08.4463796Z", "Token":"token", "DisplayClaims":{ "xui":[ { "uhs":"userhash" } ] } } */ // TODO: handle error responses ... /* { "Identity":"0", "XErr":2148916238, "Message":"", "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" } // 2148916233 = missing Xbox account // 2148916238 = child account not linked to a family */ bool parseXTokenResponse(QByteArray& data, Token& output, QString name) { qDebug() << "Parsing" << name << ":"; qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON:" << jsonError.errorString(); return false; } auto obj = doc.object(); if (!getDateTime(obj.value("IssueInstant"), output.issueInstant)) { qWarning() << "User IssueInstant is not a timestamp"; return false; } if (!getDateTime(obj.value("NotAfter"), output.notAfter)) { qWarning() << "User NotAfter is not a timestamp"; return false; } if (!getString(obj.value("Token"), output.token)) { qWarning() << "User Token is not a string"; return false; } auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); if (!arrayVal.isArray()) { qWarning() << "Missing xui claims array"; return false; } bool foundUHS = false; for (auto item : arrayVal.toArray()) { if (!item.isObject()) { continue; } auto obj_ = item.toObject(); if (obj_.contains("uhs")) { foundUHS = true; } else { continue; } // consume all 'display claims' ... whatever that means for (auto iter = obj_.begin(); iter != obj_.end(); iter++) { QString claim; if (!getString(obj_.value(iter.key()), claim)) { qWarning() << "display claim" << iter.key() << "is not a string..."; return false; } output.extra[iter.key()] = claim; } break; } if (!foundUHS) { qWarning() << "Missing uhs"; return false; } output.validity = Validity::Certain; qDebug() << name << "is valid."; return true; } bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output) { qDebug() << "Parsing Minecraft profile..."; qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON:" << jsonError.errorString(); return false; } auto obj = doc.object(); if (!getString(obj.value("id"), output.id)) { qWarning() << "Minecraft profile id is not a string"; return false; } if (!getString(obj.value("name"), output.name)) { qWarning() << "Minecraft profile name is not a string"; return false; } auto skinsArray = obj.value("skins").toArray(); for (auto skin : skinsArray) { auto skinObj = skin.toObject(); Skin skinOut; if (!getString(skinObj.value("id"), skinOut.id)) { continue; } QString state; if (!getString(skinObj.value("state"), state)) { continue; } if (state != "ACTIVE") { continue; } if (!getString(skinObj.value("url"), skinOut.url)) { continue; } skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); if (!getString(skinObj.value("variant"), skinOut.variant)) { continue; } // we deal with only the active skin output.skin = skinOut; break; } auto capesArray = obj.value("capes").toArray(); QString currentCape; for (auto cape : capesArray) { auto capeObj = cape.toObject(); Cape capeOut; if (!getString(capeObj.value("id"), capeOut.id)) { continue; } QString state; if (!getString(capeObj.value("state"), state)) { continue; } if (state == "ACTIVE") { currentCape = capeOut.id; } if (!getString(capeObj.value("url"), capeOut.url)) { continue; } capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); if (!getString(capeObj.value("alias"), capeOut.alias)) { continue; } output.capes[capeOut.id] = capeOut; } output.currentCape = currentCape; output.validity = Validity::Certain; return true; } namespace { // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) // they are needed because the session server doesn't return skin urls for default skins static const QString SKIN_URL_STEVE = "https://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; static const QString SKIN_URL_ALEX = "https://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; bool isDefaultModelSteve(QString uuid) { // need to calculate *Java* hashCode of UUID // if number is even, skin/model is steve, otherwise it is alex // just in case dashes are in the id uuid.remove('-'); if (uuid.size() != 32) { return true; } // qulonglong is guaranteed to be 64 bits // we need to use unsigned numbers to guarantee truncation below qulonglong most = uuid.left(16).toULongLong(nullptr, 16); qulonglong least = uuid.right(16).toULongLong(nullptr, 16); qulonglong xored = most ^ least; return ((static_cast(xored >> 32)) ^ static_cast(xored)) % 2 == 0; } } // namespace /** Uses session server for skin/cape lookup instead of profile, because locked Mojang accounts cannot access profile endpoint (https://api.minecraftservices.com/minecraft/profile/) ref: https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape { "id": "", "name": "", "properties": [ { "name": "textures", "value": "" } ] } decoded base64 "value": { "timestamp": , "profileId": "", "profileName": "", "textures": { "SKIN": { "url": "" }, "CAPE": { "url": "" } } } */ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) { qDebug() << "Parsing Minecraft profile..."; qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response as JSON:" << jsonError.errorString(); return false; } auto obj = Json::requireObject(doc, "mojang minecraft profile"); if (!getString(obj.value("id"), output.id)) { qWarning() << "Minecraft profile id is not a string"; return false; } if (!getString(obj.value("name"), output.name)) { qWarning() << "Minecraft profile name is not a string"; return false; } auto propsArray = obj.value("properties").toArray(); QByteArray texturePayload; for (auto p : propsArray) { auto pObj = p.toObject(); auto name = pObj.value("name"); if (!name.isString() || name.toString() != "textures") { continue; } auto value = pObj.value("value"); if (value.isString()) { texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors); } if (!texturePayload.isEmpty()) { break; } } if (texturePayload.isNull()) { qWarning() << "No texture payload data"; return false; } doc = QJsonDocument::fromJson(texturePayload, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response as JSON:" << jsonError.errorString(); return false; } obj = Json::requireObject(doc, "session texture payload"); auto textures = obj.value("textures"); if (!textures.isObject()) { qWarning() << "No textures array in response"; return false; } Skin skinOut; // fill in default skin info ourselves, as this endpoint doesn't provide it bool steve = isDefaultModelSteve(output.id); skinOut.variant = steve ? "CLASSIC" : "SLIM"; skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX; // sadly we can't figure this out, but I don't think it really matters... skinOut.id = "00000000-0000-0000-0000-000000000000"; Cape capeOut; auto tObj = textures.toObject(); for (auto idx = tObj.constBegin(); idx != tObj.constEnd(); ++idx) { if (idx->isObject()) { if (idx.key() == "SKIN") { auto skin = idx->toObject(); if (!getString(skin.value("url"), skinOut.url)) { qWarning() << "Skin url is not a string"; return false; } skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); auto maybeMeta = skin.find("metadata"); if (maybeMeta != skin.end() && maybeMeta->isObject()) { auto meta = maybeMeta->toObject(); // might not be present getString(meta.value("model"), skinOut.variant); } } else if (idx.key() == "CAPE") { auto cape = idx->toObject(); if (!getString(cape.value("url"), capeOut.url)) { qWarning() << "Cape url is not a string"; return false; } capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); // we don't know the cape ID as it is not returned from the session server // so just fake it - changing capes is probably locked anyway :( capeOut.alias = "cape"; } } } output.skin = skinOut; if (capeOut.alias == "cape") { output.capes = QMap({ { capeOut.alias, capeOut } }); output.currentCape = capeOut.alias; } output.validity = Validity::Certain; return true; } bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output) { qDebug() << "Parsing Minecraft entitlements..."; qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON:" << jsonError.errorString(); return false; } auto obj = doc.object(); output.canPlayMinecraft = false; output.ownsMinecraft = false; auto itemsArray = obj.value("items").toArray(); for (auto item : itemsArray) { auto itemObj = item.toObject(); QString name; if (!getString(itemObj.value("name"), name)) { continue; } if (name == "game_minecraft") { output.canPlayMinecraft = true; } if (name == "product_minecraft") { output.ownsMinecraft = true; } } output.validity = Validity::Certain; return true; } bool parseRolloutResponse(QByteArray& data, bool& result) { qDebug() << "Parsing Rollout response..."; qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " << jsonError.errorString(); return false; } auto obj = doc.object(); QString feature; if (!getString(obj.value("feature"), feature)) { qWarning() << "Rollout feature is not a string"; return false; } if (feature != "msamigration") { qWarning() << "Rollout feature is not what we expected (msamigration), but is instead \"" << feature << "\""; return false; } if (!getBool(obj.value("rollout"), result)) { qWarning() << "Rollout feature is not a string"; return false; } return true; } bool parseMojangResponse(QByteArray& data, Token& output) { QJsonParseError jsonError; qDebug() << "Parsing Mojang response..."; qCDebug(authCredentials()) << data; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON:" << jsonError.errorString(); return false; } auto obj = doc.object(); double expires_in = 0; if (!getNumber(obj.value("expires_in"), expires_in)) { qWarning() << "expires_in is not a valid number"; return false; } auto currentTime = QDateTime::currentDateTimeUtc(); output.issueInstant = currentTime; output.notAfter = currentTime.addSecs(expires_in); QString username; if (!getString(obj.value("username"), username)) { qWarning() << "username is not valid"; return false; } // TODO: it's a JWT... validate it? if (!getString(obj.value("access_token"), output.token)) { qWarning() << "access_token is not valid"; return false; } output.validity = Validity::Certain; qDebug() << "Mojang response is valid."; return true; } } // namespace Parsers PrismLauncher-11.0.3/launcher/minecraft/auth/Parsers.h0000644000175100017510000000135215224505336022266 0ustar runnerrunner#pragma once #include "AccountData.h" namespace Parsers { bool getDateTime(QJsonValue value, QDateTime& out); bool getString(QJsonValue value, QString& out); bool getNumber(QJsonValue value, double& out); bool getNumber(QJsonValue value, int64_t& out); bool getBool(QJsonValue value, bool& out); bool parseXTokenResponse(QByteArray& data, Token& output, QString name); bool parseMojangResponse(QByteArray& data, Token& output); bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output); bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output); bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output); bool parseRolloutResponse(QByteArray& data, bool& result); } // namespace Parsers PrismLauncher-11.0.3/launcher/minecraft/auth/AuthFlow.h0000644000175100017510000000221415224505336022376 0ustar runnerrunner#pragma once #include #include #include #include #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AuthStep.h" #include "tasks/Task.h" class AuthFlow : public Task { Q_OBJECT public: enum class Action { Refresh, Login, DeviceCode }; explicit AuthFlow(AccountData* data, Action action = Action::Refresh); virtual ~AuthFlow() = default; void executeTask() override; AccountTaskState taskState() { return m_taskState; } public slots: bool abort() override; signals: void authorizeWithBrowser(const QUrl& url); void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); protected: void succeed(); void nextStep(); private slots: // NOTE: true -> non-terminal state, false -> terminal state bool changeState(AccountTaskState newState, QString reason = QString()); void stepFinished(AccountTaskState resultingState, QString message); private: AccountTaskState m_taskState = AccountTaskState::STATE_CREATED; QList m_steps; AuthStep::Ptr m_currentStep; AccountData* m_data = nullptr; }; PrismLauncher-11.0.3/launcher/minecraft/auth/AccountList.h0000644000175100017510000001314115224505336023076 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "MinecraftAccount.h" #include "minecraft/auth/AuthFlow.h" #include #include #include #include /*! * List of available Mojang accounts. * This should be loaded in the background by Prism Launcher on startup. */ class AccountList : public QAbstractListModel { Q_OBJECT public: enum ModelRoles { PointerRole = 0x34B1CB48 }; enum VListColumns { // TODO: Add icon column. ProfileNameColumn = 0, TypeColumn, StatusColumn, NUM_COLUMNS }; explicit AccountList(QObject* parent = 0); virtual ~AccountList() noexcept; const MinecraftAccountPtr at(int i) const; int count() const; //////// List Model Functions //////// QVariant data(const QModelIndex& index, int role) const override; virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; virtual int rowCount(const QModelIndex& parent) const override; virtual int columnCount(const QModelIndex& parent) const override; virtual Qt::ItemFlags flags(const QModelIndex& index) const override; virtual bool setData(const QModelIndex& index, const QVariant& value, int role) override; void addAccount(MinecraftAccountPtr account); void removeAccount(QModelIndex index); void moveAccount(QModelIndex index, int delta); int findAccountByProfileId(const QString& profileId) const; MinecraftAccountPtr getAccountByProfileName(const QString& profileName) const; QStringList profileNames() const; // requesting a refresh pushes it to the front of the queue void requestRefresh(QString accountId); // queuing a refresh will let it go to the back of the queue (unless it's somewhere inside the queue already) void queueRefresh(QString accountId); /*! * Sets the path to load/save the list file from/to. * If autosave is true, this list will automatically save to the given path whenever it changes. * THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately * after calling this function to ensure an autosaved change doesn't overwrite the list you intended * to load. */ void setListFilePath(QString path, bool autosave = false); bool loadList(); bool loadV3(QJsonObject& root); bool saveList(); MinecraftAccountPtr defaultAccount() const; void setDefaultAccount(MinecraftAccountPtr profileId); bool anyAccountIsValid(); bool isActive() const; protected: void beginActivity(); void endActivity(); private: uint32_t m_activityCount = 0; signals: void listChanged(); void listActivityChanged(); void defaultAccountChanged(); void activityChanged(bool active); public slots: /** * This is called when one of the accounts changes and the list needs to be updated */ void accountChanged(); /** * This is called when a (refresh/login) task involving the account starts or ends */ void accountActivityChanged(bool active); /** * This is initially to run background account refresh tasks, or on a hourly timer */ void fillQueue(); private slots: void tryNext(); void authSucceeded(); void authFailed(QString reason); protected: QList m_refreshQueue; QTimer* m_refreshTimer; QTimer* m_nextTimer; shared_qobject_ptr m_currentTask; /*! * Called whenever the list changes. * This emits the listChanged() signal and autosaves the list (if autosave is enabled). */ void onListChanged(); /*! * Called whenever the active account changes. * Emits the defaultAccountChanged() signal and autosaves the list if enabled. */ void onDefaultAccountChanged(); QList m_accounts; MinecraftAccountPtr m_defaultAccount; //! Path to the account list file. Empty string if there isn't one. QString m_listFilePath; /*! * If true, the account list will automatically save to the account list path when it changes. * Ignored if m_listFilePath is blank. */ bool m_autosave = false; }; PrismLauncher-11.0.3/launcher/minecraft/auth/AuthStep.h0000644000175100017510000000234415224505336022406 0ustar runnerrunner#pragma once #include #include #include "QObjectPtr.h" #include "minecraft/auth/AccountData.h" /** * Enum for describing the state of the current task. * Used by the getStateMessage function to determine what the status message should be. */ enum class AccountTaskState { STATE_CREATED, STATE_WORKING, STATE_SUCCEEDED, STATE_DISABLED, //!< MSA Client ID has changed. Tell user to reloginn STATE_FAILED_SOFT, //!< soft failure. authentication went through partially STATE_FAILED_HARD, //!< hard failure. main tokens are invalid STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way }; class AuthStep : public QObject { Q_OBJECT public: using Ptr = shared_qobject_ptr; explicit AuthStep(AccountData* data) : QObject(nullptr), m_data(data) {}; virtual ~AuthStep() noexcept = default; virtual QString describe() = 0; public slots: virtual void perform() = 0; virtual void abort() {} signals: void finished(AccountTaskState resultingState, QString message); protected: AccountData* m_data; }; PrismLauncher-11.0.3/launcher/minecraft/auth/AuthFlow.cpp0000644000175100017510000001256615224505336022744 0ustar runnerrunner#include #include #include #include "minecraft/auth/AccountData.h" #include "minecraft/auth/steps/EntitlementsStep.h" #include "minecraft/auth/steps/GetSkinStep.h" #include "minecraft/auth/steps/LauncherLoginStep.h" #include "minecraft/auth/steps/MSADeviceCodeStep.h" #include "minecraft/auth/steps/MSAStep.h" #include "minecraft/auth/steps/MinecraftProfileStep.h" #include "minecraft/auth/steps/XboxAuthorizationStep.h" #include "minecraft/auth/steps/XboxUserStep.h" #include "tasks/Task.h" #include "AuthFlow.h" #include AuthFlow::AuthFlow(AccountData* data, Action action) : Task(), m_data(data) { if (data->type == AccountType::MSA) { if (action == Action::DeviceCode) { auto oauthStep = makeShared(m_data); connect(oauthStep.get(), &MSADeviceCodeStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowserWithExtra); connect(this, &Task::aborted, oauthStep.get(), &MSADeviceCodeStep::abort); m_steps.append(oauthStep); } else { auto oauthStep = makeShared(m_data, action == Action::Refresh); connect(oauthStep.get(), &MSAStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowser); m_steps.append(oauthStep); } m_steps.append(makeShared(m_data)); m_steps.append( makeShared(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data)); } changeState(AccountTaskState::STATE_CREATED); } void AuthFlow::succeed() { m_data->validity_ = Validity::Certain; changeState(AccountTaskState::STATE_SUCCEEDED, tr("Finished all authentication steps")); } void AuthFlow::executeTask() { changeState(AccountTaskState::STATE_WORKING, tr("Initializing")); nextStep(); } void AuthFlow::nextStep() { if (!Task::isRunning()) { return; } if (m_steps.size() == 0) { // we got to the end without an incident... assume this is all. m_currentStep.reset(); succeed(); return; } m_currentStep = m_steps.front(); qDebug() << "AuthFlow:" << m_currentStep->describe(); setStatus(m_currentStep->describe()); m_steps.pop_front(); connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished); m_currentStep->perform(); } void AuthFlow::stepFinished(AccountTaskState resultingState, QString message) { if (changeState(resultingState, message)) nextStep(); } bool AuthFlow::changeState(AccountTaskState newState, QString reason) { m_taskState = newState; setDetails(reason); switch (newState) { case AccountTaskState::STATE_CREATED: { setStatus(tr("Waiting...")); m_data->errorString.clear(); return true; } case AccountTaskState::STATE_WORKING: { if (!m_currentStep) { setStatus(tr("Preparing to log in...")); } m_data->accountState = AccountState::Working; return true; } case AccountTaskState::STATE_SUCCEEDED: { setStatus(tr("Authentication task succeeded.")); m_data->accountState = AccountState::Online; emitSucceeded(); return false; } case AccountTaskState::STATE_OFFLINE: { setStatus(tr("Failed to contact the authentication server.")); m_data->errorString = reason; m_data->accountState = AccountState::Offline; emitFailed(reason); return false; } case AccountTaskState::STATE_DISABLED: { setStatus(tr("Client ID has changed. New session needs to be created.")); m_data->errorString = reason; m_data->accountState = AccountState::Disabled; emitFailed(reason); return false; } case AccountTaskState::STATE_FAILED_SOFT: { setStatus(tr("Encountered an error during authentication.")); m_data->errorString = reason; m_data->accountState = AccountState::Errored; emitFailed(reason); return false; } case AccountTaskState::STATE_FAILED_HARD: { setStatus(tr("Failed to authenticate. The session has expired.")); m_data->errorString = reason; m_data->accountState = AccountState::Expired; emitFailed(reason); return false; } case AccountTaskState::STATE_FAILED_GONE: { setStatus(tr("Failed to authenticate. The account no longer exists.")); m_data->errorString = reason; m_data->accountState = AccountState::Gone; emitFailed(reason); return false; } default: { setStatus(tr("...")); QString error = tr("Unknown account task state: %1").arg(int(newState)); m_data->accountState = AccountState::Errored; emitFailed(error); return false; } } } bool AuthFlow::abort() { if (m_currentStep) m_currentStep->abort(); emitAborted(); return true; } PrismLauncher-11.0.3/launcher/minecraft/auth/MinecraftAccount.cpp0000644000175100017510000002344015224505336024431 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan * * 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 "MinecraftAccount.h" #include #include #include #include #include #include #include #include #include #include #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AuthFlow.h" MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { data.internalId = QUuid::createUuid().toString(QUuid::Id128); } MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) { MinecraftAccountPtr account(new MinecraftAccount()); if (account->data.resumeStateFromV3(json)) { return account; } return nullptr; } MinecraftAccountPtr MinecraftAccount::createBlankMSA() { MinecraftAccountPtr account(new MinecraftAccount()); account->data.type = AccountType::MSA; return account; } MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username) { auto account = makeShared(); account->data.type = AccountType::Offline; account->data.yggdrasilToken.token = "0"; account->data.yggdrasilToken.validity = Validity::Certain; account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); account->data.yggdrasilToken.extra["userName"] = username; account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString(QUuid::Id128); account->data.minecraftProfile.id = uuidFromUsername(username).toString(QUuid::Id128); account->data.minecraftProfile.name = username; account->data.minecraftProfile.validity = Validity::Certain; return account; } QJsonObject MinecraftAccount::saveToJson() const { return data.saveState(); } AccountState MinecraftAccount::accountState() const { return data.accountState; } QPixmap MinecraftAccount::getFace(int width, int height) const { QPixmap skinTexture; if (!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) { return QPixmap(); } QPixmap skin = QPixmap(8, 8); skin.fill(QColorConstants::Transparent); QPainter painter(&skin); painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); return skin.scaled(width, height, Qt::KeepAspectRatio); } shared_qobject_ptr MinecraftAccount::login(bool useDeviceCode) { Q_ASSERT(m_currentTask.get() == nullptr); m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login)); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); }); emit activityChanged(true); return m_currentTask; } shared_qobject_ptr MinecraftAccount::refresh() { if (m_currentTask) { return m_currentTask; } m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh)); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); }); emit activityChanged(true); return m_currentTask; } shared_qobject_ptr MinecraftAccount::currentTask() { return m_currentTask; } void MinecraftAccount::authSucceeded() { m_currentTask.reset(); emit changed(); emit activityChanged(false); } void MinecraftAccount::authFailed(QString reason) { switch (m_currentTask->taskState()) { case AccountTaskState::STATE_OFFLINE: case AccountTaskState::STATE_DISABLED: { // NOTE: user will need to fix this themselves. } case AccountTaskState::STATE_FAILED_SOFT: { // NOTE: this doesn't do much. There was an error of some sort. } break; case AccountTaskState::STATE_FAILED_HARD: { if (accountType() == AccountType::MSA) { data.msaToken.token = QString(); data.msaToken.refresh_token = QString(); data.msaToken.validity = Validity::None; data.validity_ = Validity::None; } else { data.yggdrasilToken.token = QString(); data.yggdrasilToken.validity = Validity::None; data.validity_ = Validity::None; } emit changed(); } break; case AccountTaskState::STATE_FAILED_GONE: { data.validity_ = Validity::None; emit changed(); } break; case AccountTaskState::STATE_WORKING: { data.accountState = AccountState::Unchecked; } break; case AccountTaskState::STATE_CREATED: case AccountTaskState::STATE_SUCCEEDED: { // Not reachable here, as they are not failures. } } m_currentTask.reset(); emit activityChanged(false); } QString MinecraftAccount::displayName() const { if (const QList validStates{ AccountState::Unchecked, AccountState::Working, AccountState::Offline, AccountState::Online }; !validStates.contains(accountState())) { return QString("⚠ %1").arg(profileName()); } return profileName(); } bool MinecraftAccount::isActive() const { return !m_currentTask.isNull(); } bool MinecraftAccount::shouldRefresh() const { /* * Never refresh accounts that are being used by the game, it breaks the game session. * Always refresh accounts that have not been refreshed yet during this session. * Don't refresh broken accounts. * Refresh accounts that would expire in the next 12 hours (fresh token validity is 24 hours). */ if (isInUse()) { return false; } switch (data.validity_) { case Validity::Certain: { break; } case Validity::None: { return false; } case Validity::Assumed: { return true; } } auto now = QDateTime::currentDateTimeUtc(); auto issuedTimestamp = data.yggdrasilToken.issueInstant; auto expiresTimestamp = data.yggdrasilToken.notAfter; if (!expiresTimestamp.isValid()) { expiresTimestamp = issuedTimestamp.addSecs(24 * 3600); } if (now.secsTo(expiresTimestamp) < (12 * 3600)) { return true; } return false; } void MinecraftAccount::fillSession(AuthSessionPtr session) { // volatile auth token session->access_token = data.accessToken(); // profile name session->player_name = data.profileName(); // profile ID session->uuid = data.profileId(); if (session->uuid.isEmpty()) session->uuid = uuidFromUsername(session->player_name).toString(QUuid::Id128); // 'legacy' or 'mojang', depending on account type session->user_type = typeString(); if (!session->access_token.isEmpty()) { session->session = "token:" + data.accessToken() + ":" + data.profileId(); } else { session->session = "-"; } } void MinecraftAccount::decrementUses() { Usable::decrementUses(); if (!isInUse()) { emit changed(); // FIXME: we now need a better way to identify accounts... qWarning() << "Profile" << data.profileId() << "is no longer in use."; } } void MinecraftAccount::incrementUses() { bool wasInUse = isInUse(); Usable::incrementUses(); if (!wasInUse) { emit changed(); // FIXME: we now need a better way to identify accounts... qWarning() << "Profile" << data.profileId() << "is now in use."; } } QUuid MinecraftAccount::uuidFromUsername(QString username) { auto input = QString("OfflinePlayer:%1").arg(username).toUtf8(); // basically a reimplementation of Java's UUID#nameUUIDFromBytes QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5); auto bOr = [](QByteArray& array, qsizetype index, uint8_t value) { array[index] |= value; }; auto bAnd = [](QByteArray& array, qsizetype index, uint8_t value) { array[index] &= value; }; bAnd(digest, 6, 0x0f); // clear version bOr(digest, 6, 0x30); // set to version 3 bAnd(digest, 8, 0x3f); // clear variant bOr(digest, 8, 0x80); // set to IETF variant return QUuid::fromRfc4122(digest); } PrismLauncher-11.0.3/launcher/minecraft/auth/AuthSession.cpp0000644000175100017510000000143515224505336023451 0ustar runnerrunner#include "AuthSession.h" #include #include #include #include QString AuthSession::serializeUserProperties() { QJsonObject userAttrs; /* for (auto key : u.properties.keys()) { auto array = QJsonArray::fromStringList(u.properties.values(key)); userAttrs.insert(key, array); } */ QJsonDocument value(userAttrs); return value.toJson(QJsonDocument::Compact); } bool AuthSession::MakeOffline(QString offline_playername) { session = "-"; access_token = "0"; player_name = offline_playername; return true; } void AuthSession::MakeDemo(QString name, QString u) { uuid = u; session = "-"; access_token = "0"; player_name = name; launchMode = LaunchMode::Demo; }; PrismLauncher-11.0.3/launcher/minecraft/auth/AuthSession.h0000644000175100017510000000121215224505336023107 0ustar runnerrunner#pragma once #include #include #include "LaunchMode.h" class MinecraftAccount; struct AuthSession { bool MakeOffline(QString offline_playername); void MakeDemo(QString name, QString uuid); QString serializeUserProperties(); // combined session ID QString session; // volatile auth token QString access_token; // profile name QString player_name; // profile ID QString uuid; // 'msa' or 'offline', depending on account type QString user_type; // the actual launch mode for this session LaunchMode launchMode; }; using AuthSessionPtr = std::shared_ptr; PrismLauncher-11.0.3/launcher/minecraft/auth/AccountData.h0000644000175100017510000000651115224505336023037 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include #include enum class Validity { None, Assumed, Certain }; struct Token { QDateTime issueInstant; QDateTime notAfter; QString token; QString refresh_token; QVariantMap extra; Validity validity = Validity::None; bool persistent = true; }; struct Skin { QString id; QString url; QString variant; QByteArray data; }; struct Cape { QString id; QString url; QString alias; QByteArray data; }; struct MinecraftEntitlement { bool ownsMinecraft = false; bool canPlayMinecraft = false; Validity validity = Validity::None; }; struct MinecraftProfile { QString id; QString name; Skin skin; QString currentCape; QMap capes; Validity validity = Validity::None; }; enum class AccountType { MSA, Offline }; enum class AccountState { Unchecked, Offline, Working, Online, Disabled, Errored, Expired, Gone }; struct AccountData { QJsonObject saveState() const; bool resumeStateFromV3(QJsonObject data); //! Yggdrasil access token, as passed to the game. QString accessToken() const; QString profileId() const; QString profileName() const; QString lastError() const; AccountType type = AccountType::MSA; QString msaClientID; Token msaToken; Token userToken; Token mojangservicesToken; Token yggdrasilToken; MinecraftProfile minecraftProfile; MinecraftEntitlement minecraftEntitlement; Validity validity_ = Validity::None; // runtime only information (not saved with the account) QString internalId; QString errorString; QNetworkReply::NetworkError networkError = QNetworkReply::NoError; AccountState accountState = AccountState::Unchecked; }; PrismLauncher-11.0.3/launcher/minecraft/auth/AccountData.cpp0000644000175100017510000002713515224505336023377 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "AccountData.h" #include #include #include #include #include namespace { void tokenToJSONV3(QJsonObject& parent, const Token& t, const char* tokenName) { if (!t.persistent) { return; } QJsonObject out; if (t.issueInstant.isValid()) { out["iat"] = QJsonValue(t.issueInstant.toMSecsSinceEpoch() / 1000); } if (t.notAfter.isValid()) { out["exp"] = QJsonValue(t.notAfter.toMSecsSinceEpoch() / 1000); } bool save = false; if (!t.token.isEmpty()) { out["token"] = QJsonValue(t.token); save = true; } if (!t.refresh_token.isEmpty()) { out["refresh_token"] = QJsonValue(t.refresh_token); save = true; } if (t.extra.size()) { out["extra"] = QJsonObject::fromVariantMap(t.extra); save = true; } if (save) { parent[tokenName] = out; } } Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenName) { Token out; auto tokenObject = parent.value(tokenName).toObject(); if (tokenObject.isEmpty()) { return out; } auto issueInstant = tokenObject.value("iat"); if (issueInstant.isDouble()) { out.issueInstant = QDateTime::fromMSecsSinceEpoch(((int64_t)issueInstant.toDouble()) * 1000); } auto notAfter = tokenObject.value("exp"); if (notAfter.isDouble()) { out.notAfter = QDateTime::fromMSecsSinceEpoch(((int64_t)notAfter.toDouble()) * 1000); } auto token = tokenObject.value("token"); if (token.isString()) { out.token = token.toString(); out.validity = Validity::Assumed; } auto refresh_token = tokenObject.value("refresh_token"); if (refresh_token.isString()) { out.refresh_token = refresh_token.toString(); } auto extra = tokenObject.value("extra"); if (extra.isObject()) { out.extra = extra.toObject().toVariantMap(); } return out; } void profileToJSONV3(QJsonObject& parent, MinecraftProfile p, const char* tokenName) { if (p.id.isEmpty()) { return; } QJsonObject out; out["id"] = QJsonValue(p.id); out["name"] = QJsonValue(p.name); if (!p.currentCape.isEmpty()) { out["cape"] = p.currentCape; } { QJsonObject skinObj; skinObj["id"] = p.skin.id; skinObj["url"] = p.skin.url; skinObj["variant"] = p.skin.variant; if (p.skin.data.size()) { skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64()); } out["skin"] = skinObj; } QJsonArray capesArray; for (auto& cape : p.capes) { QJsonObject capeObj; capeObj["id"] = cape.id; capeObj["url"] = cape.url; capeObj["alias"] = cape.alias; if (cape.data.size()) { capeObj["data"] = QString::fromLatin1(cape.data.toBase64()); } capesArray.push_back(capeObj); } out["capes"] = capesArray; parent[tokenName] = out; } MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenName) { MinecraftProfile out; auto tokenObject = parent.value(tokenName).toObject(); if (tokenObject.isEmpty()) { return out; } { auto idV = tokenObject.value("id"); auto nameV = tokenObject.value("name"); if (!idV.isString() || !nameV.isString()) { qWarning() << "mandatory profile attributes are missing or of unexpected type"; return MinecraftProfile(); } out.name = nameV.toString(); out.id = idV.toString(); } { auto skinV = tokenObject.value("skin"); if (!skinV.isObject()) { qWarning() << "skin is missing"; return MinecraftProfile(); } auto skinObj = skinV.toObject(); auto idV = skinObj.value("id"); auto urlV = skinObj.value("url"); auto variantV = skinObj.value("variant"); if (!idV.isString() || !urlV.isString() || !variantV.isString()) { qWarning() << "mandatory skin attributes are missing or of unexpected type"; return MinecraftProfile(); } out.skin.id = idV.toString(); out.skin.url = urlV.toString(); out.skin.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); out.skin.variant = variantV.toString(); // data for skin is optional auto dataV = skinObj.value("data"); if (dataV.isString()) { // TODO: validate base64 out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1()); } else if (!dataV.isUndefined()) { qWarning() << "skin data is something unexpected"; return MinecraftProfile(); } } { auto capesV = tokenObject.value("capes"); if (!capesV.isArray()) { qWarning() << "capes is not an array!"; return MinecraftProfile(); } auto capesArray = capesV.toArray(); for (auto capeV : capesArray) { if (!capeV.isObject()) { qWarning() << "cape is not an object!"; return MinecraftProfile(); } auto capeObj = capeV.toObject(); auto idV = capeObj.value("id"); auto urlV = capeObj.value("url"); auto aliasV = capeObj.value("alias"); if (!idV.isString() || !urlV.isString() || !aliasV.isString()) { qWarning() << "mandatory skin attributes are missing or of unexpected type"; return MinecraftProfile(); } Cape cape; cape.id = idV.toString(); cape.url = urlV.toString(); cape.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); cape.alias = aliasV.toString(); // data for cape is optional. auto dataV = capeObj.value("data"); if (dataV.isString()) { // TODO: validate base64 cape.data = QByteArray::fromBase64(dataV.toString().toLatin1()); } else if (!dataV.isUndefined()) { qWarning() << "cape data is something unexpected"; return MinecraftProfile(); } out.capes[cape.id] = cape; } } // current cape { auto capeV = tokenObject.value("cape"); if (capeV.isString()) { auto currentCape = capeV.toString(); if (out.capes.contains(currentCape)) { out.currentCape = currentCape; } } } out.validity = Validity::Assumed; return out; } void entitlementToJSONV3(QJsonObject& parent, MinecraftEntitlement p) { if (p.validity == Validity::None) { return; } QJsonObject out; out["ownsMinecraft"] = QJsonValue(p.ownsMinecraft); out["canPlayMinecraft"] = QJsonValue(p.canPlayMinecraft); parent["entitlement"] = out; } bool entitlementFromJSONV3(const QJsonObject& parent, MinecraftEntitlement& out) { auto entitlementObject = parent.value("entitlement").toObject(); if (entitlementObject.isEmpty()) { return false; } { auto ownsMinecraftV = entitlementObject.value("ownsMinecraft"); auto canPlayMinecraftV = entitlementObject.value("canPlayMinecraft"); if (!ownsMinecraftV.isBool() || !canPlayMinecraftV.isBool()) { qWarning() << "mandatory attributes are missing or of unexpected type"; return false; } out.canPlayMinecraft = canPlayMinecraftV.toBool(false); out.ownsMinecraft = ownsMinecraftV.toBool(false); out.validity = Validity::Assumed; } return true; } } // namespace bool AccountData::resumeStateFromV3(QJsonObject data) { auto typeV = data.value("type"); if (!typeV.isString()) { qWarning() << "Failed to parse account data: type is missing."; return false; } auto typeS = typeV.toString(); if (typeS == "MSA") { type = AccountType::MSA; } else if (typeS == "Offline") { type = AccountType::Offline; } else { qWarning() << "Failed to parse account data: type is not recognized."; return false; } if (type == AccountType::MSA) { auto clientIDV = data.value("msa-client-id"); if (clientIDV.isString()) { msaClientID = clientIDV.toString(); } // leave msaClientID empty if it doesn't exist or isn't a string msaToken = tokenFromJSONV3(data, "msa"); userToken = tokenFromJSONV3(data, "utoken"); mojangservicesToken = tokenFromJSONV3(data, "xrp-mc"); } yggdrasilToken = tokenFromJSONV3(data, "ygg"); // versions before 7.2 used "offline" as the offline token if (yggdrasilToken.token == "offline") yggdrasilToken.token = "0"; minecraftProfile = profileFromJSONV3(data, "profile"); if (!entitlementFromJSONV3(data, minecraftEntitlement)) { if (minecraftProfile.validity != Validity::None) { minecraftEntitlement.canPlayMinecraft = true; minecraftEntitlement.ownsMinecraft = true; minecraftEntitlement.validity = Validity::Assumed; } } validity_ = minecraftProfile.validity; return true; } QJsonObject AccountData::saveState() const { QJsonObject output; if (type == AccountType::MSA) { output["type"] = "MSA"; output["msa-client-id"] = msaClientID; tokenToJSONV3(output, msaToken, "msa"); tokenToJSONV3(output, userToken, "utoken"); tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); } else if (type == AccountType::Offline) { output["type"] = "Offline"; } tokenToJSONV3(output, yggdrasilToken, "ygg"); profileToJSONV3(output, minecraftProfile, "profile"); entitlementToJSONV3(output, minecraftEntitlement); return output; } QString AccountData::accessToken() const { return yggdrasilToken.token; } QString AccountData::profileId() const { return minecraftProfile.id; } QString AccountData::profileName() const { if (minecraftProfile.name.size() == 0) { return QObject::tr("No Minecraft profile"); } return minecraftProfile.name; } QString AccountData::lastError() const { return errorString; } PrismLauncher-11.0.3/launcher/minecraft/World.cpp0000644000175100017510000003543115224505336021335 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "World.h" #include #include #include #include #include #include #include #include #include #include #include "GZip.h" #include #include #include "FileSystem.h" #include "PSaveFile.h" #include "archive/ArchiveReader.h" using std::nullopt; using std::optional; GameType::GameType(std::optional original) : original(original) { if (!original) { return; } switch (*original) { case 0: type = GameType::Survival; break; case 1: type = GameType::Creative; break; case 2: type = GameType::Adventure; break; case 3: type = GameType::Spectator; break; default: break; } } QString GameType::toTranslatedString() const { switch (type) { case GameType::Survival: return QCoreApplication::translate("GameType", "Survival"); case GameType::Creative: return QCoreApplication::translate("GameType", "Creative"); case GameType::Adventure: return QCoreApplication::translate("GameType", "Adventure"); case GameType::Spectator: return QCoreApplication::translate("GameType", "Spectator"); default: break; } if (original) { return QCoreApplication::translate("GameType", "Unknown (%1)").arg(*original); } return QCoreApplication::translate("GameType", "Undefined"); } QString GameType::toLogString() const { switch (type) { case GameType::Survival: return "Survival"; case GameType::Creative: return "Creative"; case GameType::Adventure: return "Adventure"; case GameType::Spectator: return "Spectator"; default: break; } if (original) { return QString("Unknown (%1)").arg(*original); } return "Undefined"; } std::unique_ptr parseLevelDat(QByteArray data) { QByteArray output; if (!GZip::unzip(data, output)) { return nullptr; } std::istringstream foo(std::string(output.constData(), output.size())); try { auto pair = nbt::io::read_compound(foo); if (pair.first != "") return nullptr; if (pair.second == nullptr) return nullptr; return std::move(pair.second); } catch (const nbt::io::input_error& e) { qWarning() << "Unable to parse level.dat:" << e.what(); return nullptr; } } QByteArray serializeLevelDat(nbt::tag_compound* levelInfo) { std::ostringstream s; nbt::io::write_tag("", *levelInfo, s); QByteArray val(s.str().data(), (int)s.str().size()); return val; } QString getDatFromFS(const QFileInfo& root, QString file) { QDir worldDir(root.filePath()); if (!root.isDir() || !worldDir.exists(file)) { return QString(); } return worldDir.absoluteFilePath(file); } QString getLevelDatFromFS(const QFileInfo& file) { return getDatFromFS(file, "level.dat"); } QByteArray getDatDataFromFS(const QFileInfo& root, QString file) { auto fullFilePath = getDatFromFS(root, file); if (fullFilePath.isNull()) { return QByteArray(); } QFile f(fullFilePath); if (!f.open(QIODevice::ReadOnly)) { return QByteArray(); } return f.readAll(); } QByteArray getLevelDatDataFromFS(const QFileInfo& file) { return getDatDataFromFS(file, "level.dat"); } QByteArray getWorldGenDataFromFS(const QFileInfo& file) { return getDatDataFromFS(file, "data/minecraft/world_gen_settings.dat"); } bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) { auto fullFilePath = getLevelDatFromFS(file); if (fullFilePath.isNull()) { return false; } PSaveFile f(fullFilePath); if (!f.open(QIODevice::WriteOnly)) { return false; } QByteArray compressed; if (!GZip::zip(data, compressed)) { return false; } if (f.write(compressed) != compressed.size()) { f.cancelWriting(); return false; } return f.commit(); } World::World(const QFileInfo& file) { repath(file); } void World::repath(const QFileInfo& file) { m_containerFile = file; m_folderName = file.fileName(); if (file.isFile() && file.suffix() == "zip") { m_iconFile = QString(); readFromZip(file); } else if (file.isDir()) { QFileInfo assumedIconPath(file.absoluteFilePath() + "/icon.png"); if (assumedIconPath.exists()) { m_iconFile = assumedIconPath.absoluteFilePath(); } readFromFS(file); } } bool World::resetIcon() { if (m_iconFile.isNull()) { return false; } if (QFile(m_iconFile).remove()) { m_iconFile = QString(); return true; } return false; } int64_t loadSeed(QByteArray data); void World::readFromFS(const QFileInfo& file) { auto bytes = getLevelDatDataFromFS(file); if (bytes.isEmpty()) { m_isValid = false; return; } loadFromLevelDat(bytes); m_levelDatTime = file.lastModified(); if (m_randomSeed == 0) { bytes = getWorldGenDataFromFS(file); if (!bytes.isEmpty()) { m_randomSeed = loadSeed(bytes); } } } void World::readFromZip(const QFileInfo& file) { MMCZip::ArchiveReader r(file.absoluteFilePath()); m_isValid = false; r.parse([this](MMCZip::ArchiveReader::File* file, bool& stop) { const QString levelDat = "level.dat"; auto filePath = file->filename(); QFileInfo fi(filePath); if (fi.fileName().compare(levelDat, Qt::CaseInsensitive) == 0) { m_containerOffsetPath = filePath.chopped(levelDat.length()); m_levelDatTime = file->dateTime(); loadFromLevelDat(file->readAll()); m_isValid = true; stop = true; } return true; }); } bool World::install(const QString& to, const QString& name) { auto finalPath = FS::PathCombine(to, FS::DirNameFromString(m_actualName, to)); if (!FS::ensureFolderPathExists(finalPath)) { return false; } bool ok = false; if (m_containerFile.isFile()) { MMCZip::ArchiveReader zip(m_containerFile.absoluteFilePath()); ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath); } else if (m_containerFile.isDir()) { QString from = m_containerFile.filePath(); ok = FS::copy(from, finalPath)(); } if (ok && !name.isEmpty() && m_actualName != name) { QFileInfo finalPathInfo(finalPath); World newWorld(finalPathInfo); if (newWorld.isValid()) { newWorld.rename(name); } } return ok; } bool World::rename(const QString& newName) { if (m_containerFile.isFile()) { return false; } auto data = getLevelDatDataFromFS(m_containerFile); if (data.isEmpty()) { return false; } auto worldData = parseLevelDat(data); if (!worldData) { return false; } auto& val = worldData->at("Data"); if (val.get_type() != nbt::tag_type::Compound) { return false; } auto& dataCompound = val.as(); dataCompound.put("LevelName", nbt::value_initializer(newName.toUtf8().data())); data = serializeLevelDat(worldData.get()); putLevelDatDataToFS(m_containerFile, data); m_actualName = newName; QDir parentDir(m_containerFile.absoluteFilePath()); parentDir.cdUp(); QFile container(m_containerFile.absoluteFilePath()); auto dirName = FS::DirNameFromString(m_actualName, parentDir.absolutePath()); container.rename(parentDir.absoluteFilePath(dirName)); return true; } namespace { optional read_string(nbt::value& parent, const char* name) { try { auto& namedValue = parent.at(name); if (namedValue.get_type() != nbt::tag_type::String) { return nullopt; } auto& tag_str = namedValue.as(); return QString::fromUtf8(tag_str.get()); } catch ([[maybe_unused]] const std::out_of_range& e) { // fallback for old world formats qWarning() << "String NBT tag" << name << "could not be found."; return nullopt; } catch ([[maybe_unused]] const std::bad_cast& e) { // type mismatch qWarning() << "NBT tag" << name << "could not be converted to string."; return nullopt; } } optional read_long(nbt::value& parent, const char* name) { try { auto& namedValue = parent.at(name); if (namedValue.get_type() != nbt::tag_type::Long) { return nullopt; } auto& tag_str = namedValue.as(); return tag_str.get(); } catch ([[maybe_unused]] const std::out_of_range& e) { // fallback for old world formats qWarning() << "Long NBT tag" << name << "could not be found."; return nullopt; } catch ([[maybe_unused]] const std::bad_cast& e) { // type mismatch qWarning() << "NBT tag" << name << "could not be converted to long."; return nullopt; } } optional read_int(nbt::value& parent, const char* name) { try { auto& namedValue = parent.at(name); if (namedValue.get_type() != nbt::tag_type::Int) { return nullopt; } auto& tag_str = namedValue.as(); return tag_str.get(); } catch ([[maybe_unused]] const std::out_of_range& e) { // fallback for old world formats qWarning() << "Int NBT tag" << name << "could not be found."; return nullopt; } catch ([[maybe_unused]] const std::bad_cast& e) { // type mismatch qWarning() << "NBT tag" << name << "could not be converted to int."; return nullopt; } } GameType read_gametype(nbt::value& parent, const char* name) { return GameType(read_int(parent, name)); } } // namespace int64_t loadSeed(QByteArray data) { auto levelData = parseLevelDat(data); if (!levelData) { return 0; } nbt::value* valPtr = nullptr; try { valPtr = &levelData->at("data"); } catch (const std::out_of_range&) { return 0; } nbt::value& val = *valPtr; try { return read_long(val, "seed").value_or(0); } catch (const std::out_of_range&) { } return 0; } void World::loadFromLevelDat(QByteArray data) { auto levelData = parseLevelDat(data); if (!levelData) { m_isValid = false; return; } nbt::value* valPtr = nullptr; try { valPtr = &levelData->at("Data"); } catch (const std::out_of_range& e) { qWarning().nospace() << "Unable to read NBT tags from " << m_folderName << ": " << e.what(); m_isValid = false; return; } nbt::value& val = *valPtr; m_isValid = val.get_type() == nbt::tag_type::Compound; if (!m_isValid) return; auto name = read_string(val, "LevelName"); m_actualName = name ? *name : m_folderName; auto timestamp = read_long(val, "LastPlayed"); m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : m_levelDatTime; m_gameType = read_gametype(val, "GameType"); optional randomSeed; try { auto& WorldGen_val = val.at("WorldGenSettings"); randomSeed = read_long(WorldGen_val, "seed"); } catch (const std::out_of_range&) { } if (!randomSeed) { randomSeed = read_long(val, "RandomSeed"); } m_randomSeed = randomSeed ? *randomSeed : 0; qDebug() << "World Name:" << m_actualName; qDebug() << "Last Played:" << m_lastPlayed.toString(); if (randomSeed) { qDebug() << "Seed:" << *randomSeed; } qDebug() << "Size:" << m_size; qDebug() << "GameType:" << m_gameType.toLogString(); } bool World::replace(World& with) { if (!destroy()) return false; bool success = FS::copy(with.m_containerFile.filePath(), m_containerFile.path())(); if (success) { m_folderName = with.m_folderName; m_containerFile.refresh(); } return success; } bool World::destroy() { if (!m_isValid) return false; if (FS::trash(m_containerFile.filePath())) return true; if (m_containerFile.isDir()) { QDir d(m_containerFile.filePath()); return d.removeRecursively(); } else if (m_containerFile.isFile()) { QFile file(m_containerFile.absoluteFilePath()); return file.remove(); } return true; } bool World::operator==(const World& other) const { return m_isValid == other.m_isValid && folderName() == other.folderName(); } bool World::isSymLinkUnder(const QString& instPath) const { if (isSymLink()) return true; auto instDir = QDir(instPath); auto relAbsPath = instDir.relativeFilePath(m_containerFile.absoluteFilePath()); auto relCanonPath = instDir.relativeFilePath(m_containerFile.canonicalFilePath()); return relAbsPath != relCanonPath; } bool World::isMoreThanOneHardLink() const { if (m_containerFile.isDir()) { return FS::hardLinkCount(QDir(m_containerFile.absoluteFilePath()).filePath("level.dat")) > 1; } return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1; } void World::setSize(int64_t size) { m_size = size; } PrismLauncher-11.0.3/launcher/minecraft/launch/0000755000175100017510000000000015224505336021006 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/minecraft/launch/EnsureOfflineLibraries.h0000644000175100017510000000224015224505336025556 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2026 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "launch/LaunchStep.h" #include "minecraft/MinecraftInstance.h" class EnsureOfflineLibraries : public LaunchStep { Q_OBJECT public: explicit EnsureOfflineLibraries(LaunchTask* parent, MinecraftInstance* instance); ~EnsureOfflineLibraries() override = default; void executeTask() override; bool canAbort() const override { return false; } private: MinecraftInstance* m_instance; }; PrismLauncher-11.0.3/launcher/minecraft/launch/LauncherPartLaunch.h0000644000175100017510000000301515224505336024701 0ustar runnerrunner/* 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 #include #include #include "MinecraftTarget.h" class LauncherPartLaunch : public LaunchStep { Q_OBJECT public: explicit LauncherPartLaunch(LaunchTask* parent); virtual ~LauncherPartLaunch() = default; virtual void executeTask(); virtual bool abort(); virtual void proceed(); virtual bool canAbort() const { return true; } void setWorkingDirectory(const QString& wd); void setAuthSession(AuthSessionPtr session) { m_session = session; } void setTargetToJoin(MinecraftTarget::Ptr targetToJoin) { m_targetToJoin = std::move(targetToJoin); } private slots: void on_state(LoggedProcess::State state); private: LoggedProcess m_process; QString m_command; AuthSessionPtr m_session; QString m_launchScript; MinecraftTarget::Ptr m_targetToJoin; bool mayProceed = false; }; PrismLauncher-11.0.3/launcher/minecraft/launch/MinecraftTarget.h0000644000175100017510000000155215224505336024241 0ustar runnerrunner/* 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 #include struct MinecraftTarget { QString address; quint16 port; QString world; static MinecraftTarget parse(const QString& fullAddress, bool useWorld); using Ptr = std::shared_ptr; }; PrismLauncher-11.0.3/launcher/minecraft/launch/ReconstructAssets.h0000644000175100017510000000166015224505336024660 0ustar runnerrunner/* 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 #include class ReconstructAssets : public LaunchStep { Q_OBJECT public: explicit ReconstructAssets(LaunchTask* parent) : LaunchStep(parent) {}; virtual ~ReconstructAssets() {}; void executeTask() override; bool canAbort() const override { return false; } }; PrismLauncher-11.0.3/launcher/minecraft/launch/VerifyJavaInstall.cpp0000644000175100017510000000720015224505336025106 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "VerifyJavaInstall.h" #include #include "Application.h" #include "MessageLevel.h" #include "java/JavaInstall.h" #include "java/JavaInstallList.h" #include "java/JavaVersion.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" void VerifyJavaInstall::executeTask() { auto instance = m_parent->instance(); auto packProfile = instance->getPackProfile(); auto settings = instance->settings(); auto storedVersion = settings->get("JavaVersion").toString(); auto ignoreCompatibility = settings->get("IgnoreJavaCompatibility").toBool(); auto javaArchitecture = settings->get("JavaArchitecture").toString(); auto maxMemAlloc = settings->get("MaxMemAlloc").toInt(); if (javaArchitecture == "32" && maxMemAlloc > 2048) { emit logLine(tr("Max memory allocation exceeds the supported value.\n" "The selected installation of Java is 32-bit and doesn't support more than 2048MiB of RAM.\n" "The instance may not start due to this."), MessageLevel::Error); } auto compatibleMajors = packProfile->getProfile()->getCompatibleJavaMajors(); JavaVersion javaVersion(storedVersion); if (compatibleMajors.isEmpty() || compatibleMajors.contains(javaVersion.major())) { emitSucceeded(); return; } if (ignoreCompatibility) { emit logLine(tr("Java major version is incompatible. Things might break.\n"), MessageLevel::Warning); emitSucceeded(); return; } emit logLine(tr("This instance is not compatible with Java version %1.\n" "Please switch to one of the following Java versions for this instance:") .arg(javaVersion.major()), MessageLevel::Error); for (auto major : compatibleMajors) { emit logLine(tr("Java version %1").arg(major), MessageLevel::Error); } emit logLine(tr("Go to instance Java settings to change your Java version or disable the Java compatibility check if you know what " "you're doing."), MessageLevel::Error); emitFailed(QString("Incompatible Java major version")); } PrismLauncher-11.0.3/launcher/minecraft/launch/CreateGameFolders.cpp0000644000175100017510000000151515224505336025030 0ustar runnerrunner#include "CreateGameFolders.h" #include "FileSystem.h" #include "launch/LaunchTask.h" #include "minecraft/MinecraftInstance.h" CreateGameFolders::CreateGameFolders(LaunchTask* parent) : LaunchStep(parent) {} void CreateGameFolders::executeTask() { auto instance = m_parent->instance(); if (!FS::ensureFolderPathExists(instance->gameRoot())) { emit logLine("Couldn't create the main game folder", MessageLevel::Error); emitFailed(tr("Couldn't create the main game folder")); return; } // HACK: this is a workaround for MCL-3732 - 'server-resource-packs' folder is created. if (!FS::ensureFolderPathExists(FS::PathCombine(instance->gameRoot(), "server-resource-packs"))) { emit logLine("Couldn't create the 'server-resource-packs' folder", MessageLevel::Error); } emitSucceeded(); } PrismLauncher-11.0.3/launcher/minecraft/launch/ModMinecraftJar.h0000644000175100017510000000177315224505336024174 0ustar runnerrunner/* 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 #include class ModMinecraftJar : public LaunchStep { Q_OBJECT public: explicit ModMinecraftJar(LaunchTask* parent) : LaunchStep(parent) {}; virtual ~ModMinecraftJar() {}; virtual void executeTask() override; virtual bool canAbort() const override { return false; } void finalize() override; private: bool removeJar(); }; PrismLauncher-11.0.3/launcher/minecraft/launch/CreateGameFolders.h0000644000175100017510000000202615224505336024473 0ustar runnerrunner/* 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 #include #include // Create the main .minecraft for the instance and any other necessary folders class CreateGameFolders : public LaunchStep { Q_OBJECT public: explicit CreateGameFolders(LaunchTask* parent); virtual ~CreateGameFolders() {}; virtual void executeTask(); virtual bool canAbort() const { return false; } }; PrismLauncher-11.0.3/launcher/minecraft/launch/ClaimAccount.cpp0000644000175100017510000000112215224505336024050 0ustar runnerrunner#include "ClaimAccount.h" #include #include "Application.h" #include "minecraft/auth/AccountList.h" ClaimAccount::ClaimAccount(LaunchTask* parent, AuthSessionPtr session) : LaunchStep(parent) { if (session->launchMode == LaunchMode::Normal) { auto accounts = APPLICATION->accounts(); m_account = accounts->getAccountByProfileName(session->player_name); } } void ClaimAccount::executeTask() { if (m_account) { lock.reset(new UseLock(m_account.get())); } emitSucceeded(); } void ClaimAccount::finalize() { lock.reset(); } PrismLauncher-11.0.3/launcher/minecraft/launch/ScanModFolders.h0000644000175100017510000000224615224505336024026 0ustar runnerrunner/* 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 #include class ScanModFolders : public LaunchStep { Q_OBJECT public: explicit ScanModFolders(LaunchTask* parent) : LaunchStep(parent) {}; virtual ~ScanModFolders() {}; virtual void executeTask() override; virtual bool canAbort() const override { return false; } private slots: void coreModsDone(); void modsDone(); void nilModsDone(); private: void checkDone(); private: // DATA bool m_modsDone = false; bool m_nilModsDone = false; bool m_coreModsDone = false; }; PrismLauncher-11.0.3/launcher/minecraft/launch/ClaimAccount.h0000644000175100017510000000206415224505336023523 0ustar runnerrunner/* 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 #include class ClaimAccount : public LaunchStep { Q_OBJECT public: explicit ClaimAccount(LaunchTask* parent, AuthSessionPtr session); virtual ~ClaimAccount() = default; void executeTask() override; void finalize() override; bool canAbort() const override { return false; } private: std::unique_ptr lock; MinecraftAccountPtr m_account; }; PrismLauncher-11.0.3/launcher/minecraft/launch/AutoInstallJava.h0000644000175100017510000000444015224505336024222 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include "meta/Version.h" #include "minecraft/MinecraftInstance.h" #include "tasks/Task.h" class AutoInstallJava : public LaunchStep { Q_OBJECT public: explicit AutoInstallJava(LaunchTask* parent); ~AutoInstallJava() override = default; void executeTask() override; bool canAbort() const override { return m_current_task ? m_current_task->canAbort() : false; } bool abort() override; protected: void setJavaPath(QString path); void setJavaPathFromPartial(); void downloadJava(Meta::Version::Ptr version, QString javaName); void tryNextMajorJava(); private: MinecraftInstance* m_instance; Task::Ptr m_current_task; qsizetype m_majorJavaVersionIndex = 0; const QString m_supported_arch; }; PrismLauncher-11.0.3/launcher/minecraft/launch/EnsureAvailableMemory.cpp0000644000175100017510000001057015224505336025750 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2026 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "EnsureAvailableMemory.h" #include "HardwareInfo.h" #include "ui/dialogs/CustomMessageBox.h" EnsureAvailableMemory::EnsureAvailableMemory(LaunchTask* parent, MinecraftInstance* instance) : LaunchStep(parent), m_instance(instance) {} void EnsureAvailableMemory::executeTask() { #ifdef Q_OS_MACOS QString text; switch (MacOSHardwareInfo::memoryPressureLevel()) { case MacOSHardwareInfo::MemoryPressureLevel::Normal: emitSucceeded(); return; case MacOSHardwareInfo::MemoryPressureLevel::Warning: text = tr("The system is under increased memory pressure.\n" "This may lead to lag or slowdowns.\n" "If possible, close other applications before continuing.\n\n" "Launch anyway?"); break; case MacOSHardwareInfo::MemoryPressureLevel::Critical: text = tr("Your system is under critical memory pressure.\n" "This may lead to severe slowdowns, crashes or system instability.\n" "It is recommended to close other applications or restart your system.\n\n" "Launch anyway?"); break; } bool shouldAbort = false; if (m_instance->settings()->get("LowMemWarning").toBool()) { auto* dialog = CustomMessageBox::selectable(nullptr, tr("High memory pressure"), text, QMessageBox::Icon::Warning, QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::No); shouldAbort = dialog->exec() == QMessageBox::No; dialog->deleteLater(); } const auto message = tr("The system is under high memory pressure"); if (shouldAbort) { emit logLine(message, MessageLevel::Fatal); emitFailed(message); return; } emit logLine(message, MessageLevel::Warning); emitSucceeded(); #else const uint64_t available = HardwareInfo::availableRamMiB(); if (available == 0) { // could not read emitSucceeded(); return; } const uint64_t settingMin = m_instance->settings()->get("MinMemAlloc").toUInt(); const uint64_t settingMax = m_instance->settings()->get("MaxMemAlloc").toUInt(); const uint64_t max = std::max(settingMin, settingMax); if (static_cast(max) * 0.9 > static_cast(available)) { bool shouldAbort = false; if (m_instance->settings()->get("LowMemWarning").toBool()) { auto* dialog = CustomMessageBox::selectable( nullptr, tr("Low free memory"), tr("There might not be enough free RAM to launch this instance with the current memory settings.\n\n" "Maximum allocated: %1 MiB\nFree: %2 MiB (out of %3 MiB total)\n\n" "Launch anyway? This may cause slowdowns in the game and your system.") .arg(max) .arg(available) .arg(HardwareInfo::totalRamMiB()), QMessageBox::Icon::Warning, QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::No); shouldAbort = dialog->exec() == QMessageBox::No; dialog->deleteLater(); } const auto message = tr("Not enough RAM available to launch this instance"); if (shouldAbort) { emit logLine(message, MessageLevel::Fatal); emitFailed(message); return; } emit logLine(message, MessageLevel::Warning); } emitSucceeded(); #endif } PrismLauncher-11.0.3/launcher/minecraft/launch/ReconstructAssets.cpp0000644000175100017510000000226515224505336025215 0ustar runnerrunner/* 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 "ReconstructAssets.h" #include "launch/LaunchTask.h" #include "minecraft/AssetsUtils.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" void ReconstructAssets::executeTask() { auto instance = m_parent->instance(); auto components = instance->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); if (!AssetsUtils::reconstructAssets(assets->id, instance->resourcesDir())) { emit logLine("Failed to reconstruct Minecraft assets.", MessageLevel::Error); } emitSucceeded(); } PrismLauncher-11.0.3/launcher/minecraft/launch/MinecraftTarget.cpp0000644000175100017510000000410515224505336024571 0ustar runnerrunner/* 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 "MinecraftTarget.h" #include // FIXME: the way this is written, it can't ever do any sort of validation and can accept total junk MinecraftTarget MinecraftTarget::parse(const QString& fullAddress, bool useWorld) { if (useWorld) { MinecraftTarget target; target.world = fullAddress; return target; } QStringList split = fullAddress.split(":"); // The logic below replicates the exact logic minecraft uses for parsing server addresses. // While the conversion is not lossless and eats errors, it ensures the same behavior // within Minecraft and Prism Launcher when entering server addresses. if (fullAddress.startsWith("[")) { int bracket = fullAddress.indexOf("]"); if (bracket > 0) { QString ipv6 = fullAddress.mid(1, bracket - 1); QString port = fullAddress.mid(bracket + 1).trimmed(); if (port.startsWith(":") && !ipv6.isEmpty()) { port = port.mid(1); split = QStringList({ ipv6, port }); } else { split = QStringList({ ipv6 }); } } } if (split.size() > 2) { split = QStringList({ fullAddress }); } QString realAddress = split[0]; quint16 realPort = 25565; if (split.size() > 1) { bool ok; realPort = split[1].toUInt(&ok); if (!ok) { realPort = 25565; } } return MinecraftTarget{ realAddress, realPort }; } PrismLauncher-11.0.3/launcher/minecraft/launch/LauncherPartLaunch.cpp0000644000175100017510000002122115224505336025233 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "LauncherPartLaunch.h" #include #include #include "Application.h" #include "Commandline.h" #include "FileSystem.h" #include "launch/LaunchTask.h" #include "minecraft/MinecraftInstance.h" #ifdef Q_OS_LINUX #include "gamemode_client.h" #endif LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) : LaunchStep(parent) , m_process(parent->instance()->getJavaVersion().defaultsToUtf8() ? QStringConverter::Utf8 : QStringConverter::System) { if (parent->instance()->settings()->get("CloseAfterLaunch").toBool()) { static const QRegularExpression s_settingUser(".*Setting user.+", QRegularExpression::CaseInsensitiveOption); std::shared_ptr connection{ new QMetaObject::Connection }; *connection = connect(&m_process, &LoggedProcess::log, this, [connection](const QStringList& lines, [[maybe_unused]] MessageLevel level) { qDebug() << lines; if (lines.filter(s_settingUser).length() != 0) { APPLICATION->closeAllWindows(); disconnect(*connection); } }); } connect(&m_process, &LoggedProcess::log, this, &LauncherPartLaunch::logLines); connect(&m_process, &LoggedProcess::stateChanged, this, &LauncherPartLaunch::on_state); } void LauncherPartLaunch::executeTask() { QString jarPath = APPLICATION->getJarPath("NewLaunch.jar"); if (jarPath.isEmpty()) { const char* reason = QT_TR_NOOP("Launcher library could not be found. Please check your installation."); emit logLine(tr(reason), MessageLevel::Fatal); emitFailed(tr(reason)); return; } auto instance = m_parent->instance(); QString legacyJarPath; if (instance->getLauncher() == "legacy" || instance->shouldApplyOnlineFixes()) { legacyJarPath = APPLICATION->getJarPath("NewLaunchLegacy.jar"); if (legacyJarPath.isEmpty()) { const char* reason = QT_TR_NOOP("Legacy launcher library could not be found. Please check your installation."); emit logLine(tr(reason), MessageLevel::Fatal); emitFailed(tr(reason)); return; } } m_launchScript = instance->createLaunchScript(m_session, m_targetToJoin); QStringList args = instance->javaArguments(); QString allArgs = args.join(" "); emit logLine("Java arguments:\n " + m_parent->censorPrivateInfo(allArgs) + "\n", MessageLevel::Launcher); auto javaPath = FS::ResolveExecutable(instance->settings()->get("JavaPath").toString()); m_process.setProcessEnvironment(instance->createLaunchEnvironment()); // make detachable - this will keep the process running even if the object is destroyed m_process.setDetachable(true); auto classPath = instance->getClassPath(); classPath.prepend(jarPath); if (!legacyJarPath.isEmpty()) classPath.prepend(legacyJarPath); auto natPath = instance->getNativePath(); #ifdef Q_OS_WIN natPath = FS::getPathNameInLocal8bit(natPath); #endif args << "-Djava.library.path=" + natPath; args << "-cp"; #ifdef Q_OS_WIN QStringList processed; for (auto& item : classPath) { processed << FS::getPathNameInLocal8bit(item); } args << processed.join(';'); #else args << classPath.join(':'); #endif args << "org.prismlauncher.EntryPoint"; qDebug() << args.join(' '); 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); if (realWrapperCommand.isEmpty()) { const char* reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found."); emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal); emitFailed(tr(reason).arg(wrapperCommand)); return; } emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::Launcher); args.prepend(javaPath); m_process.start(wrapperCommand, wrapperArgs + args); } else { m_process.start(javaPath, args); } #ifdef Q_OS_LINUX if (instance->settings()->get("EnableFeralGamemode").toBool() && APPLICATION->capabilities() & Application::SupportsGameMode) { auto pid = m_process.processId(); if (pid) { gamemode_request_start_for(pid); } } #endif } void LauncherPartLaunch::on_state(LoggedProcess::State state) { switch (state) { case LoggedProcess::FailedToStart: { //: Error message displayed if instace can't start const char* reason = QT_TR_NOOP("Could not launch Minecraft: %1"); emit logLine(QString(reason).arg(m_process.errorString()), MessageLevel::Fatal); emitFailed(tr(reason).arg(m_process.errorString())); return; } case LoggedProcess::Aborted: case LoggedProcess::Crashed: { m_parent->setPid(-1); m_parent->instance()->setMinecraftRunning(false); emitFailed(tr("Game crashed.")); return; } case LoggedProcess::Finished: { auto instance = m_parent->instance(); if (instance->settings()->get("CloseAfterLaunch").toBool()) APPLICATION->showMainWindow(); m_parent->setPid(-1); m_parent->instance()->setMinecraftRunning(false); // if the exit code wasn't 0, report this as a crash auto exitCode = m_process.exitCode(); if (exitCode != 0) { emitFailed(tr("Game crashed.")); return; } // FIXME: make this work again // m_postlaunchprocess.processEnvironment().insert("INST_EXITCODE", QString(exitCode)); // run post-exit emitSucceeded(); break; } case LoggedProcess::Running: emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::Launcher); m_parent->setPid(m_process.processId()); // send the launch script to the launcher part m_process.write(m_launchScript.toUtf8()); mayProceed = true; emit readyForLaunch(); break; default: break; } } void LauncherPartLaunch::setWorkingDirectory(const QString& wd) { m_process.setWorkingDirectory(wd); } void LauncherPartLaunch::proceed() { if (mayProceed) { m_parent->instance()->setMinecraftRunning(true); QString launchString("launch\n"); m_process.write(launchString.toUtf8()); mayProceed = false; } } bool LauncherPartLaunch::abort() { if (mayProceed) { mayProceed = false; QString launchString("abort\n"); m_process.write(launchString.toUtf8()); } else { auto state = m_process.state(); if (state == LoggedProcess::Running || state == LoggedProcess::Starting) { m_process.kill(); } } return true; } PrismLauncher-11.0.3/launcher/minecraft/launch/ScanModFolders.cpp0000644000175100017510000000525215224505336024361 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ScanModFolders.h" #include "FileSystem.h" #include "MMCZip.h" #include "launch/LaunchTask.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/mod/ModFolderModel.h" void ScanModFolders::executeTask() { auto m_inst = m_parent->instance(); auto loaders = m_inst->loaderModList(); connect(loaders, &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); if (!loaders->update()) { m_modsDone = true; } auto cores = m_inst->coreModList(); connect(cores, &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); if (!cores->update()) { m_coreModsDone = true; } auto nils = m_inst->nilModList(); connect(nils, &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone); if (!nils->update()) { m_nilModsDone = true; } checkDone(); } void ScanModFolders::modsDone() { m_modsDone = true; checkDone(); } void ScanModFolders::coreModsDone() { m_coreModsDone = true; checkDone(); } void ScanModFolders::nilModsDone() { m_nilModsDone = true; checkDone(); } void ScanModFolders::checkDone() { if (m_modsDone && m_coreModsDone && m_nilModsDone) { emitSucceeded(); } } PrismLauncher-11.0.3/launcher/minecraft/launch/ExtractNatives.h0000644000175100017510000000174215224505336024127 0ustar runnerrunner/* 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 // FIXME: temporary wrapper for existing task. class ExtractNatives : public LaunchStep { Q_OBJECT public: explicit ExtractNatives(LaunchTask* parent) : LaunchStep(parent) {}; virtual ~ExtractNatives() {}; void executeTask() override; bool canAbort() const override { return false; } void finalize() override; }; PrismLauncher-11.0.3/launcher/minecraft/launch/AutoInstallJava.cpp0000644000175100017510000002662315224505336024564 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "AutoInstallJava.h" #include #include #include #include "Application.h" #include "FileSystem.h" #include "MessageLevel.h" #include "QObjectPtr.h" #include "SysInfo.h" #include "java/JavaInstall.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" #include "java/JavaVersion.h" #include "java/download/ArchiveDownloadTask.h" #include "java/download/ManifestDownloadTask.h" #include "java/download/SymlinkTask.h" #include "meta/Index.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "net/Mode.h" #include "tasks/SequentialTask.h" AutoInstallJava::AutoInstallJava(LaunchTask* parent) : LaunchStep(parent), m_instance(m_parent->instance()), m_supported_arch(SysInfo::getSupportedJavaArchitecture()) {}; void AutoInstallJava::executeTask() { auto settings = m_instance->settings(); if (!APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() || (settings->get("OverrideJavaLocation").toBool() && QFileInfo::exists(settings->get("JavaPath").toString()))) { emitSucceeded(); return; } auto packProfile = m_instance->getPackProfile(); if (!APPLICATION->settings()->get("AutomaticJavaDownload").toBool()) { auto javas = APPLICATION->javalist(); m_current_task = javas->getLoadTask(); connect(m_current_task.get(), &Task::finished, this, [this, javas, packProfile] { for (auto i = 0; i < javas->count(); i++) { auto java = std::dynamic_pointer_cast(javas->at(i)); if (java && packProfile->getProfile()->getCompatibleJavaMajors().contains(java->id.major())) { if (!java->is_64bit) { emit logLine(tr("The automatic Java mechanism detected a 32-bit installation of Java."), MessageLevel::Launcher); } setJavaPath(java->path); return; } } emit logLine(tr("No compatible Java version was found. Using the default one."), MessageLevel::Warning); emitSucceeded(); }); connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); emit progressReportingRequest(); return; } if (m_supported_arch.isEmpty()) { emit logLine(tr("Your system (%1-%2) is not compatible with automatic Java installation. Using the default Java path.") .arg(SysInfo::currentSystem(), SysInfo::useQTForArch()), MessageLevel::Warning); emitSucceeded(); return; } auto wantedJavaName = packProfile->getProfile()->getCompatibleJavaName(); if (wantedJavaName.isEmpty()) { emit logLine(tr("Your meta information is out of date or doesn't have the information necessary to determine what installation of " "Java should be used. " "Using the default Java path."), MessageLevel::Warning); emitSucceeded(); return; } QDir javaDir(APPLICATION->javaPath()); auto relativeBinary = FS::PathCombine(wantedJavaName, "bin", JavaUtils::javaExecutable); auto wantedJavaPath = javaDir.absoluteFilePath(relativeBinary); if (QFileInfo::exists(wantedJavaPath)) { setJavaPathFromPartial(); return; } auto versionList = APPLICATION->metadataIndex()->get("net.minecraft.java"); m_current_task = versionList->getLoadTask(); connect(m_current_task.get(), &Task::succeeded, this, &AutoInstallJava::tryNextMajorJava); connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::emitFailed); connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); if (!m_current_task->isRunning()) { m_current_task->start(); } emit progressReportingRequest(); } void AutoInstallJava::setJavaPath(QString path) { auto settings = m_instance->settings(); settings->set("OverrideJavaLocation", true); settings->set("JavaPath", path); settings->set("AutomaticJava", true); emit logLine(tr("Compatible Java found at: %1.").arg(path), MessageLevel::Launcher); emitSucceeded(); } void AutoInstallJava::setJavaPathFromPartial() { auto packProfile = m_instance->getPackProfile(); auto javaName = packProfile->getProfile()->getCompatibleJavaName(); QDir javaDir(APPLICATION->javaPath()); // just checking if the executable is there should suffice // but if needed this can be achieved through refreshing the javalist // and retrieving the path that contains the java name auto relativeBinary = FS::PathCombine(javaName, "bin", JavaUtils::javaExecutable); auto finalPath = javaDir.absoluteFilePath(relativeBinary); if (QFileInfo::exists(finalPath)) { setJavaPath(finalPath); } else { emit logLine(tr("No compatible Java version was found (the binary file does not exist). Using the default one."), MessageLevel::Warning); emitSucceeded(); } return; } void AutoInstallJava::downloadJava(Meta::Version::Ptr version, QString javaName) { auto runtimes = version->data()->runtimes; for (auto java : runtimes) { if (java->runtimeOS == m_supported_arch && java->name() == javaName) { QDir javaDir(APPLICATION->javaPath()); auto final_path = javaDir.absoluteFilePath(java->m_name); auto deletePath = [final_path] { FS::deletePath(final_path); }; switch (java->downloadType) { case Java::DownloadType::Manifest: m_current_task = makeShared(java->url, final_path, java->checksumType, java->checksumHash); break; case Java::DownloadType::Archive: m_current_task = makeShared(java->url, final_path, java->checksumType, java->checksumHash); break; case Java::DownloadType::Unknown: deletePath(); emitFailed(tr("Could not determine Java download type!")); return; } #if defined(Q_OS_MACOS) auto seq = makeShared(tr("Install Java")); seq->addTask(m_current_task); seq->addTask(makeShared(final_path)); m_current_task = seq; #endif connect(m_current_task.get(), &Task::failed, this, [this, deletePath](QString reason) { deletePath(); emitFailed(reason); }); connect(m_current_task.get(), &Task::aborted, this, deletePath); connect(m_current_task.get(), &Task::succeeded, this, &AutoInstallJava::setJavaPathFromPartial); connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::tryNextMajorJava); connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); m_current_task->start(); return; } } tryNextMajorJava(); } void AutoInstallJava::tryNextMajorJava() { if (!isRunning()) return; auto versionList = APPLICATION->metadataIndex()->get("net.minecraft.java"); auto packProfile = m_instance->getPackProfile(); auto wantedJavaName = packProfile->getProfile()->getCompatibleJavaName(); auto majorJavaVersions = packProfile->getProfile()->getCompatibleJavaMajors(); if (m_majorJavaVersionIndex >= majorJavaVersions.length()) { emit logLine( tr("No versions of Java were found for your operating system: %1-%2").arg(SysInfo::currentSystem(), SysInfo::useQTForArch()), MessageLevel::Warning); emit logLine(tr("No compatible version of Java was found. Using the default one."), MessageLevel::Warning); emitSucceeded(); return; } auto majorJavaVersion = majorJavaVersions[m_majorJavaVersionIndex]; m_majorJavaVersionIndex++; auto javaMajor = versionList->getVersion(QString("java%1").arg(majorJavaVersion)); if (javaMajor->isLoaded()) { downloadJava(javaMajor, wantedJavaName); } else { m_current_task = APPLICATION->metadataIndex()->loadVersion("net.minecraft.java", javaMajor->version(), Net::Mode::Online); connect(m_current_task.get(), &Task::succeeded, this, [this, javaMajor, wantedJavaName] { downloadJava(javaMajor, wantedJavaName); }); connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::tryNextMajorJava); connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); if (!m_current_task->isRunning()) { m_current_task->start(); } } } bool AutoInstallJava::abort() { if (m_current_task && m_current_task->canAbort()) { auto status = m_current_task->abort(); emitAborted(); return status; } return Task::abort(); } PrismLauncher-11.0.3/launcher/minecraft/launch/EnsureAvailableMemory.h0000644000175100017510000000223515224505336025414 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2026 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "launch/LaunchStep.h" #include "minecraft/MinecraftInstance.h" class EnsureAvailableMemory : public LaunchStep { Q_OBJECT public: explicit EnsureAvailableMemory(LaunchTask* parent, MinecraftInstance* instance); ~EnsureAvailableMemory() override = default; void executeTask() override; bool canAbort() const override { return false; } private: MinecraftInstance* m_instance; }; PrismLauncher-11.0.3/launcher/minecraft/launch/EnsureOfflineLibraries.cpp0000644000175100017510000000366415224505336026124 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2026 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "EnsureOfflineLibraries.h" #include "minecraft/PackProfile.h" EnsureOfflineLibraries::EnsureOfflineLibraries(LaunchTask* parent, MinecraftInstance* instance) : LaunchStep(parent), m_instance(instance) {} void EnsureOfflineLibraries::executeTask() { const auto profile = m_instance->getPackProfile()->getProfile(); QStringList allJars; profile->getLibraryFiles(m_instance->runtimeContext(), allJars, allJars, m_instance->getLocalLibraryPath(), m_instance->binRoot(), false); QStringList missing; for (const auto& jar : allJars) { if (!QFileInfo::exists(jar)) { missing.append(jar); } } if (missing.isEmpty()) { emitSucceeded(); return; } emit logLine("Missing libraries:", MessageLevel::Error); for (const auto& jar : missing) { emit logLine(" " + jar, MessageLevel::Error); } emit logLine(tr("\nThis instance cannot be launched because some libraries are missing or have not been downloaded yet. Please " "try again in online mode with a working Internet connection"), MessageLevel::Fatal); emitFailed("Required libraries are missing"); } PrismLauncher-11.0.3/launcher/minecraft/launch/PrintInstanceInfo.cpp0000644000175100017510000000512415224505336025111 0ustar runnerrunner/* 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 #include #include #include "PrintInstanceInfo.h" #include "HardwareInfo.h" #if defined(Q_OS_FREEBSD) namespace { void runSysctlHwModel(QStringList& log) { char buff[512]; FILE* hwmodel = popen("sysctl hw.model", "r"); while (fgets(buff, 512, hwmodel) != NULL) { log << QString::fromUtf8(buff); break; } pclose(hwmodel); } void runPciconf(QStringList& log) { char buff[512]; std::string strcard; FILE* pciconf = popen("pciconf -lv -a vgapci0", "r"); while (fgets(buff, 512, pciconf) != NULL) { if (strncmp(buff, " vendor", 10) == 0) { std::string str(buff); strcard.append(str.substr(str.find_first_of("'") + 1, str.find_last_not_of("'") - (str.find_first_of("'") + 2))); strcard.append(" "); } else if (strncmp(buff, " device", 10) == 0) { std::string str2(buff); strcard.append(str2.substr(str2.find_first_of("'") + 1, str2.find_last_not_of("'") - (str2.find_first_of("'") + 2))); } log << QString::fromStdString(strcard); break; } pclose(pciconf); } } // namespace #endif void PrintInstanceInfo::executeTask() { auto instance = m_parent->instance(); QStringList log; log << ""; log << "OS: " + QString("%1 | %2 | %3").arg(QSysInfo::prettyProductName(), QSysInfo::kernelType(), QSysInfo::kernelVersion()); #ifdef Q_OS_FREEBSD ::runSysctlHwModel(log); ::runPciconf(log); #else log << "CPU: " + HardwareInfo::cpuInfo(); #ifdef Q_OS_MACOS log << "Memory pressure level: " + MacOSHardwareInfo::memoryPressureLevelName(); #else log << QString("RAM: %1 MiB (available: %2 MiB)").arg(HardwareInfo::totalRamMiB()).arg(HardwareInfo::availableRamMiB()); #endif #endif log.append(HardwareInfo::gpuInfo()); log << ""; logLines(log, MessageLevel::Launcher); logLines(instance->verboseDescription(m_session, m_targetToJoin), MessageLevel::Launcher); emitSucceeded(); } PrismLauncher-11.0.3/launcher/minecraft/launch/VerifyJavaInstall.h0000644000175100017510000000347515224505336024565 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include class VerifyJavaInstall : public LaunchStep { Q_OBJECT public: explicit VerifyJavaInstall(LaunchTask* parent) : LaunchStep(parent) {}; ~VerifyJavaInstall() override = default; void executeTask() override; bool canAbort() const override { return false; } }; PrismLauncher-11.0.3/launcher/minecraft/launch/PrintInstanceInfo.h0000644000175100017510000000236115224505336024556 0ustar runnerrunner/* 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 #include "minecraft/auth/AuthSession.h" #include "minecraft/launch/MinecraftTarget.h" // FIXME: temporary wrapper for existing task. class PrintInstanceInfo : public LaunchStep { Q_OBJECT public: explicit PrintInstanceInfo(LaunchTask* parent, AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) : LaunchStep(parent), m_session(session), m_targetToJoin(targetToJoin) {}; virtual ~PrintInstanceInfo() = default; virtual void executeTask(); virtual bool canAbort() const { return false; } private: AuthSessionPtr m_session; MinecraftTarget::Ptr m_targetToJoin; }; PrismLauncher-11.0.3/launcher/minecraft/launch/ExtractNatives.cpp0000644000175100017510000000541215224505336024460 0ustar runnerrunner/* 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 "ExtractNatives.h" #include #include #include #include "FileSystem.h" #include "archive/ArchiveReader.h" #include "archive/ArchiveWriter.h" #ifdef major #undef major #endif #ifdef minor #undef minor #endif static QString replaceSuffix(QString target, const QString& suffix, const QString& replacement) { if (!target.endsWith(suffix)) { return target; } target.resize(target.length() - suffix.length()); return target + replacement; } static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack) { MMCZip::ArchiveReader zip(source); QDir directory(targetFolder); auto extPtr = MMCZip::ArchiveWriter::createDiskWriter(); auto ext = extPtr.get(); return zip.parse([applyJnilibHack, directory, ext](MMCZip::ArchiveReader::File* f) { QString name = f->filename(); auto lowercase = name.toLower(); if (applyJnilibHack) { name = replaceSuffix(name, ".jnilib", ".dylib"); } QString absFilePath = directory.absoluteFilePath(name); return f->writeFile(ext, absFilePath, directory); }); } void ExtractNatives::executeTask() { auto instance = m_parent->instance(); auto toExtract = instance->getNativeJars(); if (toExtract.isEmpty()) { emitSucceeded(); return; } auto outputPath = instance->getNativePath(); FS::ensureFolderPathExists(outputPath); auto javaVersion = instance->getJavaVersion(); bool jniHackEnabled = javaVersion.major() >= 8; for (const auto& source : toExtract) { if (!unzipNatives(source, outputPath, jniHackEnabled)) { const char* reason = QT_TR_NOOP("Couldn't extract native jar '%1' to destination '%2'"); emit logLine(QString(reason).arg(source, outputPath), MessageLevel::Fatal); emitFailed(tr(reason).arg(source, outputPath)); return; } } emitSucceeded(); } void ExtractNatives::finalize() { auto instance = m_parent->instance(); QString target_dir = FS::PathCombine(instance->instanceRoot(), "natives/"); QDir dir(target_dir); dir.removeRecursively(); } PrismLauncher-11.0.3/launcher/minecraft/launch/ModMinecraftJar.cpp0000644000175100017510000000635515224505336024530 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ModMinecraftJar.h" #include "FileSystem.h" #include "MMCZip.h" #include "launch/LaunchTask.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" void ModMinecraftJar::executeTask() { auto m_inst = m_parent->instance(); if (!m_inst->getJarMods().size()) { emitSucceeded(); return; } // nuke obsolete stripped jar(s) if needed if (!FS::ensureFolderPathExists(m_inst->binRoot())) { emitFailed(tr("Couldn't create the bin folder for Minecraft.jar")); return; } auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar"); if (!removeJar()) { emitFailed(tr("Couldn't remove stale jar file: %1").arg(finalJarPath)); return; } // create temporary modded jar, if needed auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto jarMods = m_inst->getJarMods(); if (jarMods.size()) { auto mainJar = profile->getMainJar(); QStringList jars, temp1, temp2, temp3, temp4; mainJar->getApplicableFiles(m_inst->runtimeContext(), jars, temp1, temp2, temp3, m_inst->getLocalLibraryPath()); auto sourceJarPath = jars[0]; if (!MMCZip::createModdedJar(sourceJarPath, finalJarPath, jarMods)) { emitFailed(tr("Failed to create the custom Minecraft jar file.")); return; } } emitSucceeded(); } void ModMinecraftJar::finalize() { removeJar(); } bool ModMinecraftJar::removeJar() { auto m_inst = m_parent->instance(); auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar"); QFile finalJar(finalJarPath); if (finalJar.exists()) { if (!finalJar.remove()) { return false; } } return true; } PrismLauncher-11.0.3/launcher/minecraft/LaunchProfile.h0000644000175100017510000001355115224505336022445 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include "Agent.h" #include "Library.h" class LaunchProfile : public ProblemProvider { public: virtual ~LaunchProfile() {} public: /* application of profile variables from patches */ void applyMinecraftVersion(const QString& id); void applyMainClass(const QString& mainClass); void applyAppletClass(const QString& appletClass); void applyMinecraftArguments(const QString& minecraftArguments); void applyAddnJvmArguments(const QStringList& minecraftArguments); void applyMinecraftVersionType(const QString& type); void applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets); void applyTraits(const QSet& traits); void applyTweakers(const QStringList& tweakers); void applyJarMods(const QList& jarMods); void applyMods(const QList& jarMods); void applyLibrary(LibraryPtr library, const RuntimeContext& runtimeContext); void applyMavenFile(LibraryPtr library, const RuntimeContext& runtimeContext); void applyAgent(const Agent& agent, const RuntimeContext& runtimeContext); void applyCompatibleJavaMajors(QList& javaMajor); void applyCompatibleJavaName(QString javaName); void applyMainJar(LibraryPtr jar); void applyProblemSeverity(ProblemSeverity severity); /// clear the profile void clear(); public: /* getters for profile variables */ QString getMinecraftVersion() const; QString getMainClass() const; QString getAppletClass() const; QString getMinecraftVersionType() const; MojangAssetIndexInfo::Ptr getMinecraftAssets() const; QString getMinecraftArguments() const; const QStringList& getAddnJvmArguments() const; const QSet& getTraits() const; const QStringList& getTweakers() const; const QList& getJarMods() const; const QList& getLibraries() const; const QList& getNativeLibraries() const; const QList& getMavenFiles() const; const QList& getAgents() const; const QList& getCompatibleJavaMajors() const; const QString getCompatibleJavaName() const; const LibraryPtr getMainJar() const; void getLibraryFiles(const RuntimeContext& runtimeContext, QStringList& jars, QStringList& nativeJars, const QString& overridePath, const QString& tempPath, bool addJarMods = true) const; bool hasTrait(const QString& trait) const; ProblemSeverity getProblemSeverity() const override; const QList getProblems() const override; private: /// the version of Minecraft - jar to use QString m_minecraftVersion; /// Release type - "release" or "snapshot" QString m_minecraftVersionType; /// Assets type - "legacy" or a version ID MojangAssetIndexInfo::Ptr m_minecraftAssets; /** * arguments that should be used for launching minecraft * * ex: "--username ${auth_player_name} --session ${auth_session} * --version ${version_name} --gameDir ${game_directory} --assetsDir ${game_assets}" */ QString m_minecraftArguments; /** * Additional arguments to pass to the JVM in addition to those the user has configured, * memory settings, etc. */ QStringList m_addnJvmArguments; /// A list of all tweaker classes QStringList m_tweakers; /// The main class to load first QString m_mainClass; /// The applet class, for some very old minecraft releases QString m_appletClass; /// the list of libraries QList m_libraries; /// the list of maven files to be placed in the libraries folder, but not acted upon QList m_mavenFiles; /// the list of java agents to add to JVM arguments QList m_agents; /// the main jar LibraryPtr m_mainJar; /// the list of native libraries QList m_nativeLibraries; /// traits, collected from all the version files (version files can only add) QSet m_traits; /// A list of jar mods. version files can add those. QList m_jarMods; /// the list of mods QList m_mods; /// compatible java major versions QList m_compatibleJavaMajors; QString m_compatibleJavaName; ProblemSeverity m_problemSeverity = ProblemSeverity::None; }; PrismLauncher-11.0.3/launcher/minecraft/ComponentUpdateTask_p.h0000644000175100017510000000120415224505336024151 0ustar runnerrunner#pragma once #include #include #include #include "net/Mode.h" #include "tasks/Task.h" #include "minecraft/ComponentUpdateTask.h" class PackProfile; struct RemoteLoadStatus { enum class Type { Index, List, Version } type = Type::Version; size_t PackProfileIndex = 0; bool finished = false; bool succeeded = false; Task::Ptr task; }; struct ComponentUpdateTaskData { PackProfile* m_profile = nullptr; QList remoteLoadStatusList; bool remoteLoadSuccessful = true; size_t remoteTasksInProgress = 0; ComponentUpdateTask::Mode mode; Net::Mode netmode; }; PrismLauncher-11.0.3/launcher/minecraft/LaunchProfile.cpp0000644000175100017510000002345415224505336023003 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "LaunchProfile.h" #include void LaunchProfile::clear() { m_minecraftVersion.clear(); m_minecraftVersionType.clear(); m_minecraftAssets.reset(); m_minecraftArguments.clear(); m_addnJvmArguments.clear(); m_tweakers.clear(); m_mainClass.clear(); m_appletClass.clear(); m_libraries.clear(); m_mavenFiles.clear(); m_agents.clear(); m_traits.clear(); m_jarMods.clear(); m_mainJar.reset(); m_problemSeverity = ProblemSeverity::None; } static void applyString(const QString& from, QString& to) { if (from.isEmpty()) return; to = from; } void LaunchProfile::applyMinecraftVersion(const QString& id) { applyString(id, this->m_minecraftVersion); } void LaunchProfile::applyAppletClass(const QString& appletClass) { applyString(appletClass, this->m_appletClass); } void LaunchProfile::applyMainClass(const QString& mainClass) { applyString(mainClass, this->m_mainClass); } void LaunchProfile::applyMinecraftArguments(const QString& minecraftArguments) { applyString(minecraftArguments, this->m_minecraftArguments); } void LaunchProfile::applyAddnJvmArguments(const QStringList& addnJvmArguments) { this->m_addnJvmArguments.append(addnJvmArguments); } void LaunchProfile::applyMinecraftVersionType(const QString& type) { applyString(type, this->m_minecraftVersionType); } void LaunchProfile::applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets) { if (assets) { m_minecraftAssets = assets; } } void LaunchProfile::applyTraits(const QSet& traits) { this->m_traits.unite(traits); } void LaunchProfile::applyTweakers(const QStringList& tweakers) { // if the applied tweakers override an existing one, skip it. this effectively moves it later in the sequence QStringList newTweakers; for (auto& tweaker : m_tweakers) { if (tweakers.contains(tweaker)) { continue; } newTweakers.append(tweaker); } // then just append the new tweakers (or moved original ones) newTweakers += tweakers; m_tweakers = newTweakers; } void LaunchProfile::applyJarMods(const QList& jarMods) { this->m_jarMods.append(jarMods); } static int findLibraryByName(QList* haystack, const GradleSpecifier& needle) { int retval = -1; for (int i = 0; i < haystack->size(); ++i) { if (haystack->at(i)->rawName().matchName(needle)) { // only one is allowed. if (retval != -1) return -1; retval = i; } } return retval; } void LaunchProfile::applyMods(const QList& mods) { QList* list = &m_mods; for (auto& mod : mods) { auto modCopy = Library::limitedCopy(mod); // find the mod by name. const int index = findLibraryByName(list, mod->rawName()); // mod not found? just add it. if (index < 0) { list->append(modCopy); return; } auto existingLibrary = list->at(index); // if we are higher it means we should update if (Version(mod->version()) > Version(existingLibrary->version())) { list->replace(index, modCopy); } } } void LaunchProfile::applyCompatibleJavaMajors(QList& javaMajor) { m_compatibleJavaMajors.append(javaMajor); } void LaunchProfile::applyCompatibleJavaName(QString javaName) { if (!javaName.isEmpty()) m_compatibleJavaName = javaName; } void LaunchProfile::applyLibrary(LibraryPtr library, const RuntimeContext& runtimeContext) { if (!library->isActive(runtimeContext)) { return; } QList* list = &m_libraries; if (library->isNative()) { list = &m_nativeLibraries; } auto libraryCopy = Library::limitedCopy(library); // find the library by name. const int index = findLibraryByName(list, library->rawName()); // library not found? just add it. if (index < 0) { list->append(libraryCopy); return; } auto existingLibrary = list->at(index); // if we are higher it means we should update if (Version(library->version()) > Version(existingLibrary->version())) { list->replace(index, libraryCopy); } } void LaunchProfile::applyMavenFile(LibraryPtr mavenFile, const RuntimeContext& runtimeContext) { if (!mavenFile->isActive(runtimeContext)) { return; } if (mavenFile->isNative()) { return; } // unlike libraries, we do not keep only one version or try to dedupe them m_mavenFiles.append(Library::limitedCopy(mavenFile)); } void LaunchProfile::applyAgent(const Agent& agent, const RuntimeContext& runtimeContext) { auto lib = agent.library; if (!lib->isActive(runtimeContext)) { return; } if (lib->isNative()) { return; } m_agents.append(agent); } const LibraryPtr LaunchProfile::getMainJar() const { return m_mainJar; } void LaunchProfile::applyMainJar(LibraryPtr jar) { if (jar) { m_mainJar = jar; } } void LaunchProfile::applyProblemSeverity(ProblemSeverity severity) { if (m_problemSeverity < severity) { m_problemSeverity = severity; } } const QList LaunchProfile::getProblems() const { // FIXME: implement something that actually makes sense here return {}; } QString LaunchProfile::getMinecraftVersion() const { return m_minecraftVersion; } QString LaunchProfile::getAppletClass() const { return m_appletClass; } QString LaunchProfile::getMainClass() const { return m_mainClass; } const QSet& LaunchProfile::getTraits() const { return m_traits; } const QStringList& LaunchProfile::getTweakers() const { return m_tweakers; } bool LaunchProfile::hasTrait(const QString& trait) const { return m_traits.contains(trait); } ProblemSeverity LaunchProfile::getProblemSeverity() const { return m_problemSeverity; } QString LaunchProfile::getMinecraftVersionType() const { return m_minecraftVersionType; } std::shared_ptr LaunchProfile::getMinecraftAssets() const { if (!m_minecraftAssets) { return std::make_shared("legacy"); } return m_minecraftAssets; } QString LaunchProfile::getMinecraftArguments() const { return m_minecraftArguments; } const QStringList& LaunchProfile::getAddnJvmArguments() const { return m_addnJvmArguments; } const QList& LaunchProfile::getJarMods() const { return m_jarMods; } const QList& LaunchProfile::getLibraries() const { return m_libraries; } const QList& LaunchProfile::getNativeLibraries() const { return m_nativeLibraries; } const QList& LaunchProfile::getMavenFiles() const { return m_mavenFiles; } const QList& LaunchProfile::getAgents() const { return m_agents; } const QList& LaunchProfile::getCompatibleJavaMajors() const { return m_compatibleJavaMajors; } const QString LaunchProfile::getCompatibleJavaName() const { return m_compatibleJavaName; } void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext, QStringList& jars, QStringList& nativeJars, const QString& overridePath, const QString& tempPath, bool addJarMods) const { QStringList native32, native64; jars.clear(); nativeJars.clear(); for (auto lib : getLibraries()) { lib->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath); } // NOTE: order is important here, add main jar last to the lists if (m_mainJar) { // FIXME: HACK!! jar modding is weird and unsystematic! if (m_jarMods.size() && addJarMods) { QDir tempDir(tempPath); jars.append(tempDir.absoluteFilePath("minecraft.jar")); } else { m_mainJar->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath); } } for (auto lib : getNativeLibraries()) { lib->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath); } if (runtimeContext.javaArchitecture == "32") { nativeJars.append(native32); } else if (runtimeContext.javaArchitecture == "64") { nativeJars.append(native64); } } PrismLauncher-11.0.3/launcher/minecraft/ShortcutUtils.cpp0000644000175100017510000002310615224505336023076 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2025 Yihe Li * * parent program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * parent program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with parent program. If not, see . * * parent file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use parent 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 "ShortcutUtils.h" #include "FileSystem.h" #include #include #include #include #include namespace ShortcutUtils { bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) { if (!shortcut.instance) return false; QString appPath = QApplication::applicationFilePath(); auto icon = APPLICATION->icons()->icon(shortcut.iconKey.isEmpty() ? shortcut.instance->iconKey() : shortcut.iconKey); if (icon == nullptr) { icon = APPLICATION->icons()->icon("grass"); } QString iconPath; QStringList args; #if defined(Q_OS_MACOS) if (appPath.startsWith("/private/var/")) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); return false; } iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "Icon.icns"); QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application: %1").arg(iconFile.errorString())); return false; } QIcon iconObj = icon->icon(); bool success = iconObj.pixmap(1024, 1024).save(iconPath, "ICNS"); iconFile.close(); if (!success) { iconFile.remove(); QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); return false; } #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) if (appPath.startsWith("/tmp/.mount_")) { // AppImage! appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); if (appPath.isEmpty()) { QMessageBox::critical( shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); } else if (appPath.endsWith("/")) { appPath.chop(1); } } iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.png"); QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut: %1").arg(iconFile.errorString())); return false; } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); iconFile.close(); if (!success) { iconFile.remove(); QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); return false; } if (DesktopServices::isFlatpak()) { appPath = "flatpak"; args.append({ "run", BuildConfig.LAUNCHER_APPID }); } #elif defined(Q_OS_WIN) iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.ico"); // part of fix for weird bug involving the window icon being replaced // dunno why it happens, but parent 2-line fix seems to be enough, so w/e auto appIcon = APPLICATION->logo(); QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut: %1").arg(iconFile.errorString())); return false; } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); iconFile.close(); // restore original window icon QGuiApplication::setWindowIcon(appIcon); if (!success) { iconFile.remove(); QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); return false; } #else QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Not supported on your platform!")); return false; #endif args.append({ "--launch", shortcut.instance->id() }); args.append(shortcut.extraArgs); QString shortcutPath = FS::createShortcut(filePath, appPath, args, shortcut.name, iconPath); if (shortcutPath.isEmpty()) { #if not defined(Q_OS_MACOS) iconFile.remove(); #endif QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create %1 shortcut!").arg(shortcut.targetString)); return false; } shortcut.instance->registerShortcut({ shortcut.name, shortcutPath, shortcut.target }); return true; } bool createInstanceShortcutOnDesktop(const Shortcut& shortcut) { if (!shortcut.instance) return false; QString desktopDir = FS::getDesktopDir(); if (desktopDir.isEmpty()) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find desktop?!")); return false; } QString shortcutFilePath = FS::PathCombine(desktopDir, FS::RemoveInvalidFilenameChars(shortcut.name)); if (!createInstanceShortcut(shortcut, shortcutFilePath)) return false; QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Created a shortcut to this %1 on your desktop!").arg(shortcut.targetString)); return true; } bool createInstanceShortcutInApplications(const Shortcut& shortcut) { if (!shortcut.instance) return false; QString applicationsDir = FS::getApplicationsDir(); if (applicationsDir.isEmpty()) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find applications folder?!")); return false; } #if defined(Q_OS_MACOS) || defined(Q_OS_WIN) applicationsDir = FS::PathCombine(applicationsDir, BuildConfig.LAUNCHER_DISPLAYNAME + " Instances"); QDir applicationsDirQ(applicationsDir); if (!applicationsDirQ.mkpath(".")) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create instances folder in applications folder!")); return false; } #endif QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(shortcut.name)); if (!createInstanceShortcut(shortcut, shortcutFilePath)) return false; QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Created a shortcut to this %1 in your applications folder!").arg(shortcut.targetString)); return true; } bool createInstanceShortcutInOther(const Shortcut& shortcut) { if (!shortcut.instance) return false; QString defaultedDir = FS::getDesktopDir(); #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) QString extension = ".desktop"; #elif defined(Q_OS_WINDOWS) QString extension = ".lnk"; #else QString extension = ""; #endif QString shortcutFilePath = FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(shortcut.name) + extension); QFileDialog fileDialog; // workaround to make sure the portal file dialog opens in the desktop directory fileDialog.setDirectoryUrl(defaultedDir); shortcutFilePath = fileDialog.getSaveFileName(shortcut.parent, QObject::tr("Create Shortcut"), shortcutFilePath, QObject::tr("Desktop Entries") + " (*" + extension + ")"); if (shortcutFilePath.isEmpty()) return false; // file dialog canceled by user if (shortcutFilePath.endsWith(extension)) shortcutFilePath = shortcutFilePath.mid(0, shortcutFilePath.length() - extension.length()); if (!createInstanceShortcut(shortcut, shortcutFilePath)) return false; QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Created a shortcut to this %1!").arg(shortcut.targetString)); return true; } } // namespace ShortcutUtils PrismLauncher-11.0.3/launcher/minecraft/Agent.h0000644000175100017510000000041215224505336020740 0ustar runnerrunner#pragma once #include #include "Library.h" struct Agent { /// The library pointing to the jar this Java agent is contained within LibraryPtr library; /// The argument to the Java agent, passed after an = if present QString argument; }; PrismLauncher-11.0.3/launcher/minecraft/MojangVersionFormat.cpp0000644000175100017510000003267115224505336024203 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "MojangVersionFormat.h" #include "MojangDownloadInfo.h" #include "OneSixVersionFormat.h" #include "Json.h" using namespace Json; #include #include "ParseUtils.h" static const int CURRENT_MINIMUM_LAUNCHER_VERSION = 18; static MojangAssetIndexInfo::Ptr assetIndexFromJson(const QJsonObject& obj); static MojangDownloadInfo::Ptr downloadInfoFromJson(const QJsonObject& obj); static MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson(const QJsonObject& libObj); static QJsonObject assetIndexToJson(MojangAssetIndexInfo::Ptr assetidxinfo); static QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo); static QJsonObject downloadInfoToJson(MojangDownloadInfo::Ptr info); namespace Bits { static void readString(const QJsonObject& root, const QString& key, QString& variable) { if (root.contains(key)) { variable = requireString(root.value(key)); } } static void readDownloadInfo(MojangDownloadInfo::Ptr out, const QJsonObject& obj) { // optional, not used readString(obj, "path", out->path); // required! out->sha1 = requireString(obj, "sha1"); out->url = requireString(obj, "url"); out->size = requireInteger(obj, "size"); } static void readAssetIndex(MojangAssetIndexInfo::Ptr out, const QJsonObject& obj) { out->totalSize = requireInteger(obj, "totalSize"); out->id = requireString(obj, "id"); // out->known = true; } } // namespace Bits MojangDownloadInfo::Ptr downloadInfoFromJson(const QJsonObject& obj) { auto out = std::make_shared(); Bits::readDownloadInfo(out, obj); return out; } MojangAssetIndexInfo::Ptr assetIndexFromJson(const QJsonObject& obj) { auto out = std::make_shared(); Bits::readDownloadInfo(out, obj); Bits::readAssetIndex(out, obj); return out; } QJsonObject downloadInfoToJson(MojangDownloadInfo::Ptr info) { QJsonObject out; if (!info->path.isNull()) { out.insert("path", info->path); } out.insert("sha1", info->sha1); out.insert("size", info->size); out.insert("url", info->url); return out; } MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson(const QJsonObject& libObj) { auto out = std::make_shared(); auto dlObj = requireObject(libObj.value("downloads")); if (dlObj.contains("artifact")) { out->artifact = downloadInfoFromJson(requireObject(dlObj, "artifact")); } if (dlObj.contains("classifiers")) { auto classifiersObj = requireObject(dlObj, "classifiers"); for (auto iter = classifiersObj.begin(); iter != classifiersObj.end(); iter++) { auto classifier = iter.key(); auto classifierObj = requireObject(iter.value()); out->classifiers[classifier] = downloadInfoFromJson(classifierObj); } } return out; } QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo) { QJsonObject out; if (libinfo->artifact) { out.insert("artifact", downloadInfoToJson(libinfo->artifact)); } if (!libinfo->classifiers.isEmpty()) { QJsonObject classifiersOut; for (auto iter = libinfo->classifiers.begin(); iter != libinfo->classifiers.end(); iter++) { classifiersOut.insert(iter.key(), downloadInfoToJson(iter.value())); } out.insert("classifiers", classifiersOut); } return out; } QJsonObject assetIndexToJson(MojangAssetIndexInfo::Ptr info) { QJsonObject out; if (!info->path.isNull()) { out.insert("path", info->path); } out.insert("sha1", info->sha1); out.insert("size", info->size); out.insert("url", info->url); out.insert("totalSize", info->totalSize); out.insert("id", info->id); return out; } void MojangVersionFormat::readVersionProperties(const QJsonObject& in, VersionFile* out) { Bits::readString(in, "id", out->minecraftVersion); Bits::readString(in, "mainClass", out->mainClass); Bits::readString(in, "minecraftArguments", out->minecraftArguments); Bits::readString(in, "type", out->type); Bits::readString(in, "assets", out->assets); if (in.contains("assetIndex")) { out->mojangAssetIndex = assetIndexFromJson(requireObject(in, "assetIndex")); } else if (!out->assets.isNull()) { out->mojangAssetIndex = std::make_shared(out->assets); } out->releaseTime = timeFromS3Time(in.value("releaseTime").toString("")); out->updateTime = timeFromS3Time(in.value("time").toString("")); if (in.contains("minimumLauncherVersion")) { out->minimumLauncherVersion = requireInteger(in.value("minimumLauncherVersion")); if (out->minimumLauncherVersion > CURRENT_MINIMUM_LAUNCHER_VERSION) { out->addProblem(ProblemSeverity::Warning, QObject::tr("The 'minimumLauncherVersion' value of this version (%1) is higher than " "supported by %3 (%2). It might not work properly!") .arg(out->minimumLauncherVersion) .arg(CURRENT_MINIMUM_LAUNCHER_VERSION) .arg(BuildConfig.LAUNCHER_DISPLAYNAME)); } } if (in.contains("compatibleJavaMajors")) { for (auto compatible : requireArray(in.value("compatibleJavaMajors"))) { out->compatibleJavaMajors.append(requireInteger(compatible)); } } if (in.contains("compatibleJavaName")) { out->compatibleJavaName = requireString(in.value("compatibleJavaName")); } if (in.contains("downloads")) { auto downloadsObj = requireObject(in, "downloads"); for (auto iter = downloadsObj.begin(); iter != downloadsObj.end(); iter++) { auto classifier = iter.key(); auto classifierObj = requireObject(iter.value()); out->mojangDownloads[classifier] = downloadInfoFromJson(classifierObj); } } } VersionFilePtr MojangVersionFormat::versionFileFromJson(const QJsonDocument& doc, const QString& filename) { VersionFilePtr out(new VersionFile()); if (doc.isEmpty() || doc.isNull()) { throw JSONValidationError(filename + " is empty or null"); } if (!doc.isObject()) { throw JSONValidationError(filename + " is not an object"); } QJsonObject root = doc.object(); readVersionProperties(root, out.get()); out->name = "Minecraft"; out->uid = "net.minecraft"; out->version = out->minecraftVersion; // out->filename = filename; if (root.contains("libraries")) { for (auto libVal : requireArray(root.value("libraries"))) { auto libObj = requireObject(libVal); auto lib = MojangVersionFormat::libraryFromJson(*out, libObj, filename); out->libraries.append(lib); } } return out; } void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObject& out) { writeString(out, "id", in->minecraftVersion); writeString(out, "mainClass", in->mainClass); writeString(out, "minecraftArguments", in->minecraftArguments); writeString(out, "type", in->type); if (!in->releaseTime.isNull()) { writeString(out, "releaseTime", timeToS3Time(in->releaseTime)); } if (!in->updateTime.isNull()) { writeString(out, "time", timeToS3Time(in->updateTime)); } if (in->minimumLauncherVersion != -1) { out.insert("minimumLauncherVersion", in->minimumLauncherVersion); } writeString(out, "assets", in->assets); if (in->mojangAssetIndex && in->mojangAssetIndex->known) { out.insert("assetIndex", assetIndexToJson(in->mojangAssetIndex)); } if (!in->mojangDownloads.isEmpty()) { QJsonObject downloadsOut; for (auto iter = in->mojangDownloads.begin(); iter != in->mojangDownloads.end(); iter++) { downloadsOut.insert(iter.key(), downloadInfoToJson(iter.value())); } out.insert("downloads", downloadsOut); } if (!in->compatibleJavaMajors.isEmpty()) { QJsonArray compatibleJavaMajorsOut; for (auto compatibleJavaMajor : in->compatibleJavaMajors) { compatibleJavaMajorsOut.append(compatibleJavaMajor); } out.insert("compatibleJavaMajors", compatibleJavaMajorsOut); } if (!in->compatibleJavaName.isEmpty()) { writeString(out, "compatibleJavaName", in->compatibleJavaName); } } QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr& patch) { QJsonObject root; writeVersionProperties(patch.get(), root); if (!patch->libraries.isEmpty()) { QJsonArray array; for (auto value : patch->libraries) { array.append(MojangVersionFormat::libraryToJson(value.get())); } root.insert("libraries", array); } // write the contents to a json document. { QJsonDocument out; out.setObject(root); return out; } } LibraryPtr MojangVersionFormat::libraryFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename) { LibraryPtr out(new Library()); if (!libObj.contains("name")) { throw JSONValidationError(filename + "contains a library that doesn't have a 'name' field"); } auto rawName = libObj.value("name").toString(); out->m_name = rawName; if (!out->m_name.valid()) { problems.addProblem(ProblemSeverity::Error, QObject::tr("Library %1 name is broken and cannot be processed.").arg(rawName)); } Bits::readString(libObj, "url", out->m_repositoryURL); if (libObj.contains("extract")) { out->m_hasExcludes = true; auto extractObj = requireObject(libObj.value("extract")); for (auto excludeVal : requireArray(extractObj.value("exclude"))) { out->m_extractExcludes.append(requireString(excludeVal)); } } if (libObj.contains("natives")) { QJsonObject nativesObj = requireObject(libObj.value("natives")); for (auto it = nativesObj.begin(); it != nativesObj.end(); ++it) { if (!it.value().isString()) { qWarning() << filename << "contains an invalid native (skipping)"; } // FIXME: Skip unknown platforms out->m_nativeClassifiers[it.key()] = it.value().toString(); } } if (libObj.contains("rules")) { out->applyRules = true; QJsonArray rulesArray = requireArray(libObj.value("rules")); for (auto rule : rulesArray) { out->m_rules.append(Rule::fromJson(requireObject(rule))); } } if (libObj.contains("downloads")) { out->m_mojangDownloads = libDownloadInfoFromJson(libObj); } return out; } QJsonObject MojangVersionFormat::libraryToJson(Library* library) { QJsonObject libRoot; libRoot.insert("name", library->m_name.serialize()); if (!library->m_repositoryURL.isEmpty()) { libRoot.insert("url", library->m_repositoryURL); } if (library->isNative()) { QJsonObject nativeList; auto iter = library->m_nativeClassifiers.begin(); while (iter != library->m_nativeClassifiers.end()) { nativeList.insert(iter.key(), iter.value()); iter++; } libRoot.insert("natives", nativeList); if (!library->m_extractExcludes.isEmpty()) { QJsonArray excludes; QJsonObject extract; for (auto exclude : library->m_extractExcludes) { excludes.append(exclude); } extract.insert("exclude", excludes); libRoot.insert("extract", extract); } } if (!library->m_rules.isEmpty()) { QJsonArray allRules; for (auto& rule : library->m_rules) { QJsonObject ruleObj = rule.toJson(); allRules.append(ruleObj); } libRoot.insert("rules", allRules); } if (library->m_mojangDownloads) { auto downloadsObj = libDownloadInfoToJson(library->m_mojangDownloads); libRoot.insert("downloads", downloadsObj); } return libRoot; } PrismLauncher-11.0.3/launcher/minecraft/PackProfile_p.h0000644000175100017510000000136315224505336022426 0ustar runnerrunner#pragma once #include #include #include #include "Component.h" #include "tasks/Task.h" class MinecraftInstance; using ComponentContainer = QList; using ComponentIndex = QMap; struct PackProfileData { // the instance this belongs to MinecraftInstance* m_instance; // the launch profile (volatile, temporary thing created on demand) std::shared_ptr m_profile; // persistent list of components and related machinery ComponentContainer components; ComponentIndex componentIndex; bool dirty = false; QTimer m_saveTimer; shared_qobject_ptr m_updateTask; bool loaded = false; bool interactionDisabled = true; }; PrismLauncher-11.0.3/launcher/minecraft/update/0000755000175100017510000000000015224505336021016 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/minecraft/update/LibrariesTask.h0000644000175100017510000000074115224505336023730 0ustar runnerrunner#pragma once #include "net/NetJob.h" #include "tasks/Task.h" class MinecraftInstance; class LibrariesTask : public Task { Q_OBJECT public: LibrariesTask(MinecraftInstance* inst); virtual ~LibrariesTask() = default; void executeTask() override; bool canAbort() const override; private slots: void jarlibFailed(QString reason); public slots: bool abort() override; private: MinecraftInstance* m_inst; NetJob::Ptr downloadJob; }; PrismLauncher-11.0.3/launcher/minecraft/update/LegacyFMLLibrariesTask.h0000644000175100017510000000122215224505336025407 0ustar runnerrunner#pragma once #include "minecraft/VersionFilterData.h" #include "net/NetJob.h" #include "tasks/Task.h" class MinecraftInstance; class LegacyFMLLibrariesTask : public Task { Q_OBJECT public: LegacyFMLLibrariesTask(MinecraftInstance* inst); virtual ~LegacyFMLLibrariesTask() = default; void executeTask() override; bool canAbort() const override; private slots: void fmllibsFinished(); void fmllibsFailed(QString reason); public slots: bool abort() override; private: static QString baseUrl(); private: MinecraftInstance* m_inst; NetJob::Ptr downloadJob; QList fmlLibsToProcess; }; PrismLauncher-11.0.3/launcher/minecraft/update/FoldersTask.cpp0000644000175100017510000000357215224505336023752 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "FoldersTask.h" #include #include "minecraft/MinecraftInstance.h" FoldersTask::FoldersTask(MinecraftInstance* inst) { m_inst = inst; } void FoldersTask::executeTask() { // Make directories QDir mcDir(m_inst->gameRoot()); if (!mcDir.exists() && !mcDir.mkpath(".")) { emitFailed(tr("Failed to create folder for Minecraft binaries.")); return; } emitSucceeded(); } PrismLauncher-11.0.3/launcher/minecraft/update/LibrariesTask.cpp0000644000175100017510000000636215224505336024270 0ustar runnerrunner#include "LibrariesTask.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "Application.h" LibrariesTask::LibrariesTask(MinecraftInstance* inst) { m_inst = inst; } void LibrariesTask::executeTask() { setStatus(tr("Downloading required library files...")); qDebug() << m_inst->name() << ": downloading libraries"; MinecraftInstance* inst = (MinecraftInstance*)m_inst; // Build a list of URLs that will need to be downloaded. auto components = inst->getPackProfile(); auto profile = components->getProfile(); NetJob::Ptr job{ new NetJob(tr("Libraries for instance %1").arg(inst->name()), APPLICATION->network()) }; downloadJob.reset(job); auto metacache = APPLICATION->metacache(); auto processArtifactPool = [this, inst, metacache](const QList& pool, QStringList& errors, const QString& localPath) { for (auto lib : pool) { if (!lib) { emitFailed(tr("Null jar is specified in the metadata, aborting.")); return false; } auto dls = lib->getDownloads(inst->runtimeContext(), metacache, errors, localPath); for (auto dl : dls) { downloadJob->addNetAction(dl); } } return true; }; QStringList failedLocalLibraries; QList libArtifactPool; libArtifactPool.append(profile->getLibraries()); libArtifactPool.append(profile->getNativeLibraries()); libArtifactPool.append(profile->getMavenFiles()); for (const auto& agent : profile->getAgents()) { libArtifactPool.append(agent.library); } libArtifactPool.append(profile->getMainJar()); processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath()); QStringList failedLocalJarMods; processArtifactPool(profile->getJarMods(), failedLocalJarMods, inst->jarModsDir()); if (!failedLocalJarMods.empty() || !failedLocalLibraries.empty()) { downloadJob.reset(); QString failed_all = (failedLocalLibraries + failedLocalJarMods).join("\n"); emitFailed(tr("Some artifacts marked as 'local' are missing their files:\n%1\n\nYou need to either add the files, or removed the " "packages that require them.\nYou'll have to correct this problem manually.") .arg(failed_all)); return; } connect(downloadJob.get(), &NetJob::succeeded, this, &LibrariesTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed); connect(downloadJob.get(), &NetJob::aborted, this, &LibrariesTask::emitAborted); connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress); connect(downloadJob.get(), &NetJob::stepProgress, this, &LibrariesTask::propagateStepProgress); downloadJob->start(); } bool LibrariesTask::canAbort() const { return true; } void LibrariesTask::jarlibFailed(QString reason) { emitFailed(tr("Game update failed: it was impossible to fetch the required libraries.\nReason:\n%1").arg(reason)); } bool LibrariesTask::abort() { if (downloadJob) { return downloadJob->abort(); } else { qWarning() << "Prematurely aborted LibrariesTask"; } return true; } PrismLauncher-11.0.3/launcher/minecraft/update/AssetUpdateTask.h0000644000175100017510000000113715224505336024236 0ustar runnerrunner#pragma once #include "net/NetJob.h" #include "tasks/Task.h" class MinecraftInstance; class AssetUpdateTask : public Task { Q_OBJECT public: AssetUpdateTask(MinecraftInstance* inst); virtual ~AssetUpdateTask() = default; void executeTask() override; bool canAbort() const override; public: static QString resourceUrl(); private slots: void assetIndexFinished(); void assetIndexFailed(QString reason); void assetsFailed(QString reason); public slots: bool abort() override; private: MinecraftInstance* m_inst; NetJob::Ptr downloadJob; }; PrismLauncher-11.0.3/launcher/minecraft/update/FoldersTask.h0000644000175100017510000000043315224505336023410 0ustar runnerrunner#pragma once #include "tasks/Task.h" class MinecraftInstance; class FoldersTask : public Task { Q_OBJECT public: FoldersTask(MinecraftInstance* inst); virtual ~FoldersTask() = default; void executeTask() override; private: MinecraftInstance* m_inst; }; PrismLauncher-11.0.3/launcher/minecraft/update/LegacyFMLLibrariesTask.cpp0000644000175100017510000001074315224505336025752 0ustar runnerrunner#include "LegacyFMLLibrariesTask.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "minecraft/VersionFilterData.h" #include "Application.h" #include "BuildConfig.h" #include "net/ApiDownload.h" LegacyFMLLibrariesTask::LegacyFMLLibrariesTask(MinecraftInstance* inst) { m_inst = inst; } void LegacyFMLLibrariesTask::executeTask() { // Get the mod list MinecraftInstance* inst = (MinecraftInstance*)m_inst; auto components = inst->getPackProfile(); auto profile = components->getProfile(); if (!profile->hasTrait("legacyFML")) { emitSucceeded(); return; } QString version = components->getComponentVersion("net.minecraft"); auto& fmlLibsMapping = g_VersionFilterData.fmlLibsMapping; if (!fmlLibsMapping.contains(version)) { emitSucceeded(); return; } auto& libList = fmlLibsMapping[version]; // determine if we need some libs for FML or forge setStatus(tr("Checking for FML libraries...")); if (!components->getComponent("net.minecraftforge")) { emitSucceeded(); return; } // now check the lib folder inside the instance for files. for (auto& lib : libList) { QFileInfo libInfo(FS::PathCombine(inst->libDir(), lib.filename)); if (libInfo.exists()) continue; fmlLibsToProcess.append(lib); } // if everything is in place, there's nothing to do here... if (fmlLibsToProcess.isEmpty()) { emitSucceeded(); return; } // download missing libs to our place setStatus(tr("Downloading FML libraries...")); NetJob::Ptr dljob{ new NetJob("FML libraries", APPLICATION->network()) }; auto metacache = APPLICATION->metacache(); Net::Download::Options options = Net::Download::Option::MakeEternal; const QString base = baseUrl(); for (auto& lib : fmlLibsToProcess) { auto entry = metacache->resolveEntry("fmllibs", lib.filename); QString urlString = base + lib.filename; dljob->addNetAction(Net::ApiDownload::makeCached(QUrl(urlString), entry, options)); } connect(dljob.get(), &NetJob::succeeded, this, &LegacyFMLLibrariesTask::fmllibsFinished); connect(dljob.get(), &NetJob::failed, this, &LegacyFMLLibrariesTask::fmllibsFailed); connect(dljob.get(), &NetJob::aborted, this, &LegacyFMLLibrariesTask::emitAborted); connect(dljob.get(), &NetJob::progress, this, &LegacyFMLLibrariesTask::progress); connect(dljob.get(), &NetJob::stepProgress, this, &LegacyFMLLibrariesTask::propagateStepProgress); downloadJob.reset(dljob); downloadJob->start(); } bool LegacyFMLLibrariesTask::canAbort() const { return true; } void LegacyFMLLibrariesTask::fmllibsFinished() { downloadJob.reset(); if (!fmlLibsToProcess.isEmpty()) { setStatus(tr("Copying FML libraries into the instance...")); MinecraftInstance* inst = (MinecraftInstance*)m_inst; auto metacache = APPLICATION->metacache(); int index = 0; for (auto& lib : fmlLibsToProcess) { progress(index, fmlLibsToProcess.size()); auto entry = metacache->resolveEntry("fmllibs", lib.filename); auto path = FS::PathCombine(inst->libDir(), lib.filename); if (!FS::ensureFilePathExists(path)) { emitFailed(tr("Failed creating FML library folder inside the instance.")); return; } if (!QFile::copy(entry->getFullPath(), FS::PathCombine(inst->libDir(), lib.filename))) { emitFailed(tr("Failed copying Forge/FML library: %1.").arg(lib.filename)); return; } index++; } progress(index, fmlLibsToProcess.size()); } emitSucceeded(); } void LegacyFMLLibrariesTask::fmllibsFailed(QString reason) { QStringList failed = downloadJob->getFailedFiles(); QString failed_all = failed.join("\n"); emitFailed(tr("Failed to download the following files:\n%1\n\nReason:%2\nPlease try again.").arg(failed_all, reason)); } bool LegacyFMLLibrariesTask::abort() { if (downloadJob) { return downloadJob->abort(); } else { qWarning() << "Prematurely aborted LegacyFMLLibrariesTask"; } return true; } QString LegacyFMLLibrariesTask::baseUrl() { if (const QString urlOverride = APPLICATION->settings()->get("LegacyFMLLibsURLOverride").toString(); !urlOverride.isEmpty()) { return urlOverride; } return BuildConfig.LEGACY_FMLLIBS_BASE_URL; } PrismLauncher-11.0.3/launcher/minecraft/update/AssetUpdateTask.cpp0000644000175100017510000001030315224505336024564 0ustar runnerrunner#include "AssetUpdateTask.h" #include "BuildConfig.h" #include "launch/LaunchStep.h" #include "minecraft/AssetsUtils.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "net/ChecksumValidator.h" #include "Application.h" #include "net/ApiDownload.h" AssetUpdateTask::AssetUpdateTask(MinecraftInstance* inst) { m_inst = inst; } void AssetUpdateTask::executeTask() { setStatus(tr("Updating assets index...")); auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); QUrl indexUrl = assets->url; QString localPath = assets->id + ".json"; auto job = makeShared(tr("Asset index for %1").arg(m_inst->name()), APPLICATION->network()); auto metacache = APPLICATION->metacache(); auto entry = metacache->resolveEntry("asset_indexes", localPath); entry->setStale(true); auto hexSha1 = assets->sha1.toLatin1(); qDebug() << "Asset index SHA1:" << hexSha1; auto dl = Net::ApiDownload::makeCached(indexUrl, entry); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, assets->sha1)); job->addNetAction(dl); downloadJob.reset(job); connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::assetIndexFinished); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetIndexFailed); connect(downloadJob.get(), &NetJob::aborted, this, &AssetUpdateTask::emitAborted); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress); qDebug() << "Starting asset index download for" << m_inst->name(); downloadJob->start(); } bool AssetUpdateTask::canAbort() const { return true; } void AssetUpdateTask::assetIndexFinished() { AssetsIndex index; qDebug() << "Finished asset index download for" << m_inst->name(); auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); QString asset_fname = "assets/indexes/" + assets->id + ".json"; // FIXME: this looks like a job for a generic validator based on json schema? if (!AssetsUtils::loadAssetsIndexJson(assets->id, asset_fname, index)) { auto metacache = APPLICATION->metacache(); auto entry = metacache->resolveEntry("asset_indexes", assets->id + ".json"); metacache->evictEntry(entry); emitFailed(tr("Failed to read the assets index!")); return; } auto job = index.getDownloadJob(); if (job) { QString resourceURL = resourceUrl(); QString source = tr("Mojang"); if (resourceURL != BuildConfig.DEFAULT_RESOURCE_BASE) { source = QUrl(resourceURL).host(); } setStatus(tr("Getting the asset files from %1...").arg(source)); downloadJob = job; connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); connect(downloadJob.get(), &NetJob::aborted, this, &AssetUpdateTask::emitAborted); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress); downloadJob->start(); return; } emitSucceeded(); } void AssetUpdateTask::assetIndexFailed(QString reason) { qDebug() << m_inst->name() << ": Failed asset index download"; emitFailed(tr("Failed to download the assets index:\n%1").arg(reason)); } void AssetUpdateTask::assetsFailed(QString reason) { emitFailed(tr("Failed to download assets:\n%1").arg(reason)); } bool AssetUpdateTask::abort() { if (downloadJob) { return downloadJob->abort(); } else { qWarning() << "Prematurely aborted AssetUpdateTask"; } return true; } QString AssetUpdateTask::resourceUrl() { if (const QString urlOverride = APPLICATION->settings()->get("ResourceURLOverride").toString(); !urlOverride.isEmpty()) { return urlOverride; } return BuildConfig.DEFAULT_RESOURCE_BASE; } PrismLauncher-11.0.3/launcher/minecraft/Library.cpp0000644000175100017510000003501715224505336021652 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Library.h" #include "MinecraftInstance.h" #include "net/NetRequest.h" #include #include #include #include /** * @brief Collect applicable files for the library. * * Depending on whether the library is native or not, it adds paths to the * appropriate lists for jar files, native libraries for 32-bit, and native * libraries for 64-bit. * * @param runtimeContext The current runtime context. * @param jar List to store paths for jar files. * @param native List to store paths for native libraries. * @param native32 List to store paths for 32-bit native libraries. * @param native64 List to store paths for 64-bit native libraries. * @param overridePath Optional path to override the default storage path. */ void Library::getApplicableFiles(const RuntimeContext& runtimeContext, QStringList& jar, QStringList& native, QStringList& native32, QStringList& native64, const QString& overridePath) const { bool local = isLocal(); // Lambda function to get the absolute file path auto actualPath = [this, local, overridePath](QString relPath) { relPath = FS::RemoveInvalidPathChars(relPath); QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); if (local && !overridePath.isEmpty()) { QString fileName = out.fileName(); return QFileInfo(FS::PathCombine(overridePath, fileName)).absoluteFilePath(); } return out.absoluteFilePath(); }; QString raw_storage = storageSuffix(runtimeContext); if (isNative()) { if (raw_storage.contains("${arch}")) { auto nat32Storage = raw_storage; nat32Storage.replace("${arch}", "32"); auto nat64Storage = raw_storage; nat64Storage.replace("${arch}", "64"); native32 += actualPath(nat32Storage); native64 += actualPath(nat64Storage); } else { native += actualPath(raw_storage); } } else { jar += actualPath(raw_storage); } } /** * @brief Get download requests for the library files. * * Depending on whether the library is native or not, and the current runtime context, * this function prepares download requests for the necessary files. It handles both local * and remote files, checks for stale cache entries, and adds checksummed downloads. * * @param runtimeContext The current runtime context. * @param cache Pointer to the HTTP meta cache. * @param failedLocalFiles List to store paths for failed local files. * @param overridePath Optional path to override the default storage path. * @return QList List of download requests. */ QList Library::getDownloads(const RuntimeContext& runtimeContext, class HttpMetaCache* cache, QStringList& failedLocalFiles, const QString& overridePath) const { QList out; bool stale = isAlwaysStale(); bool local = isLocal(); // Lambda function to check if a local file exists auto check_local_file = [overridePath, &failedLocalFiles](QString storage) { QFileInfo fileinfo(storage); QString fileName = fileinfo.fileName(); auto fullPath = FS::PathCombine(overridePath, fileName); QFileInfo localFileInfo(fullPath); if (!localFileInfo.exists()) { failedLocalFiles.append(localFileInfo.filePath()); return false; } return true; }; // Lambda function to add a download request auto add_download = [this, local, check_local_file, cache, stale, &out](QString storage, QString url, QString sha1) { if (local) { return check_local_file(storage); } auto entry = cache->resolveEntry("libraries", storage); if (stale) { entry->setStale(true); } if (!entry->isStale()) return true; Net::Download::Options options; if (stale) { options |= Net::Download::Option::AcceptLocalFiles; } // Don't add a time limit for the libraries cache entry validity options |= Net::Download::Option::MakeEternal; if (sha1.size()) { auto dl = Net::ApiDownload::makeCached(url, entry, options); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, sha1)); qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url << "expected sha1:" << sha1; out.append(dl); } else { out.append(Net::ApiDownload::makeCached(url, entry, options)); qDebug() << "Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; } return true; }; QString raw_storage = storageSuffix(runtimeContext); if (m_mojangDownloads) { if (isNative()) { auto nativeClassifier = getCompatibleNative(runtimeContext); if (!nativeClassifier.isNull()) { if (nativeClassifier.contains("${arch}")) { auto nat32Classifier = nativeClassifier; nat32Classifier.replace("${arch}", "32"); auto nat64Classifier = nativeClassifier; nat64Classifier.replace("${arch}", "64"); auto nat32info = m_mojangDownloads->getDownloadInfo(nat32Classifier); if (nat32info) { auto cooked_storage = raw_storage; cooked_storage.replace("${arch}", "32"); add_download(cooked_storage, nat32info->url, nat32info->sha1); } auto nat64info = m_mojangDownloads->getDownloadInfo(nat64Classifier); if (nat64info) { auto cooked_storage = raw_storage; cooked_storage.replace("${arch}", "64"); add_download(cooked_storage, nat64info->url, nat64info->sha1); } } else { auto info = m_mojangDownloads->getDownloadInfo(nativeClassifier); if (info) { add_download(raw_storage, info->url, info->sha1); } } } else { qDebug() << "Ignoring native library" << m_name.serialize() << "because it has no classifier for current OS"; } } else { if (m_mojangDownloads->artifact) { auto artifact = m_mojangDownloads->artifact; add_download(raw_storage, artifact->url, artifact->sha1); } else { qDebug() << "Ignoring java library" << m_name.serialize() << "because it has no artifact"; } } } else { auto raw_dl = [this, raw_storage]() { if (!m_absoluteURL.isEmpty()) { return m_absoluteURL; } if (m_repositoryURL.isEmpty()) { return BuildConfig.LIBRARY_BASE + raw_storage; } if (m_repositoryURL.endsWith('/')) { return m_repositoryURL + raw_storage; } else { return m_repositoryURL + QChar('/') + raw_storage; } }(); if (raw_storage.contains("${arch}")) { QString cooked_storage = raw_storage; QString cooked_dl = raw_dl; add_download(cooked_storage.replace("${arch}", "32"), cooked_dl.replace("${arch}", "32"), QString()); cooked_storage = raw_storage; cooked_dl = raw_dl; add_download(cooked_storage.replace("${arch}", "64"), cooked_dl.replace("${arch}", "64"), QString()); } else { add_download(raw_storage, raw_dl, QString()); } } return out; } /** * @brief Check if the library is active in the given runtime context. * * This function evaluates rules to determine if the library should be active, * considering both general rules and native compatibility. * * @param runtimeContext The current runtime context. * @return bool True if the library is active, false otherwise. */ bool Library::isActive(const RuntimeContext& runtimeContext) const { bool result = true; if (m_rules.empty()) { result = true; } else { Rule::Action ruleResult = Rule::Disallow; for (auto rule : m_rules) { Rule::Action temp = rule.apply(runtimeContext); if (temp != Rule::Defer) ruleResult = temp; } result = result && (ruleResult == Rule::Allow); } if (isNative()) { result = result && !getCompatibleNative(runtimeContext).isNull(); } return result; } /** * @brief Check if the library is considered local. * * @return bool True if the library is local, false otherwise. */ bool Library::isLocal() const { return m_hint == "local"; } /** * @brief Check if the library is always considered stale. * * @return bool True if the library is always stale, false otherwise. */ bool Library::isAlwaysStale() const { return m_hint == "always-stale"; } /** * @brief Get the compatible native classifier for the current runtime context. * * This function attempts to match the current runtime context with the appropriate * native classifier. * * @param runtimeContext The current runtime context. * @return QString The compatible native classifier, or an empty string if none is found. */ QString Library::getCompatibleNative(const RuntimeContext& runtimeContext) const { // try to match precise classifier "[os]-[arch]" auto entry = m_nativeClassifiers.constFind(runtimeContext.getClassifier()); // try to match imprecise classifier on legacy architectures "[os]" if (entry == m_nativeClassifiers.constEnd() && runtimeContext.isLegacyArch()) entry = m_nativeClassifiers.constFind(runtimeContext.system); if (entry == m_nativeClassifiers.constEnd()) return QString(); return entry.value(); } /** * @brief Set the storage prefix for the library. * * @param prefix The storage prefix to set. */ void Library::setStoragePrefix(QString prefix) { m_storagePrefix = prefix; } /** * @brief Get the default storage prefix for libraries. * * @return QString The default storage prefix. */ QString Library::defaultStoragePrefix() { return "libraries/"; } /** * @brief Get the current storage prefix for the library. * * @return QString The current storage prefix. */ QString Library::storagePrefix() const { if (m_storagePrefix.isEmpty()) { return defaultStoragePrefix(); } return m_storagePrefix; } /** * @brief Get the filename for the library in the current runtime context. * * This function determines the appropriate filename for the library, taking into * account native classifiers if applicable. * * @param runtimeContext The current runtime context. * @return QString The filename of the library. */ QString Library::filename(const RuntimeContext& runtimeContext) const { if (!m_filename.isEmpty()) { return m_filename; } // non-native? use only the gradle specifier if (!isNative()) { return m_name.getFileName(); } // otherwise native, override classifiers. Mojang HACK! GradleSpecifier nativeSpec = m_name; QString nativeClassifier = getCompatibleNative(runtimeContext); if (!nativeClassifier.isNull()) { nativeSpec.setClassifier(nativeClassifier); } else { nativeSpec.setClassifier("INVALID"); } return nativeSpec.getFileName(); } /** * @brief Get the display name for the library in the current runtime context. * * This function returns the display name for the library, defaulting to the filename * if no display name is set. * * @param runtimeContext The current runtime context. * @return QString The display name of the library. */ QString Library::displayName(const RuntimeContext& runtimeContext) const { if (!m_displayname.isEmpty()) return m_displayname; return filename(runtimeContext); } /** * @brief Get the storage suffix for the library in the current runtime context. * * This function determines the appropriate storage suffix for the library, taking into * account native classifiers if applicable. * * @param runtimeContext The current runtime context. * @return QString The storage suffix of the library. */ QString Library::storageSuffix(const RuntimeContext& runtimeContext) const { // non-native? use only the gradle specifier if (!isNative()) { return m_name.toPath(m_filename); } // otherwise native, override classifiers. Mojang HACK! GradleSpecifier nativeSpec = m_name; QString nativeClassifier = getCompatibleNative(runtimeContext); if (!nativeClassifier.isNull()) { nativeSpec.setClassifier(nativeClassifier); } else { nativeSpec.setClassifier("INVALID"); } return nativeSpec.toPath(m_filename); } PrismLauncher-11.0.3/launcher/minecraft/Logging.h0000644000175100017510000000164215224505336021276 0ustar runnerrunner // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include Q_DECLARE_LOGGING_CATEGORY(instanceProfileC) Q_DECLARE_LOGGING_CATEGORY(instanceProfileResolveC) PrismLauncher-11.0.3/launcher/minecraft/mod/0000755000175100017510000000000015224505336020313 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/minecraft/mod/DataPack.cpp0000644000175100017510000003072315224505336022474 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "DataPack.h" #include #include #include #include #include "MTPixmapCache.h" #include "Version.h" #include "minecraft/mod/tasks/LocalDataPackParseTask.h" // Values taken from: // https://minecraft.wiki/w/Pack_format#List_of_data_pack_formats static const QMap, std::pair> s_pack_format_versions = { { { 4, 0 }, { Version("1.13"), Version("1.14.4") } }, { { 5, 0 }, { Version("1.15"), Version("1.16.1") } }, { { 6, 0 }, { Version("1.16.2"), Version("1.16.5") } }, { { 7, 0 }, { Version("1.17"), Version("1.17.1") } }, { { 8, 0 }, { Version("1.18"), Version("1.18.1") } }, { { 9, 0 }, { Version("1.18.2"), Version("1.18.2") } }, { { 10, 0 }, { Version("1.19"), Version("1.19.3") } }, { { 11, 0 }, { Version("23w03a"), Version("23w05a") } }, { { 12, 0 }, { Version("1.19.4"), Version("1.19.4") } }, { { 13, 0 }, { Version("23w12a"), Version("23w14a") } }, { { 14, 0 }, { Version("23w16a"), Version("23w17a") } }, { { 15, 0 }, { Version("1.20"), Version("1.20.1") } }, { { 16, 0 }, { Version("23w31a"), Version("23w31a") } }, { { 17, 0 }, { Version("23w32a"), Version("23w35a") } }, { { 18, 0 }, { Version("1.20.2"), Version("1.20.2") } }, { { 19, 0 }, { Version("23w40a"), Version("23w40a") } }, { { 20, 0 }, { Version("23w41a"), Version("23w41a") } }, { { 21, 0 }, { Version("23w42a"), Version("23w42a") } }, { { 22, 0 }, { Version("23w43a"), Version("23w43b") } }, { { 23, 0 }, { Version("23w44a"), Version("23w44a") } }, { { 24, 0 }, { Version("23w45a"), Version("23w45a") } }, { { 25, 0 }, { Version("23w46a"), Version("23w46a") } }, { { 26, 0 }, { Version("1.20.3"), Version("1.20.4") } }, { { 27, 0 }, { Version("23w51a"), Version("23w51b") } }, { { 28, 0 }, { Version("24w03a"), Version("24w03b") } }, { { 29, 0 }, { Version("24w04a"), Version("24w04a") } }, { { 30, 0 }, { Version("24w05a"), Version("24w05b") } }, { { 31, 0 }, { Version("24w06a"), Version("24w06a") } }, { { 32, 0 }, { Version("24w07a"), Version("24w07a") } }, { { 33, 0 }, { Version("24w09a"), Version("24w09a") } }, { { 34, 0 }, { Version("24w10a"), Version("24w10a") } }, { { 35, 0 }, { Version("24w11a"), Version("24w11a") } }, { { 36, 0 }, { Version("24w12a"), Version("24w12a") } }, { { 37, 0 }, { Version("24w13a"), Version("24w13a") } }, { { 38, 0 }, { Version("24w14a"), Version("24w14a") } }, { { 39, 0 }, { Version("1.20.5-pre1"), Version("1.20.5-pre1") } }, { { 40, 0 }, { Version("1.20.5-pre2"), Version("1.20.5-pre2") } }, { { 41, 0 }, { Version("1.20.5"), Version("1.20.6") } }, { { 42, 0 }, { Version("24w18a"), Version("24w18a") } }, { { 43, 0 }, { Version("24w19a"), Version("24w19b") } }, { { 44, 0 }, { Version("24w20a"), Version("24w20a") } }, { { 45, 0 }, { Version("24w21a"), Version("24w21b") } }, { { 46, 0 }, { Version("1.21-pre1"), Version("1.21-pre1") } }, { { 47, 0 }, { Version("1.21-pre2"), Version("1.21-pre2") } }, { { 48, 0 }, { Version("1.21"), Version("1.21.1") } }, { { 49, 0 }, { Version("24w33a"), Version("24w33a") } }, { { 50, 0 }, { Version("24w34a"), Version("24w34a") } }, { { 51, 0 }, { Version("24w35a"), Version("24w35a") } }, { { 52, 0 }, { Version("24w36a"), Version("24w36a") } }, { { 53, 0 }, { Version("24w37a"), Version("24w37a") } }, { { 54, 0 }, { Version("24w38a"), Version("24w38a") } }, { { 55, 0 }, { Version("24w39a"), Version("24w39a") } }, { { 56, 0 }, { Version("24w40a"), Version("24w40a") } }, { { 57, 0 }, { Version("1.21.2"), Version("1.21.3") } }, { { 58, 0 }, { Version("24w44a"), Version("24w44a") } }, { { 59, 0 }, { Version("24w45a"), Version("24w45a") } }, { { 60, 0 }, { Version("24w46a"), Version("1.21.4-pre1") } }, { { 61, 0 }, { Version("1.21.4"), Version("1.21.4") } }, { { 62, 0 }, { Version("25w02a"), Version("25w02a") } }, { { 63, 0 }, { Version("25w03a"), Version("25w03a") } }, { { 64, 0 }, { Version("25w04a"), Version("25w04a") } }, { { 65, 0 }, { Version("25w05a"), Version("25w05a") } }, { { 66, 0 }, { Version("25w06a"), Version("25w06a") } }, { { 67, 0 }, { Version("25w07a"), Version("25w07a") } }, { { 68, 0 }, { Version("25w08a"), Version("25w08a") } }, { { 69, 0 }, { Version("25w09a"), Version("25w09b") } }, { { 70, 0 }, { Version("25w10a"), Version("1.21.5-pre1") } }, { { 71, 0 }, { Version("1.21.5"), Version("1.21.5") } }, { { 72, 0 }, { Version("25w15a"), Version("25w15a") } }, { { 73, 0 }, { Version("25w16a"), Version("25w16a") } }, { { 74, 0 }, { Version("25w17a"), Version("25w17a") } }, { { 75, 0 }, { Version("25w18a"), Version("25w18a") } }, { { 76, 0 }, { Version("25w19a"), Version("25w19a") } }, { { 77, 0 }, { Version("25w20a"), Version("25w20a") } }, { { 78, 0 }, { Version("25w21a"), Version("25w21a") } }, { { 79, 0 }, { Version("1.21.6-pre1"), Version("1.21.6-pre2") } }, { { 80, 0 }, { Version("1.21.6"), Version("1.21.6") } }, { { 81, 0 }, { Version("1.21.7"), Version("1.21.8") } }, { { 82, 0 }, { Version("25w31a"), Version("25w31a") } }, { { 83, 0 }, { Version("25w32a"), Version("25w32a") } }, { { 83, 1 }, { Version("25w33a"), Version("25w33a") } }, { { 84, 0 }, { Version("25w34a"), Version("25w34b") } }, { { 85, 0 }, { Version("25w35a"), Version("25w35a") } }, { { 86, 0 }, { Version("25w36a"), Version("25w36b") } }, { { 87, 0 }, { Version("25w37a"), Version("1.21.9-pre1") } }, { { 87, 1 }, { Version("1.21.9-pre1"), Version("1.21.9-pre1") } }, { { 88, 0 }, { Version("1.21.9"), Version("1.21.10") } }, { { 89, 0 }, { Version("25w41a"), Version("25w41a") } }, { { 90, 0 }, { Version("25w42a"), Version("25w42a") } }, { { 91, 0 }, { Version("25w43a"), Version("25w43a") } }, { { 92, 0 }, { Version("25w44a"), Version("25w44a") } }, { { 93, 0 }, { Version("25w45a"), Version("25w45a") } }, { { 93, 1 }, { Version("25w46a"), Version("25w46a") } }, { { 94, 0 }, { Version("1.21.11-pre1"), Version("1.21.11-pre3") } }, { { 94, 1 }, { Version("1.21.11-pre4"), Version("1.21.11") } }, { { 95, 0 }, { Version("26.1-snap1"), Version("26.1-snap1") } }, { { 96, 0 }, { Version("26.1-snap2"), Version("26.1-snap2") } }, { { 97, 0 }, { Version("26.1-snap3"), Version("26.1-snap3") } }, { { 97, 1 }, { Version("26.1-snap4"), Version("26.1-snap4") } }, { { 98, 0 }, { Version("26.1-snap5"), Version("26.1-snap5") } }, { { 99, 0 }, { Version("26.1-snap6"), Version("26.1-snap6") } }, { { 99, 1 }, { Version("26.1-snap7"), Version("26.1-snap7") } }, { { 99, 2 }, { Version("26.1-snap8"), Version("26.1-snap9") } }, { { 99, 3 }, { Version("26.1-snap10"), Version("26.1-snap10") } }, { { 100, 0 }, { Version("26.1-snap11"), Version("26.1-snap11") } }, }; void DataPack::setPackFormat(int new_format_id, std::pair min_format, std::pair max_format) { QMutexLocker locker(&m_data_lock); m_pack_format = new_format_id; m_min_format = min_format; m_max_format = max_format; } void DataPack::setDescription(QString new_description) { QMutexLocker locker(&m_data_lock); m_description = new_description; } void DataPack::setImage(QImage new_image) const { QMutexLocker locker(&m_data_lock); Q_ASSERT(!new_image.isNull()); if (m_pack_image_cache_key.key.isValid()) PixmapCache::instance().remove(m_pack_image_cache_key.key); // scale the image to avoid flooding the pixmapcache auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap); m_pack_image_cache_key.was_ever_used = true; // This can happen if the pixmap is too big to fit in the cache :c if (!m_pack_image_cache_key.key.isValid()) { qWarning() << "Could not insert a image cache entry! Ignoring it."; m_pack_image_cache_key.was_ever_used = false; } } QPixmap DataPack::image(QSize size, Qt::AspectRatioMode mode) const { QPixmap cached_image; if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; return cached_image.scaled(size, mode, Qt::SmoothTransformation); } // No valid image we can get if (!m_pack_image_cache_key.was_ever_used) { return {}; } else { qDebug() << "Data Pack" << name() << "Had it's image evicted from the cache. reloading..."; PixmapCache::markCacheMissByEviciton(); } // Imaged got evicted from the cache. Re-process it and retry. DataPackUtils::processPackPNG(this); return image(size); } static std::pair map(std::pair format, const QMap, std::pair>& versions) { if (format.first == 0 || !versions.contains(format)) { return { {}, {} }; } return versions.constFind(format).value(); } static std::pair map(int format, const QMap, std::pair>& versions) { return map({ format, 0 }, versions); } int DataPack::compare(const Resource& other, SortType type) const { const auto& cast_other = static_cast(other); if (type == SortType::PACK_FORMAT) { auto this_ver = packFormat(); auto other_ver = cast_other.packFormat(); if (this_ver > other_ver) return 1; if (this_ver < other_ver) return -1; } else { return Resource::compare(other, type); } return 0; } bool DataPack::applyFilter(QRegularExpression filter) const { if (filter.match(description()).hasMatch()) { return true; } if (filter.match(QString::number(packFormat())).hasMatch()) { return true; } auto versions = { map(m_pack_format, mappings()), map(m_min_format, mappings()), map(m_max_format, mappings()) }; for (const auto& version : versions) { if (!version.first.isEmpty()) { if (filter.match(version.first.toString()).hasMatch()) { return true; } if (filter.match(version.second.toString()).hasMatch()) { return true; } } } return Resource::applyFilter(filter); } bool DataPack::valid() const { return m_pack_format != 0 || (m_min_format.first != 0 && m_max_format.first != 0); } QMap, std::pair> DataPack::mappings() const { return s_pack_format_versions; } QString DataPack::packFormatStr() const { if (m_pack_format != 0) { auto version_bounds = map(m_pack_format, mappings()); if (version_bounds.first.toString().isEmpty()) { return QString::number(m_pack_format); } return QString("%1 (%2 - %3)") .arg(QString::number(m_pack_format), version_bounds.first.toString(), version_bounds.second.toString()); } auto min_bound = map(m_min_format, mappings()); auto max_bound = map(m_max_format, mappings()); auto min_version = min_bound.first; auto max_version = max_bound.second; if (min_version.isEmpty() || max_version.isEmpty()) { return tr("Unrecognized"); } auto str = QString("[") + QString::number(m_min_format.first); if (m_min_format.second != 0) { str += "." + QString::number(m_min_format.second); } str += QString(" - ") + QString::number(m_max_format.first); if (m_max_format.second != 0) { str += "." + QString::number(m_max_format.second); } return str + QString(" (%2 - %3)").arg(min_version.toString(), max_version.toString()); } PrismLauncher-11.0.3/launcher/minecraft/mod/WorldSave.cpp0000644000175100017510000000241215224505336022724 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "WorldSave.h" #include "minecraft/mod/tasks/LocalWorldSaveParseTask.h" void WorldSave::setSaveFormat(WorldSaveFormat new_save_format) { QMutexLocker locker(&m_data_lock); m_save_format = new_save_format; } void WorldSave::setSaveDirName(QString dir_name) { QMutexLocker locker(&m_data_lock); m_save_dir_name = dir_name; } bool WorldSave::valid() const { return m_save_format != WorldSaveFormat::INVALID; } PrismLauncher-11.0.3/launcher/minecraft/mod/Resource.h0000644000175100017510000001670715224505336022266 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include "MetadataHandler.h" #include "QObjectPtr.h" class BaseInstance; enum class ResourceType { UNKNOWN, //!< Indicates an unspecified resource type. ZIPFILE, //!< The resource is a zip file containing the resource's class files. SINGLEFILE, //!< The resource is a single file (not a zip file). FOLDER, //!< The resource is in a folder on the filesystem. LITEMOD, //!< The resource is a litemod }; QDebug operator<<(QDebug debug, ResourceType type); enum class ResourceStatus { INSTALLED, // Both JAR and Metadata are present NOT_INSTALLED, // Only the Metadata is present NO_METADATA, // Only the JAR is present UNKNOWN, // Default status }; QDebug operator<<(QDebug debug, ResourceStatus status); enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, MC_VERSIONS, LOADERS, RELEASE_TYPE, REQUIRES, REQUIRED_BY, }; enum class EnableAction { ENABLE, DISABLE, TOGGLE }; /** General class for managed resources. It mirrors a file in disk, with some more info * for display and house-keeping purposes. * * Subclass it to add additional data / behavior, such as Mods or Resource packs. */ class Resource : public QObject { Q_OBJECT Q_DISABLE_COPY(Resource) public: using Ptr = shared_qobject_ptr; using WeakPtr = QPointer; Resource(QObject* parent = nullptr); Resource(QFileInfo file_info); Resource(QString file_path) : Resource(QFileInfo(file_path)) {} ~Resource() override = default; void setFile(QFileInfo file_info); void parseFile(); auto fileinfo() const -> QFileInfo { return m_file_info; } auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; } auto internal_id() const -> QString { return m_internal_id; } auto type() const -> ResourceType { return m_type; } bool enabled() const { return m_enabled; } auto getOriginalFileName() const -> QString; QString sizeStr() const { return m_size_str; } qint64 sizeInfo() const { return m_size_info; } virtual auto name() const -> QString; virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } auto status() const -> ResourceStatus { return m_status; }; auto metadata() -> std::shared_ptr { return m_metadata; } auto metadata() const -> std::shared_ptr { return m_metadata; } auto provider() const -> QString; virtual auto homepage() const -> QString; void setStatus(ResourceStatus status) { m_status = status; } void setMetadata(std::shared_ptr&& metadata); void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared(metadata)); } /** * Returns compatibility issues with the resource and the instance. * This is initially empty, and may be updated when calling updateIssues. */ QStringList issues() const; void updateIssues(const BaseInstance* inst); bool hasIssues() const { return !m_issues.empty(); } /** Compares two Resources, for sorting purposes, considering a ascending order, returning: * > 0: 'this' comes after 'other' * = 0: 'this' is equal to 'other' * < 0: 'this' comes before 'other' */ virtual int compare(const Resource& other, SortType type = SortType::NAME) const; /** Returns whether the given filter should filter out 'this' (false), * or if such filter includes the Resource (true). */ virtual bool applyFilter(QRegularExpression filter) const; /** Changes the enabled property, according to 'action'. * * Returns whether a change was applied to the Resource's properties. */ bool enable(EnableAction action); auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; } auto isResolving() const -> bool { return m_is_resolving; } auto isResolved() const -> bool { return m_is_resolved; } auto resolutionTicket() const -> int { return m_resolution_ticket; } void setResolving(bool resolving, int resolutionTicket) { m_is_resolving = resolving; m_resolution_ticket = resolutionTicket; } // Delete all files of this resource. auto destroy(const QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; // Delete the metadata only. auto destroyMetadata(const QDir& index_dir) -> void; auto isSymLink() const -> bool { return m_file_info.isSymLink(); } /** * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance * * @param instPath path to an instance directory * @return true * @return false */ bool isSymLinkUnder(const QString& instPath) const; bool isMoreThanOneHardLink() const; protected: /* The file corresponding to this resource. */ QFileInfo m_file_info; /* The cached date when this file was last changed. */ QDateTime m_changed_date_time; /* Internal ID for internal purposes. Properties such as human-readability should not be assumed. */ QString m_internal_id; /* Name as reported via the file name. In the absence of a better name, this is shown to the user. */ QString m_name; /* The type of file we're dealing with. */ ResourceType m_type = ResourceType::UNKNOWN; /* Installation status of the resource. */ ResourceStatus m_status = ResourceStatus::UNKNOWN; std::shared_ptr m_metadata = nullptr; /* Whether the resource is enabled (e.g. shows up in the game) or not. */ bool m_enabled = true; QList m_issues; /* Used to keep trach of pending / concluded actions on the resource. */ bool m_is_resolving = false; bool m_is_resolved = false; int m_resolution_ticket = 0; QString m_size_str; qint64 m_size_info; }; PrismLauncher-11.0.3/launcher/minecraft/mod/TexturePackFolderModel.cpp0000644000175100017510000001304015224505336025371 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "TexturePackFolderModel.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" #include "minecraft/mod/tasks/ResourceFolderLoadTask.h" TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size" }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }); m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; m_columnsHideable = { false, true, false, true, true, true }; } Task* TexturePackFolderModel::createParseTask(Resource& resource) { return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast(resource)); } QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const { if (!validateIndex(index)) return {}; int row = index.row(); int column = index.column(); switch (role) { case Qt::BackgroundRole: return rowBackground(row); case Qt::DecorationRole: { if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } break; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } break; } // map the columns to the base equivilents QModelIndex mappedIndex; switch (column) { case ActiveColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); break; case NameColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); break; case DateColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); break; case ProviderColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); break; case SizeColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); break; } if (mappedIndex.isValid()) { return ResourceFolderModel::data(mappedIndex, role); } return {}; } QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case ActiveColumn: case NameColumn: case DateColumn: case ImageColumn: case ProviderColumn: case SizeColumn: return columnNames().at(section); default: return {}; } case Qt::ToolTipRole: { switch (section) { case ActiveColumn: return tr("Is the texture pack enabled?"); case NameColumn: return tr("The name of the texture pack."); case DateColumn: return tr("The date and time this texture pack was last changed (or added)."); case ProviderColumn: return tr("The source provider of the texture pack."); case SizeColumn: return tr("The size of the texture pack."); default: return {}; } } default: break; } return {}; } int TexturePackFolderModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : NUM_COLUMNS; } PrismLauncher-11.0.3/launcher/minecraft/mod/TexturePack.h0000644000175100017510000000432515224505336022727 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "Resource.h" #include #include #include #include class Version; class TexturePack : public Resource { Q_OBJECT public: using Ptr = shared_qobject_ptr; TexturePack(QObject* parent = nullptr) : Resource(parent) {} TexturePack(QFileInfo file_info) : Resource(file_info) {} /** Gets the description of the texture pack. */ QString description() const { return m_description; } /** Gets the image of the texture pack, converted to a QPixmap for drawing, and scaled to size. */ QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ void setDescription(QString new_description); /** Thread-safe. */ void setImage(QImage new_image) const; bool valid() const override; protected: mutable QMutex m_data_lock; /** The texture pack's description, as defined in the pack.txt file. */ QString m_description; /** The texture pack's image file cache key, for access in the QPixmapCache global instance. * * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. */ struct { QPixmapCache::Key key; bool was_ever_used = false; } mutable m_pack_image_cache_key; }; PrismLauncher-11.0.3/launcher/minecraft/mod/ModDetails.h0000644000175100017510000001150115224505336022507 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include struct ModLicense { QString name = {}; QString id = {}; QString url = {}; QString description = {}; ModLicense() {} ModLicense(const QString license) { // FIXME: come up with a better license parsing. // handle SPDX identifiers? https://spdx.org/licenses/ auto parts = license.split(' '); QStringList notNameParts = {}; for (auto part : parts) { auto _url = QUrl(part); if (part.startsWith("(") && part.endsWith(")")) _url = QUrl(part.mid(1, part.size() - 2)); if (_url.isValid() && !_url.scheme().isEmpty() && !_url.host().isEmpty()) { this->url = _url.toString(); notNameParts.append(part); continue; } } for (auto part : notNameParts) { parts.removeOne(part); } auto licensePart = parts.join(' '); this->name = licensePart; this->description = licensePart; if (parts.size() == 1) { this->id = parts.first(); } } ModLicense(const QString& name_, const QString& id_, const QString& url_, const QString& description_) : name(name_), id(id_), url(url_), description(description_) {} ModLicense(const ModLicense& other) : name(other.name), id(other.id), url(other.url), description(other.description) {} ModLicense& operator=(const ModLicense& other) { this->name = other.name; this->id = other.id; this->url = other.url; this->description = other.description; return *this; } ModLicense& operator=(const ModLicense&& other) { this->name = other.name; this->id = other.id; this->url = other.url; this->description = other.description; return *this; } bool isEmpty() { return this->name.isEmpty() && this->id.isEmpty() && this->url.isEmpty() && this->description.isEmpty(); } }; struct ModDetails { /* Mod ID as defined in the ModLoader-specific metadata */ QString mod_id = {}; /* Human-readable name */ QString name = {}; /* Human-readable mod version */ QString version = {}; /* Human-readable minecraft version */ QString mcversion = {}; /* URL for mod's home page */ QString homeurl = {}; /* Human-readable description */ QString description = {}; /* List of the author's names */ QStringList authors = {}; /* Issue Tracker URL */ QString issue_tracker = {}; /* License */ QList licenses = {}; /* Path of mod logo */ QString icon_file = {}; QStringList dependencies = {}; ModDetails() = default; /** Metadata should be handled manually to properly set the mod status. */ ModDetails(const ModDetails& other) : mod_id(other.mod_id) , name(other.name) , version(other.version) , mcversion(other.mcversion) , homeurl(other.homeurl) , description(other.description) , authors(other.authors) , issue_tracker(other.issue_tracker) , licenses(other.licenses) , icon_file(other.icon_file) , dependencies(other.dependencies) {} ModDetails& operator=(const ModDetails& other) = default; ModDetails& operator=(ModDetails&& other) = default; }; PrismLauncher-11.0.3/launcher/minecraft/mod/DataPack.h0000644000175100017510000000576615224505336022152 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "Resource.h" #include #include #include class Version; /* TODO: * * Store localized descriptions * */ class DataPack : public Resource { Q_OBJECT public: DataPack(QObject* parent = nullptr) : Resource(parent) {} DataPack(QFileInfo file_info) : Resource(file_info) {} /** Gets the numerical ID of the pack format. */ int packFormat() const { return m_pack_format; } /** Gets the description of the data pack. */ QString description() const { return m_description; } /** Gets the image of the data pack, converted to a QPixmap for drawing, and scaled to size. */ QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ void setPackFormat(int new_format_id, std::pair min_format, std::pair max_format); /** Thread-safe. */ void setDescription(QString new_description); /** Thread-safe. */ void setImage(QImage new_image) const; bool valid() const override; [[nodiscard]] int compare(const Resource& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; QString packFormatStr() const; protected: virtual QMap, std::pair> mappings() const; protected: mutable QMutex m_data_lock; /* The 'version' of a data pack, as defined in the pack.mcmeta file. * See https://minecraft.wiki/w/Data_pack#pack.mcmeta */ int m_pack_format = 0; std::pair m_min_format; std::pair m_max_format; /** The data pack's description, as defined in the pack.mcmeta file. */ QString m_description; /** The data pack's image file cache key, for access in the QPixmapCache global instance. * * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. */ struct { QPixmapCache::Key key; bool was_ever_used = false; } mutable m_pack_image_cache_key; }; PrismLauncher-11.0.3/launcher/minecraft/mod/DataPackFolderModel.h0000644000175100017510000000465015224505336024256 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ResourceFolderModel.h" #include "DataPack.h" #include "ResourcePack.h" class DataPackFolderModel : public ResourceFolderModel { Q_OBJECT public: enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, NUM_COLUMNS }; explicit DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); virtual QString id() const override { return "datapacks"; } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override; [[nodiscard]] Task* createParseTask(Resource&) override; RESOURCE_HELPERS(DataPack) }; PrismLauncher-11.0.3/launcher/minecraft/mod/ResourceFolderModel.h0000644000175100017510000002762315224505336024402 0ustar runnerrunner#pragma once #include #include #include #include #include #include #include #include #include #include "Resource.h" #include "BaseInstance.h" #include "tasks/ConcurrentTask.h" #include "tasks/Task.h" class QSortFilterProxyModel; /* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ #define RESOURCE_HELPERS(T) \ T& at(int index) \ { \ return *static_cast(m_resources[index].get()); \ } \ const T& at(int index) const \ { \ return *static_cast(m_resources.at(index).get()); \ } \ QList selected##T##s(const QModelIndexList& indexes) \ { \ QList result; \ for (const QModelIndex& index : indexes) { \ if (index.column() != 0) \ continue; \ \ result.append(&at(index.row())); \ } \ return result; \ } \ QList all##T##s() \ { \ QList result; \ result.reserve(m_resources.size()); \ \ for (const Resource::Ptr& resource : m_resources) \ result.append(static_cast(resource.get())); \ \ return result; \ } /** A basic model for external resources. * * This model manages a list of resources. As such, external users of such resources do not own them, * and the resource's lifetime is contingent on the model's lifetime. * * TODO: Make the resources unique pointers accessible through weak pointers. */ class ResourceFolderModel : public QAbstractListModel { Q_OBJECT public: ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); ~ResourceFolderModel() override; virtual QString id() const { return "resource"; } /** Starts watching the paths for changes. * * Returns whether starting to watch all the paths was successful. * If one or more fails, it returns false. */ bool startWatching(const QStringList& paths); /** Stops watching the paths for changes. * * Returns whether stopping to watch all the paths was successful. * If one or more fails, it returns false. */ bool stopWatching(const QStringList& paths); /* Helper methods for subclasses, using a predetermined list of paths. */ virtual bool startWatching() { return startWatching({ indexDir().absolutePath(), m_dir.absolutePath() }); } virtual bool stopWatching() { return stopWatching({ indexDir().absolutePath(), m_dir.absolutePath() }); } virtual QDir indexDir() const { return { QString("%1/.index").arg(dir().absolutePath()) }; } /** Given a path in the system, install that resource, moving it to its place in the * instance file hierarchy. * * Returns whether the installation was succcessful. */ virtual bool installResource(QString path); virtual void installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers); /** Uninstall (i.e. remove all data about it) a resource, given its file name. * * Returns whether the removal was successful. */ virtual bool uninstallResource(const QString& file_name, bool preserve_metadata = false); virtual bool deleteResources(const QModelIndexList&); virtual void deleteMetadata(const QModelIndexList&); /** Applies the given 'action' to the resources in 'indexes'. * * Returns whether the action was successfully applied to all resources. */ virtual bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action); /** Creates a new update task and start it. Returns false if no update was done, like when an update is already underway. */ virtual bool update(); /** Creates a new parse task, if needed, for 'res' and start it.*/ virtual void resolveResource(Resource::Ptr res); qsizetype size() const { return m_resources.size(); } [[nodiscard]] bool empty() const { return size() == 0; } Resource& at(int index) { return *m_resources[index].get(); } const Resource& at(int index) const { return *m_resources.at(index).get(); } QList selectedResources(const QModelIndexList& indexes); QList allResources(); Resource::Ptr find(QString id); QDir const& dir() const { return m_dir; } /** Checks whether there's any parse tasks being done. * * Since they can be quite expensive, and are usually done in a separate thread, if we were to destroy the model while having * such tasks would introduce an undefined behavior, most likely resulting in a crash. */ bool hasPendingParseTasks() const; /* Qt behavior */ /* Basic columns */ enum Columns { ActiveColumn = 0, NameColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; } int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast(size()); } int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; } Qt::DropActions supportedDropActions() const override; /// flags, mostly to support drag&drop Qt::ItemFlags flags(const QModelIndex& index) const override; QStringList mimeTypes() const override; [[nodiscard]] bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; [[nodiscard]] bool validateIndex(const QModelIndex& index) const; QBrush rowBackground(int row) const; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; void setupHeaderAction(QAction* act, int column); void saveColumns(QTreeView* tree); void loadColumns(QTreeView* tree); QMenu* createHeaderContextMenu(QTreeView* tree); /** This creates a proxy model to filter / sort the model for a UI. * * The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead! */ QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr); SortType columnToSortKey(size_t column) const; QList columnResizeModes() const { return m_column_resize_modes; } class ProxyModel : public QSortFilterProxyModel { public: explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} protected: bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; }; QString instDirPath() const; BaseInstance* instance() const { return m_instance; } signals: void updateFinished(); void parseFinished(); protected: [[nodiscard]] virtual Task* createPreUpdateTask() { return nullptr; } /** This creates a new update task to be executed by update(). * * The task should load and parse all resources necessary, and provide a way of accessing such results. * * This Task is normally executed when opening a page, so it shouldn't contain much heavy work. * If such work is needed, try using it in the Task create by createParseTask() instead! */ [[nodiscard]] Task* createUpdateTask(); [[nodiscard]] virtual Resource* createResource(const QFileInfo& info) { return new Resource(info); } /** This creates a new parse task to be executed by onUpdateSucceeded(). * * This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed * in the background, so it slowly updates the UI as tasks get done. */ [[nodiscard]] virtual Task* createParseTask(Resource&) { return nullptr; } /** Standard implementation of the model update logic. * * It uses set operations to find differences between the current state and the updated state, * to act only on those disparities. * */ void applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources); protected slots: void directoryChanged(QString); /** Called when the update task is successful. * * This usually calls static_cast on the specific Task type returned by createUpdateTask, * so care must be taken in such cases. * TODO: Figure out a way to express this relationship better without templated classes (Q_OBJECT macro disallows that). */ virtual void onUpdateSucceeded(); virtual void onUpdateFailed() {} /** Called when the parse task with the given ticket is successful. * * This is just a simple reference implementation. You probably want to override it with your own logic in a subclass * if the resource is complex and has more stuff to parse. */ virtual void onParseSucceeded(int ticket, QString resource_id); virtual void onParseFailed(int ticket, QString resource_id); protected: // Represents the relationship between a column's index (represented by the list index), and it's sorting key. // As such, the order in with they appear is very important! QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; QStringList m_column_names = { "Enable", "Name", "Last Modified", "Provider", "Size" }; QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }; QList m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; QList m_columnsHideable = { false, false, true, true, true }; QDir m_dir; BaseInstance* m_instance; QFileSystemWatcher m_watcher; bool m_is_watching = false; bool m_is_indexed; bool m_first_folder_load = true; Task::Ptr m_current_update_task = nullptr; bool m_scheduled_update = false; QList m_resources; // Represents the relationship between a resource's internal ID and it's row position on the model. QMap m_resources_index; // Runs off-thread ConcurrentTask m_resourceResolver; bool m_resourceResolverRunning = false; QMap m_active_parse_tasks; std::atomic m_next_resolution_ticket = 0; }; PrismLauncher-11.0.3/launcher/minecraft/mod/WorldSave.h0000644000175100017510000000362615224505336022401 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "Resource.h" #include class Version; enum class WorldSaveFormat { SINGLE, MULTI, INVALID }; class WorldSave : public Resource { Q_OBJECT public: using Ptr = shared_qobject_ptr; WorldSave(QObject* parent = nullptr) : Resource(parent) {} WorldSave(QFileInfo file_info) : Resource(file_info) {} /** Gets the format of the save. */ WorldSaveFormat saveFormat() const { return m_save_format; } /** Gets the name of the save dir (first found in multi mode). */ QString saveDirName() const { return m_save_dir_name; } /** Thread-safe. */ void setSaveFormat(WorldSaveFormat new_save_format); /** Thread-safe. */ void setSaveDirName(QString dir_name); bool valid() const override; protected: mutable QMutex m_data_lock; /** The format in which the save file is in. * Since saves can be distributed in various slightly different ways, this allows us to treat them separately. */ WorldSaveFormat m_save_format = WorldSaveFormat::INVALID; QString m_save_dir_name; }; PrismLauncher-11.0.3/launcher/minecraft/mod/Mod.cpp0000644000175100017510000002121115224505336021533 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Mod.h" #include #include #include #include "MTPixmapCache.h" #include "MetadataHandler.h" #include "Resource.h" #include "Version.h" #include "minecraft/mod/ModDetails.h" #include "minecraft/mod/tasks/LocalModParseTask.h" #include "modplatform/ModIndex.h" Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details() { m_enabled = (file.suffix() != "disabled"); } void Mod::setDetails(const ModDetails& details) { m_local_details = details; } int Mod::compare(const Resource& other, SortType type) const { auto cast_other = dynamic_cast(&other); if (!cast_other) return Resource::compare(other, type); switch (type) { default: case SortType::ENABLED: case SortType::NAME: case SortType::DATE: case SortType::SIZE: return Resource::compare(other, type); case SortType::VERSION: { auto this_ver = Version(version()); auto other_ver = Version(cast_other->version()); if (this_ver > other_ver) return 1; if (this_ver < other_ver) return -1; break; } case SortType::SIDE: { auto compare_result = QString::compare(side(), cast_other->side(), Qt::CaseInsensitive); if (compare_result != 0) return compare_result; break; } case SortType::MC_VERSIONS: { auto compare_result = QString::compare(mcVersions(), cast_other->mcVersions(), Qt::CaseInsensitive); if (compare_result != 0) return compare_result; break; } case SortType::LOADERS: { auto compare_result = QString::compare(loaders(), cast_other->loaders(), Qt::CaseInsensitive); if (compare_result != 0) return compare_result; break; } case SortType::RELEASE_TYPE: { auto compare_result = QString::compare(releaseType(), cast_other->releaseType(), Qt::CaseInsensitive); if (compare_result != 0) return compare_result; break; } case SortType::REQUIRED_BY: { if (requiredByCount() > cast_other->requiredByCount()) return 1; if (requiredByCount() < cast_other->requiredByCount()) return -1; break; } case SortType::REQUIRES: { if (requiresCount() > cast_other->requiresCount()) return 1; if (requiresCount() < cast_other->requiresCount()) return -1; break; } } return 0; } bool Mod::applyFilter(QRegularExpression filter) const { if (filter.match(description()).hasMatch()) return true; for (auto& author : authors()) { if (filter.match(author).hasMatch()) { return true; } } return Resource::applyFilter(filter); } auto Mod::details() const -> const ModDetails& { return m_local_details; } auto Mod::name() const -> QString { auto d_name = details().name; if (!d_name.isEmpty()) return d_name; return Resource::name(); } auto Mod::mod_id() const -> QString { auto d_mod_id = details().mod_id; if (!d_mod_id.isEmpty()) return d_mod_id; return Resource::name(); } auto Mod::version() const -> QString { return details().version; } auto Mod::homepage() const -> QString { QString metaUrl = Resource::homepage(); if (metaUrl.isEmpty()) return details().homeurl; else return metaUrl; } auto Mod::loaders() const -> QString { if (metadata()) { QStringList loaders; auto modLoaders = metadata()->loaders; for (auto loader : ModPlatform::modLoaderTypesToList(modLoaders)) { loaders << getModLoaderAsString(loader); } return loaders.join(", "); } return {}; } auto Mod::side() const -> QString { if (metadata()) return ModPlatform::SideUtils::toString(metadata()->side); return ModPlatform::SideUtils::toString(ModPlatform::Side::UniversalSide); } auto Mod::mcVersions() const -> QString { if (metadata()) return metadata()->mcVersions.join(", "); return {}; } auto Mod::releaseType() const -> QString { if (metadata()) return metadata()->releaseType.toString(); return ModPlatform::IndexedVersionType(ModPlatform::IndexedVersionType::Unknown).toString(); } auto Mod::description() const -> QString { return details().description; } auto Mod::authors() const -> QStringList { return details().authors; } void Mod::finishResolvingWithDetails(ModDetails&& details) { m_is_resolving = false; m_is_resolved = true; m_local_details = std::move(details); if (!iconPath().isEmpty()) { m_packImageCacheKey.wasReadAttempt = false; } } auto Mod::licenses() const -> const QList& { return details().licenses; } auto Mod::issueTracker() const -> QString { return details().issue_tracker; } QPixmap Mod::setIcon(QImage new_image) const { QMutexLocker locker(&m_data_lock); Q_ASSERT(!new_image.isNull()); if (m_packImageCacheKey.key.isValid()) PixmapCache::remove(m_packImageCacheKey.key); // scale the image to avoid flooding the pixmapcache auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); m_packImageCacheKey.key = PixmapCache::insert(pixmap); m_packImageCacheKey.wasEverUsed = true; m_packImageCacheKey.wasReadAttempt = true; return pixmap; } QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const { auto pixmap_transform = [&size, &mode](QPixmap pixmap) { if (size.isNull()) return pixmap; return pixmap.scaled(size, mode, Qt::SmoothTransformation); }; QPixmap cached_image; if (PixmapCache::find(m_packImageCacheKey.key, &cached_image)) { return pixmap_transform(cached_image); } // No valid image we can get if ((!m_packImageCacheKey.wasEverUsed && m_packImageCacheKey.wasReadAttempt) || iconPath().isEmpty()) return {}; if (m_packImageCacheKey.wasEverUsed) { qDebug() << "Mod" << name() << "Had it's icon evicted from the cache. reloading..."; PixmapCache::markCacheMissByEviciton(); } // Image got evicted from the cache or an attempt to load it has not been made. load it and retry. m_packImageCacheKey.wasReadAttempt = true; if (ModUtils::loadIconFile(*this, &cached_image)) { return pixmap_transform(cached_image); } // Image failed to load return {}; } bool Mod::valid() const { return !m_local_details.mod_id.isEmpty(); } QStringList Mod::dependencies() const { return details().dependencies; } int Mod::requiredByCount() const { return m_requiredByCount; } int Mod::requiresCount() const { return m_requiresCount; } void Mod::setRequiredByCount(int value) { m_requiredByCount = value; } void Mod::setRequiresCount(int value) { m_requiresCount = value; } PrismLauncher-11.0.3/launcher/minecraft/mod/TexturePack.cpp0000644000175100017510000000460515224505336023263 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "TexturePack.h" #include #include #include "MTPixmapCache.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" void TexturePack::setDescription(QString new_description) { QMutexLocker locker(&m_data_lock); m_description = new_description; } void TexturePack::setImage(QImage new_image) const { QMutexLocker locker(&m_data_lock); Q_ASSERT(!new_image.isNull()); if (m_pack_image_cache_key.key.isValid()) PixmapCache::remove(m_pack_image_cache_key.key); // scale the image to avoid flooding the pixmapcache auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); m_pack_image_cache_key.key = PixmapCache::insert(pixmap); m_pack_image_cache_key.was_ever_used = true; } QPixmap TexturePack::image(QSize size, Qt::AspectRatioMode mode) const { QPixmap cached_image; if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; return cached_image.scaled(size, mode, Qt::SmoothTransformation); } // No valid image we can get if (!m_pack_image_cache_key.was_ever_used) { return {}; } else { qDebug() << "Texture Pack" << name() << "Had it's image evicted from the cache. reloading..."; PixmapCache::markCacheMissByEviciton(); } // Imaged got evicted from the cache. Re-process it and retry. TexturePackUtils::processPackPNG(*this); return image(size); } bool TexturePack::valid() const { return m_description != nullptr; } PrismLauncher-11.0.3/launcher/minecraft/mod/ResourcePack.h0000644000175100017510000000112415224505336023050 0ustar runnerrunner#pragma once #include "Resource.h" #include "minecraft/mod/DataPack.h" #include #include #include #include class Version; /* TODO: * * Store localized descriptions * */ class ResourcePack : public DataPack { Q_OBJECT public: ResourcePack(QObject* parent = nullptr) : DataPack(parent) {} ResourcePack(QFileInfo file_info) : DataPack(file_info) {} /** Gets, respectively, the lower and upper versions supported by the set pack format. */ QMap, std::pair> mappings() const override; }; PrismLauncher-11.0.3/launcher/minecraft/mod/MetadataHandler.h0000644000175100017510000000315015224505336023501 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "modplatform/packwiz/Packwiz.h" namespace Metadata { using ModStruct = Packwiz::V1::Mod; inline ModStruct create(const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) { return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version); } inline void update(const QDir& index_dir, ModStruct& mod) { Packwiz::V1::updateModIndex(index_dir, mod); } inline void remove(const QDir& index_dir, QString mod_slug) { Packwiz::V1::deleteModIndex(index_dir, mod_slug); } inline ModStruct get(const QDir& index_dir, QString mod_slug) { return Packwiz::V1::getIndexForMod(index_dir, std::move(mod_slug)); } inline ModStruct get(const QDir& index_dir, QVariant& mod_id) { return Packwiz::V1::getIndexForMod(index_dir, mod_id); } }; // namespace Metadata PrismLauncher-11.0.3/launcher/minecraft/mod/ResourcePackFolderModel.h0000644000175100017510000000171315224505336025171 0ustar runnerrunner#pragma once #include "ResourceFolderModel.h" #include "ResourcePack.h" class ResourcePackFolderModel : public ResourceFolderModel { Q_OBJECT public: enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; explicit ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); QString id() const override { return "resourcepacks"; } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new ResourcePack(file); } [[nodiscard]] Task* createParseTask(Resource&) override; RESOURCE_HELPERS(ResourcePack) }; PrismLauncher-11.0.3/launcher/minecraft/mod/Mod.h0000644000175100017510000000752215224505336021211 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include #include "ModDetails.h" #include "Resource.h" class Mod : public Resource { Q_OBJECT public: using Ptr = shared_qobject_ptr; using WeakPtr = QPointer; Mod() = default; Mod(const QFileInfo& file); Mod(QString file_path) : Mod(QFileInfo(file_path)) {} auto details() const -> const ModDetails&; auto name() const -> QString override; auto mod_id() const -> QString; auto version() const -> QString; auto homepage() const -> QString override; auto description() const -> QString; auto authors() const -> QStringList; auto licenses() const -> const QList&; auto issueTracker() const -> QString; auto side() const -> QString; auto loaders() const -> QString; auto mcVersions() const -> QString; auto releaseType() const -> QString; QStringList dependencies() const; int requiredByCount() const; int requiresCount() const; void setRequiredByCount(int value); void setRequiresCount(int value); /** Get the intneral path to the mod's icon file*/ QString iconPath() const { return m_local_details.icon_file; } /** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */ QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ QPixmap setIcon(QImage new_image) const; void setDetails(const ModDetails& details); bool valid() const override; [[nodiscard]] int compare(const Resource& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; // Delete all the files of this mod auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; // Delete the metadata only void destroyMetadata(QDir& index_dir); void finishResolvingWithDetails(ModDetails&& details); protected: ModDetails m_local_details; mutable QMutex m_data_lock; struct { QPixmapCache::Key key; bool wasEverUsed = false; bool wasReadAttempt = false; } mutable m_packImageCacheKey; int m_requiredByCount = 0; int m_requiresCount = 0; }; PrismLauncher-11.0.3/launcher/minecraft/mod/TexturePackFolderModel.h0000644000175100017510000000470715224505336025050 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ResourceFolderModel.h" #include "TexturePack.h" class TexturePackFolderModel : public ResourceFolderModel { Q_OBJECT public: enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; explicit TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); virtual QString id() const override { return "texturepacks"; } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new TexturePack(file); } [[nodiscard]] Task* createParseTask(Resource&) override; RESOURCE_HELPERS(TexturePack) }; PrismLauncher-11.0.3/launcher/minecraft/mod/ShaderPackFolderModel.h0000644000175100017510000000233515224505336024611 0ustar runnerrunner#pragma once #include "ResourceFolderModel.h" #include "minecraft/mod/ShaderPack.h" #include "minecraft/mod/tasks/LocalShaderPackParseTask.h" class ShaderPackFolderModel : public ResourceFolderModel { Q_OBJECT public: explicit ShaderPackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr) : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) {} virtual QString id() const override { return "shaderpacks"; } [[nodiscard]] Resource* createResource(const QFileInfo& info) override { return new ShaderPack(info); } [[nodiscard]] Task* createParseTask(Resource& resource) override { return new LocalShaderPackParseTask(m_next_resolution_ticket, static_cast(resource)); } QDir indexDir() const override { return m_dir; } Task* createPreUpdateTask() override; // avoid watching twice virtual bool startWatching() override { return ResourceFolderModel::startWatching({ m_dir.absolutePath() }); } virtual bool stopWatching() override { return ResourceFolderModel::stopWatching({ m_dir.absolutePath() }); } RESOURCE_HELPERS(ShaderPack); private: QMutex m_migrateLock; }; PrismLauncher-11.0.3/launcher/minecraft/mod/ResourceFolderModel.cpp0000644000175100017510000007627215224505336024741 0ustar runnerrunner#include "ResourceFolderModel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "Application.h" #include "FileSystem.h" #include "minecraft/mod/tasks/ResourceFolderLoadTask.h" #include "Json.h" #include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" #include "settings/Setting.h" #include "tasks/SequentialTask.h" #include "tasks/Task.h" #include "ui/dialogs/CustomMessageBox.h" ResourceFolderModel::ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this), m_is_indexed(is_indexed) { if (create_dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); } m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); connect(&m_resourceResolver, &ConcurrentTask::finished, this, [this] { m_resourceResolver.clear(); m_resourceResolverRunning = false; }); if (APPLICATION_DYN) { // in tests the application macro doesn't work m_resourceResolver.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); } } ResourceFolderModel::~ResourceFolderModel() { while (!QThreadPool::globalInstance()->waitForDone(100)) QCoreApplication::processEvents(); } bool ResourceFolderModel::startWatching(const QStringList& paths) { // Remove orphaned metadata next time m_first_folder_load = true; if (m_is_watching) return false; auto couldnt_be_watched = m_watcher.addPaths(paths); for (auto path : paths) { if (couldnt_be_watched.contains(path)) qDebug() << "Failed to start watching" << path; else qDebug() << "Started watching" << path; } update(); m_is_watching = !m_is_watching; return m_is_watching; } bool ResourceFolderModel::stopWatching(const QStringList& paths) { if (!m_is_watching) return false; auto couldnt_be_stopped = m_watcher.removePaths(paths); for (auto path : paths) { if (couldnt_be_stopped.contains(path)) qDebug() << "Failed to stop watching" << path; else qDebug() << "Stopped watching" << path; } m_is_watching = !m_is_watching; return !m_is_watching; } bool ResourceFolderModel::installResource(QString original_path) { // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName original_path = FS::NormalizePath(original_path); QFileInfo file_info(original_path); if (!file_info.exists() || !file_info.isReadable()) { qWarning() << "Caught attempt to install non-existing file or file-like object:" << original_path; return false; } qDebug() << "Installing:" << file_info.absoluteFilePath(); Resource resource(file_info); if (!resource.valid()) { qWarning() << original_path << "is not a valid resource. Ignoring it."; return false; } auto new_path = FS::NormalizePath(m_dir.filePath(file_info.fileName())); if (original_path == new_path) { qWarning() << "Overwriting the mod (" << original_path << ") with itself makes no sense..."; return false; } switch (resource.type()) { case ResourceType::SINGLEFILE: case ResourceType::ZIPFILE: case ResourceType::LITEMOD: { if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) { if (!FS::deletePath(new_path)) { qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!"; return false; } qDebug() << new_path << "has been deleted."; } if (!QFile::copy(original_path, new_path)) { qCritical() << "Copy from" << original_path << "to" << new_path << "has failed."; return false; } FS::updateTimestamp(new_path); QFileInfo new_path_file_info(new_path); resource.setFile(new_path_file_info); if (!m_is_watching) return update(); return true; } case ResourceType::FOLDER: { if (QFile::exists(new_path)) { qDebug() << "Ignoring folder '" << original_path << "', it would merge with" << new_path; return false; } if (!FS::copy(original_path, new_path)()) { qWarning() << "Copy of folder from" << original_path << "to" << new_path << "has (potentially partially) failed."; return false; } QFileInfo newpathInfo(new_path); resource.setFile(newpathInfo); if (!m_is_watching) return update(); return true; } default: break; } return false; } void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers) { auto install = [this, path] { installResource(std::move(path)); }; if (vers.addonId.isValid()) { ModPlatform::IndexedPack pack{ vers.addonId, ModPlatform::ResourceProvider::FLAME, }; auto [job, response] = FlameAPI().getProject(vers.addonId.toString()); connect(job.get(), &Task::failed, this, install); connect(job.get(), &Task::aborted, this, install); connect(job.get(), &Task::succeeded, [response, this, &vers, install, &pack] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response for mod info at" << parse_error.offset << "reason:" << parse_error.errorString(); qDebug() << *response; return; } try { auto obj = Json::requireObject(Json::requireObject(doc), "data"); FlameMod::loadIndexedPack(pack, obj); } catch (const JSONValidationError& e) { qDebug() << doc; qWarning() << "Error while reading mod info:" << e.cause(); } LocalResourceUpdateTask update_metadata(indexDir(), pack, vers); connect(&update_metadata, &Task::finished, this, install); update_metadata.start(); }); job->start(); } else { install(); } } bool ResourceFolderModel::uninstallResource(const QString& file_name, bool preserve_metadata) { for (auto& resource : m_resources) { auto resourceFileInfo = resource->fileinfo(); auto resourceFileName = resource->fileinfo().fileName(); if (!resource->enabled() && resourceFileName.endsWith(".disabled")) { resourceFileName.chop(9); } if (resourceFileName == file_name) { auto res = resource->destroy(indexDir(), preserve_metadata, false); update(); return res; } } return false; } bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) { if (indexes.isEmpty()) return true; for (auto i : indexes) { if (i.column() != 0) continue; auto& resource = m_resources.at(i.row()); resource->destroy(indexDir()); } update(); return true; } void ResourceFolderModel::deleteMetadata(const QModelIndexList& indexes) { if (indexes.isEmpty()) return; for (auto i : indexes) { if (i.column() != 0) continue; auto& resource = m_resources.at(i.row()); resource->destroyMetadata(indexDir()); } update(); } bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) { if (m_instance != nullptr && m_instance->isRunning()) { auto response = CustomMessageBox::selectable(nullptr, tr("Confirm toggle"), tr("If you enable/disable this resource while the game is running it may crash your game.\n" "Are you sure you want to do this?"), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return false; } if (indexes.isEmpty()) return true; bool succeeded = true; for (auto const& idx : indexes) { if (!validateIndex(idx) || idx.column() != 0) continue; int row = idx.row(); auto& resource = m_resources[row]; // Preserve the row, but change its ID auto old_id = resource->internal_id(); if (!resource->enable(action)) { succeeded = false; continue; } auto new_id = resource->internal_id(); m_resources_index.remove(old_id); m_resources_index[new_id] = row; emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } return succeeded; } static QMutex s_update_task_mutex; bool ResourceFolderModel::update() { // We hold a lock here to prevent race conditions on the m_current_update_task reset. QMutexLocker lock(&s_update_task_mutex); // Already updating, so we schedule a future update and return. if (m_current_update_task) { m_scheduled_update = true; return false; } m_current_update_task.reset(createUpdateTask()); if (!m_current_update_task) return false; connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, Qt::ConnectionType::QueuedConnection); connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); connect( m_current_update_task.get(), &Task::finished, this, [this] { m_current_update_task.reset(); if (m_scheduled_update) { m_scheduled_update = false; update(); } else { emit updateFinished(); } }, Qt::ConnectionType::QueuedConnection); Task::Ptr preUpdate{ createPreUpdateTask() }; if (preUpdate != nullptr) { auto task = new SequentialTask("ResourceFolderModel::update"); task->addTask(preUpdate); task->addTask(m_current_update_task); connect(task, &Task::finished, [task] { task->deleteLater(); }); QThreadPool::globalInstance()->start(task); } else { QThreadPool::globalInstance()->start(m_current_update_task.get()); } return true; } void ResourceFolderModel::resolveResource(Resource::Ptr res) { if (!res->shouldResolve()) { return; } Task::Ptr task{ createParseTask(*res) }; if (!task) return; int ticket = m_next_resolution_ticket.fetch_add(1); res->setResolving(true, ticket); m_active_parse_tasks.insert(ticket, task); connect( task.get(), &Task::succeeded, this, [this, ticket, res] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); connect( task.get(), &Task::failed, this, [this, ticket, res] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); connect( task.get(), &Task::finished, this, [this, ticket] { m_active_parse_tasks.remove(ticket); emit parseFinished(); }, Qt::ConnectionType::QueuedConnection); m_resourceResolver.addTask(task); if (!m_resourceResolverRunning) { QThreadPool::globalInstance()->start(&m_resourceResolver); m_resourceResolverRunning = true; } } void ResourceFolderModel::onUpdateSucceeded() { auto update_results = static_cast(m_current_update_task.get())->result(); auto& new_resources = update_results->resources; auto current_list = m_resources_index.keys(); QSet current_set(current_list.begin(), current_list.end()); auto new_list = new_resources.keys(); QSet new_set(new_list.begin(), new_list.end()); applyUpdates(current_set, new_set, new_resources); } void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) { auto iter = m_active_parse_tasks.constFind(ticket); if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) return; int row = m_resources_index[resource_id]; emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } Task* ResourceFolderModel::createUpdateTask() { auto index_dir = indexDir(); auto task = new ResourceFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load, [this](const QFileInfo& file) { return createResource(file); }); m_first_folder_load = false; return task; } bool ResourceFolderModel::hasPendingParseTasks() const { return !m_active_parse_tasks.isEmpty(); } void ResourceFolderModel::directoryChanged(QString path) { update(); } Qt::DropActions ResourceFolderModel::supportedDropActions() const { // copy from outside, move from within and other resource lists return Qt::CopyAction | Qt::MoveAction; } Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const { Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); auto flags = defaultFlags | Qt::ItemIsDropEnabled; if (index.isValid()) flags |= Qt::ItemIsUserCheckable; return flags; } QStringList ResourceFolderModel::mimeTypes() const { QStringList types; types << "text/uri-list"; return types; } bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) { if (action == Qt::IgnoreAction) { return true; } // check if the action is supported if (!data || !(action & supportedDropActions())) { return false; } // files dropped from outside? if (data->hasUrls()) { auto urls = data->urls(); for (auto url : urls) { // only local files may be dropped... if (!url.isLocalFile()) { continue; } // TODO: implement not only copy, but also move // FIXME: handle errors here installResource(url.toLocalFile()); } return true; } return false; } bool ResourceFolderModel::validateIndex(const QModelIndex& index) const { if (!index.isValid()) return false; int row = index.row(); if (row < 0 || row >= m_resources.size()) return false; return true; } // HACK: all subclasses need to call this to have the whole row painted // and they only delegate to the superclass for compatible columns QBrush ResourceFolderModel::rowBackground(int row) const { if (APPLICATION->settings()->get("ShowModIncompat").toBool() && m_resources[row]->hasIssues()) { return { QColor(255, 0, 0, 40) }; } else { return {}; } } QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const { if (!validateIndex(index)) return {}; int row = index.row(); int column = index.column(); switch (role) { case Qt::BackgroundRole: return rowBackground(row); case Qt::DisplayRole: switch (column) { case NameColumn: return m_resources[row]->name(); case DateColumn: return m_resources[row]->dateTimeChanged(); case ProviderColumn: return m_resources[row]->provider(); case SizeColumn: return m_resources[row]->sizeStr(); default: return {}; } case Qt::ToolTipRole: { QString tooltip = m_resources[row]->internal_id(); if (column == NameColumn) { if (APPLICATION->settings()->get("ShowModIncompat").toBool()) { for (const QString& issue : at(row).issues()) { tooltip += "\n" + issue; } } if (at(row).isSymLinkUnder(instDirPath())) { tooltip += m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") .arg(at(row).fileinfo().canonicalFilePath()); } if (at(row).isMoreThanOneHardLink()) { tooltip += tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } } return tooltip; } case Qt::DecorationRole: { if (column == NameColumn) { if (APPLICATION->settings()->get("ShowModIncompat").toBool() && at(row).hasIssues()) { return QIcon::fromTheme("status-bad"); } else if (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()) { return QIcon::fromTheme("status-yellow"); } } return {}; } case Qt::CheckStateRole: if (column == ActiveColumn) return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; return {}; default: return {}; } } bool ResourceFolderModel::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) { int row = index.row(); if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) return false; if (role == Qt::CheckStateRole) { return setResourceEnabled({ index }, EnableAction::TOGGLE); } return false; } QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case ActiveColumn: case NameColumn: case DateColumn: case ProviderColumn: case SizeColumn: return columnNames().at(section); default: return {}; } case Qt::ToolTipRole: { //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. switch (section) { case ActiveColumn: return tr("Is the resource enabled?"); case NameColumn: return tr("The name of the resource."); case DateColumn: return tr("The date and time this resource was last changed (or added)."); case ProviderColumn: return tr("The source provider of the resource."); case SizeColumn: return tr("The size of the resource."); default: return {}; } } default: break; } return {}; } void ResourceFolderModel::setupHeaderAction(QAction* act, int column) { Q_ASSERT(act); act->setText(columnNames().at(column)); } void ResourceFolderModel::saveColumns(QTreeView* tree) { auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); auto stateSetting = m_instance->settings()->getSetting(stateSettingName); stateSetting->set(QString::fromUtf8(tree->header()->saveState().toBase64())); // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false auto settings = m_instance->settings(); if (!settings->get(overrideSettingName).toBool()) { settings = APPLICATION->settings(); } auto visibility = Json::toMap(settings->get(visibilitySettingName).toString()); for (auto i = 0; i < m_column_names.size(); ++i) { if (m_columnsHideable[i]) { auto name = m_column_names[i]; visibility[name] = !tree->isColumnHidden(i); } } settings->set(visibilitySettingName, Json::fromMap(visibility)); } void ResourceFolderModel::loadColumns(QTreeView* tree) { auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); auto stateSetting = m_instance->settings()->getOrRegisterSetting(stateSettingName, ""); tree->header()->restoreState(QByteArray::fromBase64(stateSetting->get().toString().toUtf8())); auto setVisible = [this, tree](QVariant value) { auto visibility = Json::toMap(value.toString()); for (auto i = 0; i < m_column_names.size(); ++i) { if (m_columnsHideable[i]) { auto name = m_column_names[i]; tree->setColumnHidden(i, !visibility.value(name, false).toBool()); } } }; auto const defaultValue = Json::fromMap({ { "Image", true }, { "Version", true }, { "Last Modified", true }, { "Provider", true }, { "Pack Format", true }, }); // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false auto settings = m_instance->settings(); if (!settings->getOrRegisterSetting(overrideSettingName, false)->get().toBool()) { settings = APPLICATION->settings(); } auto visibility = settings->getOrRegisterSetting(visibilitySettingName, defaultValue); setVisible(visibility->get()); // allways connect the signal in case the setting is toggled on and off auto gSetting = APPLICATION->settings()->getOrRegisterSetting(visibilitySettingName, defaultValue); connect(gSetting.get(), &Setting::SettingChanged, tree, [this, setVisible, overrideSettingName](const Setting&, QVariant value) { if (!m_instance->settings()->get(overrideSettingName).toBool()) { setVisible(value); } }); } QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) { auto menu = new QMenu(tree); { // action to decide if the visibility is per instance or not auto act = new QAction(tr("Override Columns Visibility"), menu); auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); act->setCheckable(true); act->setChecked(m_instance->settings()->getOrRegisterSetting(overrideSettingName, false)->get().toBool()); connect(act, &QAction::toggled, tree, [this, tree, overrideSettingName](bool toggled) { m_instance->settings()->set(overrideSettingName, toggled); saveColumns(tree); }); menu->addAction(act); } menu->addSeparator()->setText(tr("Show / Hide Columns")); for (int col = 0; col < columnCount(); ++col) { // Skip creating actions for columns that should not be hidden if (!m_columnsHideable.at(col)) continue; auto act = new QAction(menu); setupHeaderAction(act, col); act->setCheckable(true); act->setChecked(!tree->isColumnHidden(col)); connect(act, &QAction::toggled, tree, [this, col, tree](bool toggled) { tree->setColumnHidden(col, !toggled); for (int c = 0; c < columnCount(); ++c) { if (m_column_resize_modes.at(c) == QHeaderView::ResizeToContents) tree->resizeColumnToContents(c); } saveColumns(tree); }); menu->addAction(act); } return menu; } QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent) { return new ProxyModel(parent); } SortType ResourceFolderModel::columnToSortKey(size_t column) const { Q_ASSERT(m_column_sort_keys.size() == columnCount()); return m_column_sort_keys.at(column); } /* Standard Proxy Model for createFilterProxyModel */ bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, [[maybe_unused]] const QModelIndex& source_parent) const { auto* model = qobject_cast(sourceModel()); if (!model) return true; const auto& resource = model->at(source_row); return resource.applyFilter(filterRegularExpression()); } bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const { auto* model = qobject_cast(sourceModel()); if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) { return QSortFilterProxyModel::lessThan(source_left, source_right); } // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and // proceed. auto column_sort_key = model->columnToSortKey(source_left.column()); auto const& resource_left = model->at(source_left.row()); auto const& resource_right = model->at(source_right.row()); auto compare_result = resource_left.compare(resource_right, column_sort_key); if (compare_result == 0) return QSortFilterProxyModel::lessThan(source_left, source_right); return compare_result < 0; } QString ResourceFolderModel::instDirPath() const { return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); } void ResourceFolderModel::onParseFailed(int ticket, QString resource_id) { auto iter = m_active_parse_tasks.constFind(ticket); if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) return; auto removed_index = m_resources_index[resource_id]; auto removed_it = m_resources.begin() + removed_index; Q_ASSERT(removed_it != m_resources.end()); beginRemoveRows(QModelIndex(), removed_index, removed_index); m_resources.erase(removed_it); // update index m_resources_index.clear(); int idx = 0; for (auto const& mod : qAsConst(m_resources)) { m_resources_index[mod->internal_id()] = idx; idx++; } endRemoveRows(); } void ResourceFolderModel::applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources) { // see if the kept resources changed in some way { QSet kept_set = current_set; kept_set.intersect(new_set); for (auto const& kept : kept_set) { auto row_it = m_resources_index.constFind(kept); Q_ASSERT(row_it != m_resources_index.constEnd()); auto row = row_it.value(); auto& new_resource = new_resources[kept]; auto const& current_resource = m_resources.at(row); if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { // no significant change bool hadIssues = !current_resource->hasIssues(); current_resource->updateIssues(m_instance); if (hadIssues != current_resource->hasIssues()) { emit dataChanged(index(row, 0), index(row, columnCount({}) - 1)); } continue; } // If the resource is resolving, but something about it changed, we don't want to // continue the resolving. if (current_resource->isResolving()) { auto ticket = current_resource->resolutionTicket(); if (m_active_parse_tasks.contains(ticket)) { auto task = (*m_active_parse_tasks.find(ticket)).get(); task->abort(); } } m_resources[row].reset(new_resource); new_resource->updateIssues(m_instance); resolveResource(m_resources.at(row)); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } } // remove resources no longer present { QSet removed_set = current_set; removed_set.subtract(new_set); QList removed_rows; for (auto& removed : removed_set) removed_rows.append(m_resources_index[removed]); std::sort(removed_rows.begin(), removed_rows.end(), std::greater()); for (auto& removed_index : removed_rows) { auto removed_it = m_resources.begin() + removed_index; Q_ASSERT(removed_it != m_resources.end()); if ((*removed_it)->isResolving()) { auto ticket = (*removed_it)->resolutionTicket(); if (m_active_parse_tasks.contains(ticket)) { auto task = (*m_active_parse_tasks.find(ticket)).get(); task->abort(); } } beginRemoveRows(QModelIndex(), removed_index, removed_index); m_resources.erase(removed_it); endRemoveRows(); } } // add new resources to the end { QSet added_set = new_set; added_set.subtract(current_set); // When you have a Qt build with assertions turned on, proceeding here will abort the application if (added_set.size() > 0) { beginInsertRows(QModelIndex(), static_cast(m_resources.size()), static_cast(m_resources.size() + added_set.size() - 1)); for (auto& added : added_set) { auto res = new_resources[added]; res->updateIssues(m_instance); m_resources.append(res); resolveResource(m_resources.last()); } endInsertRows(); } } // update index { m_resources_index.clear(); int idx = 0; for (auto const& mod : qAsConst(m_resources)) { m_resources_index[mod->internal_id()] = idx; idx++; } } } Resource::Ptr ResourceFolderModel::find(QString id) { auto iter = std::find_if(m_resources.constBegin(), m_resources.constEnd(), [&](Resource::Ptr const& r) { return r->internal_id() == id; }); if (iter == m_resources.constEnd()) return nullptr; return *iter; } QList ResourceFolderModel::allResources() { QList result; result.reserve(m_resources.size()); for (const Resource ::Ptr& resource : m_resources) result.append((resource.get())); return result; } QList ResourceFolderModel::selectedResources(const QModelIndexList& indexes) { QList result; for (const QModelIndex& index : indexes) { if (index.column() != 0) continue; result.append(&at(index.row())); } return result; } PrismLauncher-11.0.3/launcher/minecraft/mod/DataPackFolderModel.cpp0000644000175100017510000001445415224505336024614 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "DataPackFolderModel.h" #include #include #include "Version.h" #include "minecraft/mod/tasks/LocalDataPackParseTask.h" DataPackFolderModel::DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive }; m_columnsHideable = { false, true, false, true, true }; } QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const { if (!validateIndex(index)) return {}; int row = index.row(); int column = index.column(); switch (role) { case Qt::BackgroundRole: return rowBackground(row); case Qt::DisplayRole: switch (column) { case PackFormatColumn: { const auto& resource = at(row); return resource.packFormatStr(); } } break; case Qt::DecorationRole: { if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } break; } case Qt::ToolTipRole: { if (column == PackFormatColumn) { //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); } break; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } break; } // map the columns to the base equivilents QModelIndex mappedIndex; switch (column) { case ActiveColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); break; case NameColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); break; case DateColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); break; case ProviderColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); break; // FIXME: there is no size column due to an oversight } if (mappedIndex.isValid()) { return ResourceFolderModel::data(mappedIndex, role); } return {}; } QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case ActiveColumn: case NameColumn: case PackFormatColumn: case DateColumn: case ImageColumn: return columnNames().at(section); default: return {}; } case Qt::ToolTipRole: switch (section) { case ActiveColumn: return tr("Is the data pack enabled? (Only valid for ZIPs)"); case NameColumn: return tr("The name of the data pack."); case PackFormatColumn: //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); case DateColumn: return tr("The date and time this data pack was last changed (or added)."); default: return {}; } case Qt::SizeHintRole: if (section == ImageColumn) { return QSize(64, 0); } return {}; default: return {}; } } int DataPackFolderModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : NUM_COLUMNS; } Resource* DataPackFolderModel::createResource(const QFileInfo& file) { return new DataPack(file); } Task* DataPackFolderModel::createParseTask(Resource& resource) { return new LocalDataPackParseTask(m_next_resolution_ticket, static_cast(&resource)); } PrismLauncher-11.0.3/launcher/minecraft/mod/ShaderPack.cpp0000644000175100017510000000211615224505336023024 0ustar runnerrunner // SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ShaderPack.h" void ShaderPack::setPackFormat(ShaderPackFormat new_format) { QMutexLocker locker(&m_data_lock); m_pack_format = new_format; } bool ShaderPack::valid() const { return m_pack_format != ShaderPackFormat::INVALID; } PrismLauncher-11.0.3/launcher/minecraft/mod/ResourcePack.cpp0000644000175100017510000001314715224505336023413 0ustar runnerrunner#include "ResourcePack.h" #include #include #include #include #include "MTPixmapCache.h" #include "Version.h" // Values taken from: // https://minecraft.wiki/w/Pack_format#List_of_resource_pack_formats static const QMap, std::pair> s_pack_format_versions = { { { 1, 0 }, { Version("1.6.1"), Version("1.8.9") } }, { { 2, 0 }, { Version("1.9"), Version("1.10.2") } }, { { 3, 0 }, { Version("1.11"), Version("1.12.2") } }, { { 4, 0 }, { Version("1.13"), Version("1.14.4") } }, { { 5, 0 }, { Version("1.15"), Version("1.16.1") } }, { { 6, 0 }, { Version("1.16.2"), Version("1.16.5") } }, { { 7, 0 }, { Version("1.17"), Version("1.17.1") } }, { { 8, 0 }, { Version("1.18"), Version("1.18.2") } }, { { 9, 0 }, { Version("1.19"), Version("1.19.2") } }, { { 11, 0 }, { Version("22w42a"), Version("22w44a") } }, { { 12, 0 }, { Version("1.19.3"), Version("1.19.3") } }, { { 13, 0 }, { Version("1.19.4"), Version("1.19.4") } }, { { 14, 0 }, { Version("23w14a"), Version("23w16a") } }, { { 15, 0 }, { Version("1.20"), Version("1.20.1") } }, { { 16, 0 }, { Version("23w31a"), Version("23w31a") } }, { { 17, 0 }, { Version("23w32a"), Version("1.20.2-pre1") } }, { { 18, 0 }, { Version("1.20.2"), Version("1.20.2") } }, { { 19, 0 }, { Version("23w42a"), Version("23w42a") } }, { { 20, 0 }, { Version("23w43a"), Version("23w44a") } }, { { 21, 0 }, { Version("23w45a"), Version("23w46a") } }, { { 22, 0 }, { Version("1.20.3"), Version("1.20.4") } }, { { 24, 0 }, { Version("24w03a"), Version("24w04a") } }, { { 25, 0 }, { Version("24w05a"), Version("24w05b") } }, { { 26, 0 }, { Version("24w06a"), Version("24w07a") } }, { { 28, 0 }, { Version("24w09a"), Version("24w10a") } }, { { 29, 0 }, { Version("24w11a"), Version("24w11a") } }, { { 30, 0 }, { Version("24w12a"), Version("24w12a") } }, { { 31, 0 }, { Version("24w13a"), Version("1.20.5-pre3") } }, { { 32, 0 }, { Version("1.20.5"), Version("1.20.6") } }, { { 33, 0 }, { Version("24w18a"), Version("24w20a") } }, { { 34, 0 }, { Version("1.21"), Version("1.21.1") } }, { { 35, 0 }, { Version("24w33a"), Version("24w33a") } }, { { 36, 0 }, { Version("24w34a"), Version("24w35a") } }, { { 37, 0 }, { Version("24w36a"), Version("24w36a") } }, { { 38, 0 }, { Version("24w37a"), Version("24w37a") } }, { { 39, 0 }, { Version("24w38a"), Version("24w39a") } }, { { 40, 0 }, { Version("24w40a"), Version("24w40a") } }, { { 41, 0 }, { Version("1.21.2-pre1"), Version("1.21.2-pre2") } }, { { 42, 0 }, { Version("1.21.2"), Version("1.21.3") } }, { { 43, 0 }, { Version("24w44a"), Version("24w44a") } }, { { 44, 0 }, { Version("24w45a"), Version("24w45a") } }, { { 45, 0 }, { Version("24w46a"), Version("24w46a") } }, { { 46, 0 }, { Version("1.21.4"), Version("1.21.4") } }, { { 47, 0 }, { Version("25w02a"), Version("25w02a") } }, { { 48, 0 }, { Version("25w03a"), Version("25w03a") } }, { { 49, 0 }, { Version("25w04a"), Version("25w04a") } }, { { 50, 0 }, { Version("25w05a"), Version("25w05a") } }, { { 51, 0 }, { Version("25w06a"), Version("25w06a") } }, { { 52, 0 }, { Version("25w07a"), Version("25w07a") } }, { { 53, 0 }, { Version("25w08a"), Version("25w09b") } }, { { 54, 0 }, { Version("25w10a"), Version("25w10a") } }, { { 55, 0 }, { Version("1.21.5"), Version("1.21.5") } }, { { 56, 0 }, { Version("25w15a"), Version("25w15a") } }, { { 57, 0 }, { Version("25w16a"), Version("25w16a") } }, { { 58, 0 }, { Version("25w17a"), Version("25w17a") } }, { { 59, 0 }, { Version("25w18a"), Version("25w18a") } }, { { 60, 0 }, { Version("25w19a"), Version("25w19a") } }, { { 61, 0 }, { Version("25w20a"), Version("25w20a") } }, { { 62, 0 }, { Version("25w21a"), Version("25w21a") } }, { { 63, 0 }, { Version("1.21.6"), Version("1.21.6") } }, { { 64, 0 }, { Version("1.21.7"), Version("1.21.8") } }, { { 65, 0 }, { Version("25w31a"), Version("25w31a") } }, { { 65, 1 }, { Version("25w32a"), Version("25w32a") } }, { { 65, 2 }, { Version("25w33a"), Version("25w33a") } }, { { 66, 0 }, { Version("25w34a"), Version("25w34b") } }, { { 67, 0 }, { Version("25w35a"), Version("25w35a") } }, { { 68, 0 }, { Version("25w36a"), Version("25w36b") } }, { { 69, 0 }, { Version("1.21.9"), Version("1.21.10") } }, { { 70, 0 }, { Version("25w41a"), Version("25w41a") } }, { { 70, 1 }, { Version("25w42a"), Version("25w42a") } }, { { 71, 0 }, { Version("25w43a"), Version("25w43a") } }, { { 72, 0 }, { Version("25w44a"), Version("25w44a") } }, { { 73, 0 }, { Version("25w45a"), Version("25w45a") } }, { { 74, 0 }, { Version("25w46a"), Version("25w46a") } }, { { 75, 0 }, { Version("1.21.11"), Version("1.21.11") } }, { { 76, 0 }, { Version("26.1-snap1"), Version("26.1-snap1") } }, { { 77, 0 }, { Version("26.1-snap2"), Version("26.1-snap2") } }, { { 78, 0 }, { Version("26.1-snap3"), Version("26.1-snap3") } }, { { 78, 1 }, { Version("26.1-snap4"), Version("26.1-snap4") } }, { { 79, 0 }, { Version("26.1-snap5"), Version("26.1-snap5") } }, { { 80, 0 }, { Version("26.1-snap6"), Version("26.1-snap6") } }, { { 81, 0 }, { Version("26.1-snap7"), Version("26.1-snap7") } }, { { 81, 1 }, { Version("26.1-snap8"), Version("26.1-snap9") } }, { { 82, 0 }, { Version("26.1-snap10"), Version("26.1-snap10") } }, { { 83, 0 }, { Version("26.1-snap11"), Version("26.1-snap11") } }, }; QMap, std::pair> ResourcePack::mappings() const { return s_pack_format_versions; } PrismLauncher-11.0.3/launcher/minecraft/mod/ShaderPackFolderModel.cpp0000644000175100017510000000303415224505336025141 0ustar runnerrunner#include "ShaderPackFolderModel.h" #include "FileSystem.h" namespace { class ShaderPackIndexMigrateTask : public Task { Q_OBJECT public: ShaderPackIndexMigrateTask(QDir resourceDir, QDir indexDir) : m_resourceDir(std::move(resourceDir)), m_indexDir(std::move(indexDir)) {} void executeTask() override { if (!m_indexDir.exists()) { qDebug() << m_indexDir.absolutePath() << "does not exist; nothing to migrate"; emitSucceeded(); return; } QStringList pwFiles = m_indexDir.entryList({ "*.pw.toml" }, QDir::Files); bool movedAll = true; for (const auto& file : pwFiles) { QString src = m_indexDir.filePath(file); QString dest = m_resourceDir.filePath(file); if (FS::move(src, dest)) { qDebug() << "Moved" << src << "to" << dest; } else { movedAll = false; } } if (!movedAll) { // FIXME: not shown in the UI emitFailed(tr("Failed to migrate shaderpack metadata from .index")); return; } if (!FS::deletePath(m_indexDir.absolutePath())) { emitFailed(tr("Failed to remove old .index dir")); return; } emitSucceeded(); } private: QDir m_resourceDir, m_indexDir; }; } // namespace Task* ShaderPackFolderModel::createPreUpdateTask() { return new ShaderPackIndexMigrateTask(m_dir, ResourceFolderModel::indexDir()); } #include "ShaderPackFolderModel.moc" PrismLauncher-11.0.3/launcher/minecraft/mod/Resource.cpp0000644000175100017510000002131415224505336022607 0ustar runnerrunner#include "Resource.h" #include #include #include #include #include "FileSystem.h" #include "StringUtils.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" Resource::Resource(QObject* parent) : QObject(parent) {} Resource::Resource(QFileInfo file_info) : QObject() { setFile(file_info); } void Resource::setFile(QFileInfo file_info) { m_file_info = file_info; parseFile(); } static std::tuple calculateFileSize(const QFileInfo& file) { if (file.isDir()) { auto dir = QDir(file.absoluteFilePath()); dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); auto count = dir.count(); auto str = QObject::tr("item"); if (count != 1) str = QObject::tr("items"); return { QString("%1 %2").arg(QString::number(count), str), count }; } return { StringUtils::humanReadableFileSize(file.size(), true), file.size() }; } void Resource::parseFile() { QString file_name{ m_file_info.fileName() }; m_type = ResourceType::UNKNOWN; m_internal_id = file_name; std::tie(m_size_str, m_size_info) = calculateFileSize(m_file_info); if (m_file_info.isDir()) { m_type = ResourceType::FOLDER; m_name = file_name; } else if (m_file_info.isFile()) { if (file_name.endsWith(".disabled")) { file_name.chop(9); m_enabled = false; } if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) { m_type = ResourceType::ZIPFILE; file_name.chop(4); } else if (file_name.endsWith(".nilmod")) { m_type = ResourceType::ZIPFILE; file_name.chop(7); } else if (file_name.endsWith(".litemod")) { m_type = ResourceType::LITEMOD; file_name.chop(8); } else { m_type = ResourceType::SINGLEFILE; } m_name = file_name; } m_changed_date_time = m_file_info.lastModified(); } auto Resource::name() const -> QString { if (metadata()) return metadata()->name; return m_name; } static void removeThePrefix(QString& string) { static const QRegularExpression s_regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption); string.remove(s_regex); string = string.trimmed(); } auto Resource::provider() const -> QString { if (metadata()) return ModPlatform::ProviderCapabilities::readableName(metadata()->provider); return tr("Unknown"); } auto Resource::homepage() const -> QString { if (metadata()) return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id); return {}; } void Resource::setMetadata(std::shared_ptr&& metadata) { if (status() == ResourceStatus::NO_METADATA) setStatus(ResourceStatus::INSTALLED); m_metadata = metadata; } QStringList Resource::issues() const { QStringList result; result.reserve(m_issues.length()); for (const char* issue : m_issues) { result.append(tr(issue)); } return result; } void Resource::updateIssues(const BaseInstance* inst) { m_issues.clear(); if (m_metadata == nullptr) { return; } auto mcInst = dynamic_cast(inst); if (mcInst == nullptr) { return; } auto profile = mcInst->getPackProfile(); QString mcVersion = profile->getComponentVersion("net.minecraft"); if (!m_metadata->mcVersions.empty() && !m_metadata->mcVersions.contains(mcVersion)) { // delay translation until issues() is called m_issues.append(QT_TR_NOOP("Not marked as compatible with the instance's game version.")); } } int Resource::compare(const Resource& other, SortType type) const { switch (type) { default: case SortType::ENABLED: if (enabled() && !other.enabled()) return 1; if (!enabled() && other.enabled()) return -1; break; case SortType::NAME: { QString this_name{ name() }; QString other_name{ other.name() }; // TODO do we need this? it could result in 0 being returned removeThePrefix(this_name); removeThePrefix(other_name); return QString::compare(this_name, other_name, Qt::CaseInsensitive); } case SortType::DATE: if (dateTimeChanged() > other.dateTimeChanged()) return 1; if (dateTimeChanged() < other.dateTimeChanged()) return -1; break; case SortType::SIZE: { if (this->type() != other.type()) { if (this->type() == ResourceType::FOLDER) return -1; if (other.type() == ResourceType::FOLDER) return 1; } if (sizeInfo() > other.sizeInfo()) return 1; if (sizeInfo() < other.sizeInfo()) return -1; break; } case SortType::PROVIDER: { auto compare_result = QString::compare(provider(), other.provider(), Qt::CaseInsensitive); if (compare_result != 0) return compare_result; break; } } return 0; } bool Resource::applyFilter(QRegularExpression filter) const { return filter.match(name()).hasMatch(); } bool Resource::enable(EnableAction action) { if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER) return false; QString path = m_file_info.absoluteFilePath(); QFile file(path); bool enable = true; switch (action) { case EnableAction::ENABLE: enable = true; break; case EnableAction::DISABLE: enable = false; break; case EnableAction::TOGGLE: default: enable = !enabled(); break; } if (m_enabled == enable) return false; if (enable) { // m_enabled is false, but there's no '.disabled' suffix. // TODO: Report error? if (!path.endsWith(".disabled")) return false; path.chop(9); } else { path += ".disabled"; if (QFile::exists(path)) { path = FS::getUniqueResourceName(path); } } if (!file.rename(path)) return false; setFile(QFileInfo(path)); m_enabled = enable; return true; } auto Resource::destroy(const QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool { m_type = ResourceType::UNKNOWN; if (!preserve_metadata) { qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); destroyMetadata(index_dir); } return (attempt_trash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath()); } auto Resource::destroyMetadata(const QDir& index_dir) -> void { if (metadata()) { Metadata::remove(index_dir, metadata()->slug); } else { auto n = name(); Metadata::remove(index_dir, n); } m_metadata = nullptr; } bool Resource::isSymLinkUnder(const QString& instPath) const { if (isSymLink()) return true; auto instDir = QDir(instPath); auto relAbsPath = instDir.relativeFilePath(m_file_info.absoluteFilePath()); auto relCanonPath = instDir.relativeFilePath(m_file_info.canonicalFilePath()); return relAbsPath != relCanonPath; } bool Resource::isMoreThanOneHardLink() const { return FS::hardLinkCount(m_file_info.absoluteFilePath()) > 1; } auto Resource::getOriginalFileName() const -> QString { auto fileName = m_file_info.fileName(); if (!m_enabled) fileName.chop(9); return fileName; } QDebug operator<<(QDebug debug, ResourceType type) { switch (type) { case ResourceType::ZIPFILE: debug << "ZIPFILE"; break; case ResourceType::SINGLEFILE: debug << "SINGLEFILE"; break; case ResourceType::FOLDER: debug << "FOLDER"; break; case ResourceType::LITEMOD: debug << "LITEMOD"; break; case ResourceType::UNKNOWN: default: debug << "UNKNOWN"; break; }; return debug; } QDebug operator<<(QDebug debug, ResourceStatus status) { switch (status) { case ResourceStatus::INSTALLED: debug << "INSTALLED"; break; case ResourceStatus::NOT_INSTALLED: debug << "NOT_INSTALLED"; break; case ResourceStatus::NO_METADATA: debug << "NO_METADATA"; break; case ResourceStatus::UNKNOWN: default: debug << "UNKNOWN"; break; }; return debug; } PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/0000755000175100017510000000000015224505336021440 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.h0000644000175100017510000000266215224505336026347 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include "modplatform/ModIndex.h" #include "tasks/Task.h" class LocalResourceUpdateTask : public Task { Q_OBJECT public: using Ptr = shared_qobject_ptr; explicit LocalResourceUpdateTask(QDir index_dir, ModPlatform::IndexedPack& project, ModPlatform::IndexedVersion& version); auto canAbort() const -> bool override { return true; } auto abort() -> bool override; protected slots: //! Entry point for tasks. void executeTask() override; signals: void hasOldResource(QString name, QString filename); private: QDir m_index_dir; ModPlatform::IndexedPack m_project; ModPlatform::IndexedVersion m_version; }; PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h0000644000175100017510000000410715224505336026054 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "minecraft/mod/DataPack.h" #include "tasks/Task.h" namespace DataPackUtils { enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); bool processMCMeta(DataPack* pack, QByteArray&& raw_data); QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false); bool processPackPNG(const DataPack* pack, QByteArray&& raw_data); /// processes ONLY the pack.png (rest of the pack may be invalid) bool processPackPNG(const DataPack* pack); /** Checks whether a file is valid as a data pack or not. */ bool validate(QFileInfo file); /** Checks whether a file is valid as a resource pack or not. */ bool validateResourcePack(QFileInfo file); } // namespace DataPackUtils class LocalDataPackParseTask : public Task { Q_OBJECT public: LocalDataPackParseTask(int token, DataPack* dp); void executeTask() override; int token() const { return m_token; } private: int m_token; DataPack* m_data_pack; }; PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/GetModDependenciesTask.h0000644000175100017510000000651015224505336026124 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include #include #include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/ModFolderModel.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "tasks/SequentialTask.h" #include "tasks/Task.h" #include "ui/pages/modplatform/ModModel.h" class GetModDependenciesTask : public SequentialTask { Q_OBJECT public: using Ptr = shared_qobject_ptr; struct PackDependency { ModPlatform::Dependency dependency; ModPlatform::IndexedPack::Ptr pack; ModPlatform::IndexedVersion version; PackDependency() = default; PackDependency(ModPlatform::IndexedPack::Ptr p, ModPlatform::IndexedVersion v) : pack(std::move(p)), version(std::move(v)) {} }; struct PackDependencyExtraInfo { bool maybe_installed{}; QStringList required_by; }; explicit GetModDependenciesTask(BaseInstance* instance, ModFolderModel* folder, QList> selected); auto getDependecies() const -> QList> { return m_pack_dependencies; } QHash getExtraInfo(); private: ResourceAPI* getAPI(ModPlatform::ResourceProvider provider) { if (provider == ModPlatform::ResourceProvider::FLAME) { return &m_flameAPI; } return &m_modrinthAPI; } protected slots: Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, ModPlatform::ResourceProvider, int); QList getDependenciesForVersion(const ModPlatform::IndexedVersion&, ModPlatform::ResourceProvider providerName); void prepare(); Task::Ptr getProjectInfoTask(std::shared_ptr pDep); ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, ModPlatform::ResourceProvider providerName); void removePack(const QVariant& addonId); bool isLocalyInstalled(std::shared_ptr pDep); bool maybeInstalled(std::shared_ptr pDep); private: QList> m_pack_dependencies; QList> m_mods; QList> m_selected; QStringList m_mods_file_names; Version m_version; ModPlatform::ModLoaderTypes m_loaderType; ModrinthAPI m_modrinthAPI; FlameAPI m_flameAPI; }; PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h0000644000175100017510000000341115224505336026406 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "minecraft/mod/ShaderPack.h" #include "tasks/Task.h" namespace ShaderPackUtils { enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); /** Checks whether a file is valid as a shader pack or not. */ bool validate(QFileInfo file); } // namespace ShaderPackUtils class LocalShaderPackParseTask : public Task { Q_OBJECT public: LocalShaderPackParseTask(int token, ShaderPack& sp); bool canAbort() const override { return true; } bool abort() override; void executeTask() override; int token() const { return m_token; } private: int m_token; ShaderPack& m_shader_pack; bool m_aborted = false; }; PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h0000644000175100017510000000535415224505336026166 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include "minecraft/mod/Mod.h" #include "tasks/Task.h" class ResourceFolderLoadTask : public Task { Q_OBJECT public: struct Result { QMap resources; }; using ResultPtr = std::shared_ptr; ResultPtr result() const { return m_result; } public: ResourceFolderLoadTask(const QDir& resource_dir, const QDir& index_dir, bool is_indexed, bool clean_orphan, std::function create_function); bool canAbort() const override { return true; } bool abort() override { m_aborted.store(true); return true; } void executeTask() override; private: void getFromMetadata(); private: QDir m_resource_dir, m_index_dir; bool m_is_indexed; bool m_clean_orphan; std::function m_create_func; ResultPtr m_result; std::atomic m_aborted = false; /** This is the thread in which we should put new mod objects */ QThread* m_thread_to_spawn_into; }; PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp0000644000175100017510000000504715224505336026702 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "LocalResourceUpdateTask.h" #include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" #ifdef Q_OS_WIN32 #include #endif LocalResourceUpdateTask::LocalResourceUpdateTask(QDir index_dir, ModPlatform::IndexedPack& project, ModPlatform::IndexedVersion& version) : m_index_dir(index_dir), m_project(project), m_version(version) { // Ensure a '.index' folder exists in the mods folder, and create it if it does not if (!FS::ensureFolderPathExists(index_dir.path())) { emitFailed(QString("Unable to create index directory at %1!").arg(index_dir.absolutePath())); return; } #ifdef Q_OS_WIN32 std::wstring wpath = index_dir.path().toStdWString(); if (index_dir.dirName().startsWith('.')) { SetFileAttributesW(wpath.c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); } else { // fix shaderpacks folder being hidden by Prism Launcher 10.0.1 SetFileAttributesW(wpath.c_str(), FILE_ATTRIBUTE_NORMAL); } #endif } void LocalResourceUpdateTask::executeTask() { setStatus(tr("Updating index for resource:\n%1").arg(m_project.name)); auto old_metadata = Metadata::get(m_index_dir, m_project.addonId); if (old_metadata.isValid()) { emit hasOldResource(old_metadata.name, old_metadata.filename); if (m_project.slug.isEmpty()) m_project.slug = old_metadata.slug; } auto pw_mod = Metadata::create(m_index_dir, m_project, m_version); if (pw_mod.isValid()) { Metadata::update(m_index_dir, pw_mod); emitSucceeded(); } else { qCritical() << "Tried to update an invalid resource!"; emitFailed(tr("Invalid metadata")); } } auto LocalResourceUpdateTask::abort() -> bool { emitAborted(); return true; } PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp0000644000175100017510000003614015224505336026461 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "GetModDependenciesTask.h" #include #include #include #include "Json.h" #include "QObjectPtr.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/MetadataHandler.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "tasks/SequentialTask.h" #include "ui/pages/modplatform/ModModel.h" static Version mcVersion(BaseInstance* inst) { return static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion(); } static ModPlatform::ModLoaderTypes mcLoaders(BaseInstance* inst) { return static_cast(inst)->getPackProfile()->getSupportedModLoaders().value(); } static bool checkDependencies(std::shared_ptr sel, Version mcVersion, ModPlatform::ModLoaderTypes loaders) { return (sel->pack->versions.isEmpty() || sel->version.mcVersion.contains(mcVersion.toString())) && (!loaders || !sel->version.loaders || sel->version.loaders & loaders); } GetModDependenciesTask::GetModDependenciesTask(BaseInstance* instance, ModFolderModel* folder, QList> selected) : SequentialTask(tr("Get dependencies")), m_selected(selected), m_version(mcVersion(instance)), m_loaderType(mcLoaders(instance)) { for (auto mod : folder->allMods()) { m_mods_file_names << mod->fileinfo().fileName(); if (auto meta = mod->metadata(); meta) m_mods.append(meta); } prepare(); } void GetModDependenciesTask::prepare() { for (auto sel : m_selected) { if (checkDependencies(sel, m_version, m_loaderType)) for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) { addTask(prepareDependencyTask(dep, sel->pack->provider, 20)); } } } ModPlatform::Dependency GetModDependenciesTask::getOverride(const ModPlatform::Dependency& dep, const ModPlatform::ResourceProvider providerName) { if (auto isQuilt = (m_loaderType & ModPlatform::Quilt) != 0U; isQuilt || (m_loaderType & ModPlatform::Fabric) != 0U) { auto overide = ModPlatform::getOverrideDeps(); auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, providerName, isQuilt](const auto& o) { return o.provider == providerName && dep.addonId == (isQuilt ? o.fabric : o.quilt); }); if (over != overide.cend()) { return { .addonId = isQuilt ? over->quilt : over->fabric, .type = dep.type, .version = "" }; } } return dep; } QList GetModDependenciesTask::getDependenciesForVersion(const ModPlatform::IndexedVersion& version, const ModPlatform::ResourceProvider providerName) { QList c_dependencies; for (auto ver_dep : version.dependencies) { if (ver_dep.type != ModPlatform::DependencyType::REQUIRED) { continue; } ver_dep = getOverride(ver_dep, providerName); auto isOnlyVersion = providerName == ModPlatform::ResourceProvider::MODRINTH && ver_dep.addonId.toString().isEmpty(); if (auto dep = std::find_if(c_dependencies.begin(), c_dependencies.end(), [&ver_dep, isOnlyVersion](const ModPlatform::Dependency& i) { return isOnlyVersion ? i.version == ver_dep.version : i.addonId == ver_dep.addonId; }); dep != c_dependencies.end()) { continue; // check the current dependency list } if (auto dep = std::find_if(m_selected.begin(), m_selected.end(), [&ver_dep, providerName, isOnlyVersion](const std::shared_ptr& i) { return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.version : i->pack->addonId == ver_dep.addonId); }); dep != m_selected.end()) { continue; // check the selected versions } if (auto dep = std::find_if(m_mods.begin(), m_mods.end(), [&ver_dep, providerName, isOnlyVersion](const std::shared_ptr& i) { return i->provider == providerName && (isOnlyVersion ? i->file_id == ver_dep.version : i->project_id == ver_dep.addonId); }); dep != m_mods.end()) { continue; // check the existing mods } if (auto dep = std::find_if(m_pack_dependencies.begin(), m_pack_dependencies.end(), [&ver_dep, providerName, isOnlyVersion](const std::shared_ptr& i) { return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.addonId : i->pack->addonId == ver_dep.addonId); }); dep != m_pack_dependencies.end()) { // check loaded dependencies continue; } c_dependencies.append(ver_dep); } return c_dependencies; } Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr pDep) { auto provider = pDep->pack->provider; auto [info, responseInfo] = getAPI(provider)->getProject(pDep->pack->addonId.toString()); connect(info.get(), &NetJob::succeeded, [this, responseInfo, provider, pDep] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*responseInfo, &parse_error); if (parse_error.error != QJsonParseError::NoError) { removePack(pDep->pack->addonId); qWarning() << "Error while parsing JSON response for mod info at" << parse_error.offset << "reason:" << parse_error.errorString(); qDebug() << *responseInfo; return; } try { auto obj = provider == ModPlatform::ResourceProvider::FLAME ? Json::requireObject(Json::requireObject(doc), "data") : Json::requireObject(doc); getAPI(provider)->loadIndexedPack(*pDep->pack, obj); } catch (const JSONValidationError& e) { removePack(pDep->pack->addonId); qDebug() << doc; qWarning() << "Error while reading mod info:" << e.cause(); } }); return info; } Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Dependency& dep, const ModPlatform::ResourceProvider providerName, int level) { auto pDep = std::make_shared(); pDep->dependency = dep; pDep->pack = std::make_shared(); pDep->pack->addonId = dep.addonId; pDep->pack->provider = providerName; m_pack_dependencies.append(pDep); auto provider = providerName; auto tasks = makeShared( QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString())); if (!dep.addonId.toString().isEmpty()) { tasks->addTask(getProjectInfoTask(pDep)); } ResourceAPI::DependencySearchArgs args = { .dependency = dep, .mcVersion = m_version, .loader = m_loaderType, .includeChangelog = true }; ResourceAPI::Callback callbacks; callbacks.on_fail = [](const QString& reason, int) { qCritical() << tr("A network error occurred. Could not load project dependencies:%1").arg(reason); }; callbacks.on_succeed = [dep, provider, pDep, level, this](auto& pack) { pDep->version = pack; if (!pDep->version.addonId.isValid()) { if (m_loaderType & ModPlatform::Quilt) { // falback for quilt auto overide = ModPlatform::getOverrideDeps(); auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, provider](const auto& o) { return o.provider == provider && dep.addonId == o.quilt; }); if (over != overide.cend()) { removePack(dep.addonId); addTask(prepareDependencyTask({ .addonId = over->fabric, .type = dep.type, .version = "" }, provider, level)); return; } } removePack(dep.addonId); return; } pDep->version.is_currently_selected = true; pDep->pack->versions = { pDep->version }; pDep->pack->versionsLoaded = true; if (level == 0) { removePack(dep.addonId); qWarning() << "Dependency cycle exceeded"; return; } if (dep.addonId.toString().isEmpty() && !pDep->version.addonId.toString().isEmpty()) { pDep->pack->addonId = pDep->version.addonId; auto dep_ = getOverride({ .addonId = pDep->version.addonId, .type = pDep->dependency.type, .version = "" }, provider); if (dep_.addonId != pDep->version.addonId) { removePack(pDep->version.addonId); addTask(prepareDependencyTask(dep_, provider, level)); } else { addTask(getProjectInfoTask(pDep)); } } if (isLocalyInstalled(pDep)) { removePack(pDep->version.addonId); return; } for (const auto& dep_ : getDependenciesForVersion(pDep->version, provider)) { addTask(prepareDependencyTask(dep_, provider, level - 1)); } }; auto version = getAPI(provider)->getDependencyVersion(std::move(args), std::move(callbacks)); tasks->addTask(version); return tasks; } void GetModDependenciesTask::removePack(const QVariant& addonId) { auto pred = [addonId](const std::shared_ptr& v) { return v->pack->addonId == addonId; }; #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) m_pack_dependencies.removeIf(pred); #else for (auto it = m_pack_dependencies.begin(); it != m_pack_dependencies.end();) if (pred(*it)) it = m_pack_dependencies.erase(it); else ++it; #endif } auto GetModDependenciesTask::getExtraInfo() -> QHash { QHash rby; auto fullList = m_selected + m_pack_dependencies; for (auto& mod : fullList) { auto addonId = mod->pack->addonId; auto provider = mod->pack->provider; auto version = mod->version.fileId; auto req = QStringList(); for (auto& smod : fullList) { if (provider != smod->pack->provider) continue; auto deps = smod->version.dependencies; if (auto dep = std::find_if(deps.begin(), deps.end(), [addonId, provider, version](const ModPlatform::Dependency& d) { return d.type == ModPlatform::DependencyType::REQUIRED && (provider == ModPlatform::ResourceProvider::MODRINTH && d.addonId.toString().isEmpty() ? version == d.version : d.addonId == addonId); }); dep != deps.end()) { req.append(smod->pack->name); } } rby[addonId.toString()] = { maybeInstalled(mod), req }; } return rby; } // super lax compare (but not fuzzy) // convert to lowercase // convert all speratores to whitespace // simplify sequence of internal whitespace to a single space // efectivly compare two strings ignoring all separators and case auto laxCompare = [](QString fsfilename, QString metadataFilename, bool excludeDigits = false) { // allowed character seperators QList allowedSeperators = { '-', '+', '.', '_' }; if (excludeDigits) allowedSeperators.append({ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }); // copy in lowercase auto fsName = fsfilename.toLower(); auto metaName = metadataFilename.toLower(); // replace all potential allowed seperatores with whitespace for (auto sep : allowedSeperators) { fsName = fsName.replace(sep, ' '); metaName = metaName.replace(sep, ' '); } // remove extraneous whitespace fsName = fsName.simplified(); metaName = metaName.simplified(); return fsName.compare(metaName) == 0; }; bool GetModDependenciesTask::isLocalyInstalled(std::shared_ptr pDep) { return pDep->version.fileName.isEmpty() || std::find_if(m_selected.begin(), m_selected.end(), [pDep](std::shared_ptr i) { return !i->version.fileName.isEmpty() && laxCompare(i->version.fileName, pDep->version.fileName); }) != m_selected.end() || // check the selected versions std::find_if(m_mods_file_names.begin(), m_mods_file_names.end(), [pDep](QString i) { return !i.isEmpty() && laxCompare(i, pDep->version.fileName); }) != m_mods_file_names.end() || // check the existing mods std::find_if(m_pack_dependencies.begin(), m_pack_dependencies.end(), [pDep](std::shared_ptr i) { return pDep->pack->addonId != i->pack->addonId && !i->version.fileName.isEmpty() && laxCompare(pDep->version.fileName, i->version.fileName); }) != m_pack_dependencies.end(); // check loaded dependencies } bool GetModDependenciesTask::maybeInstalled(std::shared_ptr pDep) { return std::find_if(m_mods_file_names.begin(), m_mods_file_names.end(), [pDep](QString i) { return !i.isEmpty() && laxCompare(i, pDep->version.fileName, true); }) != m_mods_file_names.end(); // check the existing mods } PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h0000644000175100017510000000327415224505336026316 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "minecraft/mod/WorldSave.h" #include "tasks/Task.h" namespace WorldSaveUtils { enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(WorldSave& save, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); bool validate(QFileInfo file); } // namespace WorldSaveUtils class LocalWorldSaveParseTask : public Task { Q_OBJECT public: LocalWorldSaveParseTask(int token, WorldSave& save); bool canAbort() const override { return true; } bool abort() override; void executeTask() override; int token() const { return m_token; } private: int m_token; WorldSave& m_save; bool m_aborted = false; }; PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp0000644000175100017510000001335015224505336026645 0ustar runnerrunner // SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "LocalWorldSaveParseTask.h" #include "FileSystem.h" #include "archive/ArchiveReader.h" #include #include #include namespace WorldSaveUtils { bool process(WorldSave& pack, ProcessingLevel level) { switch (pack.type()) { case ResourceType::FOLDER: return WorldSaveUtils::processFolder(pack, level); case ResourceType::ZIPFILE: return WorldSaveUtils::processZIP(pack, level); default: qWarning() << "Invalid type for world save parse task!"; return false; } } /// @brief checks a folder structure to see if it contains a level.dat /// @param dir the path to check /// @param saves used in recursive call if a "saves" dir was found /// @return std::tuple of ( /// bool , /// QString , /// bool /// ) static std::tuple contains_level_dat(QDir dir, bool saves = false) { for (auto const& entry : dir.entryInfoList()) { if (!entry.isDir()) { continue; } if (!saves && entry.fileName() == "saves") { return contains_level_dat(QDir(entry.filePath()), true); } QFileInfo level_dat(FS::PathCombine(entry.filePath(), "level.dat")); if (level_dat.exists() && level_dat.isFile()) { return std::make_tuple(true, entry.fileName(), saves); } } return std::make_tuple(false, "", saves); } bool processFolder(WorldSave& save, ProcessingLevel level) { Q_ASSERT(save.type() == ResourceType::FOLDER); auto [found, save_dir_name, found_saves_dir] = contains_level_dat(QDir(save.fileinfo().filePath())); if (!found) { return false; } save.setSaveDirName(save_dir_name); if (found_saves_dir) { save.setSaveFormat(WorldSaveFormat::MULTI); } else { save.setSaveFormat(WorldSaveFormat::SINGLE); } if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } // reserved for more intensive processing return true; // all tests passed } /// @brief checks a folder structure to see if it contains a level.dat /// @param zip the zip file to check /// @return std::tuple of ( /// bool , /// QString , /// bool /// ) static std::tuple contains_level_dat(QString fileName) { MMCZip::ArchiveReader zip(fileName); if (!zip.collectFiles()) { return std::make_tuple(false, "", false); } bool saves = false; if (zip.exists("/saves")) { saves = true; } for (auto file : zip.getFiles()) { QString relativePath = file; if (saves) { if (!relativePath.startsWith("saves/", Qt::CaseInsensitive)) continue; relativePath = relativePath.mid(QString("saves/").length()); } if (!relativePath.endsWith("/level.dat", Qt::CaseInsensitive)) continue; int slashIndex = relativePath.indexOf('/'); if (slashIndex == -1) continue; // malformed: no slash between saves/ and level.dat QString worldName = relativePath.left(slashIndex); QString remaining = relativePath.mid(slashIndex + 1); // Check that there's nothing between worldName/ and level.dat if (remaining == "level.dat") { return std::make_tuple(true, worldName, saves); } } return std::make_tuple(false, "", saves); } bool processZIP(WorldSave& save, ProcessingLevel level) { Q_ASSERT(save.type() == ResourceType::ZIPFILE); auto [found, save_dir_name, found_saves_dir] = contains_level_dat(save.fileinfo().filePath()); if (!found) { return false; } if (save_dir_name.endsWith("/")) { save_dir_name.chop(1); } save.setSaveDirName(save_dir_name); if (found_saves_dir) { save.setSaveFormat(WorldSaveFormat::MULTI); } else { save.setSaveFormat(WorldSaveFormat::SINGLE); } if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } // reserved for more intensive processing return true; } bool validate(QFileInfo file) { WorldSave sp{ file }; return WorldSaveUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid(); } } // namespace WorldSaveUtils LocalWorldSaveParseTask::LocalWorldSaveParseTask(int token, WorldSave& save) : Task(false), m_token(token), m_save(save) {} bool LocalWorldSaveParseTask::abort() { m_aborted = true; return true; } void LocalWorldSaveParseTask::executeTask() { if (!WorldSaveUtils::process(m_save)) { emitFailed("this is not a world"); return; } if (m_aborted) emitAborted(); else emitSucceeded(); } PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp0000644000175100017510000001453515224505336027204 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "LocalTexturePackParseTask.h" #include "FileSystem.h" #include "archive/ArchiveReader.h" #include namespace TexturePackUtils { bool process(TexturePack& pack, ProcessingLevel level) { switch (pack.type()) { case ResourceType::FOLDER: return TexturePackUtils::processFolder(pack, level); case ResourceType::ZIPFILE: return TexturePackUtils::processZIP(pack, level); default: qWarning() << "Invalid type for resource pack parse task!"; return false; } } bool processFolder(TexturePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::FOLDER); QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.txt")); if (mcmeta_file_info.isFile()) { QFile mcmeta_file(mcmeta_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) return false; auto data = mcmeta_file.readAll(); bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data)); mcmeta_file.close(); if (!packTXT_result) { return false; } } else { return false; } if (level == ProcessingLevel::BasicInfoOnly) return true; QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); if (image_file_info.isFile()) { QFile mcmeta_file(image_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) return false; auto data = mcmeta_file.readAll(); bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); mcmeta_file.close(); if (!packPNG_result) { return false; } } else { return false; } return true; } bool processZIP(TexturePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); MMCZip::ArchiveReader zip(pack.fileinfo().filePath()); bool packProcessed = false; bool iconProcessed = false; return zip.parse([&packProcessed, &iconProcessed, &pack, level](MMCZip::ArchiveReader::File* file, bool& stop) { if (!packProcessed && file->filename() == "pack.txt") { packProcessed = true; auto data = file->readAll(); stop = packProcessed && (iconProcessed || level == ProcessingLevel::BasicInfoOnly); return TexturePackUtils::processPackTXT(pack, std::move(data)); } if (!iconProcessed && file->filename() == "pack.png") { iconProcessed = true; auto data = file->readAll(); stop = packProcessed && iconProcessed; return TexturePackUtils::processPackPNG(pack, std::move(data)); } file->skip(); return true; }); } bool processPackTXT(TexturePack& pack, QByteArray&& raw_data) { pack.setDescription(QString(raw_data)); return true; } bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { pack.setImage(img); } else { qWarning() << "Failed to parse pack.png."; return false; } return true; } bool processPackPNG(const TexturePack& pack) { auto png_invalid = [&pack]() { qWarning() << "Texture pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; return false; }; switch (pack.type()) { case ResourceType::FOLDER: { QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); if (image_file_info.exists() && image_file_info.isFile()) { QFile pack_png_file(image_file_info.filePath()); if (!pack_png_file.open(QIODevice::ReadOnly)) return png_invalid(); // can't open pack.png file auto data = pack_png_file.readAll(); bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); pack_png_file.close(); if (!pack_png_result) { return png_invalid(); // pack.png invalid } } else { return png_invalid(); // pack.png does not exists or is not a valid file. } return false; } case ResourceType::ZIPFILE: { MMCZip::ArchiveReader zip(pack.fileinfo().filePath()); auto file = zip.goToFile("pack.png"); if (file) { auto data = file->readAll(); bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); if (!pack_png_result) { return png_invalid(); // pack.png invalid } } return png_invalid(); // could not set pack.mcmeta as current file. } default: qWarning() << "Invalid type for resource pack parse task!"; return false; } } bool validate(QFileInfo file) { TexturePack rp{ file }; return TexturePackUtils::process(rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); } } // namespace TexturePackUtils LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp) : Task(false), m_token(token), m_texture_pack(rp) {} bool LocalTexturePackParseTask::abort() { m_aborted = true; return true; } void LocalTexturePackParseTask::executeTask() { if (!TexturePackUtils::process(m_texture_pack)) { emitFailed("this is not a texture pack"); return; } if (m_aborted) emitAborted(); else emitSucceeded(); } PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalResourceParse.cpp0000644000175100017510000000505515224505336025706 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include "LocalResourceParse.h" #include "LocalDataPackParseTask.h" #include "LocalModParseTask.h" #include "LocalShaderPackParseTask.h" #include "LocalTexturePackParseTask.h" #include "LocalWorldSaveParseTask.h" #include "modplatform/ResourceType.h" namespace ResourceUtils { ModPlatform::ResourceType identify(QFileInfo file) { if (file.exists() && file.isFile()) { if (ModUtils::validate(file)) { // mods can contain resource and data packs so they must be tested first qDebug() << file.fileName() << "is a mod"; return ModPlatform::ResourceType::Mod; } else if (DataPackUtils::validateResourcePack(file)) { qDebug() << file.fileName() << "is a resource pack"; return ModPlatform::ResourceType::ResourcePack; } else if (TexturePackUtils::validate(file)) { qDebug() << file.fileName() << "is a pre 1.6 texture pack"; return ModPlatform::ResourceType::TexturePack; } else if (DataPackUtils::validate(file)) { qDebug() << file.fileName() << "is a data pack"; return ModPlatform::ResourceType::DataPack; } else if (WorldSaveUtils::validate(file)) { qDebug() << file.fileName() << "is a world save"; return ModPlatform::ResourceType::World; } else if (ShaderPackUtils::validate(file)) { qDebug() << file.fileName() << "is a shader pack"; return ModPlatform::ResourceType::ShaderPack; } else { qDebug() << "Can't Identify" << file.fileName(); } } else { qDebug() << "Can't find" << file.absolutePath(); } return ModPlatform::ResourceType::Unknown; } } // namespace ResourceUtils PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalModParseTask.h0000644000175100017510000000303515224505336025122 0ustar runnerrunner#pragma once #include #include #include "minecraft/mod/Mod.h" #include "minecraft/mod/ModDetails.h" #include "tasks/Task.h" namespace ModUtils { ModDetails ReadFabricModInfo(QByteArray contents); ModDetails ReadQuiltModInfo(QByteArray contents); ModDetails ReadForgeInfo(QByteArray contents); ModDetails ReadLiteModInfo(QByteArray contents); enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); /** Checks whether a file is valid as a mod or not. */ bool validate(QFileInfo file); bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap); bool loadIconFile(const Mod& mod, QPixmap* pixmap); } // namespace ModUtils class LocalModParseTask : public Task { Q_OBJECT public: struct Result { ModDetails details; }; using ResultPtr = std::shared_ptr; ResultPtr result() const { return m_result; } bool canAbort() const override { return true; } bool abort() override; LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile); void executeTask() override; int token() const { return m_token; } private: int m_token; ResourceType m_type; QFileInfo m_modFile; ResultPtr m_result; std::atomic m_aborted = false; }; PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp0000644000175100017510000001314415224505336026515 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ResourceFolderLoadTask.h" #include "Application.h" #include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" #include ResourceFolderLoadTask::ResourceFolderLoadTask(const QDir& resource_dir, const QDir& index_dir, bool is_indexed, bool clean_orphan, std::function create_function) : Task(false) , m_resource_dir(resource_dir) , m_index_dir(index_dir) , m_is_indexed(is_indexed) , m_clean_orphan(clean_orphan) , m_create_func(create_function) , m_result(new Result()) , m_thread_to_spawn_into(thread()) {} void ResourceFolderLoadTask::executeTask() { if (thread() != m_thread_to_spawn_into) connect(this, &Task::finished, this->thread(), &QThread::quit); if (m_is_indexed) { // Read metadata first getFromMetadata(); } // Read JAR files that don't have metadata m_resource_dir.refresh(); for (auto entry : m_resource_dir.entryInfoList()) { auto filePath = entry.absoluteFilePath(); if (auto app = APPLICATION_DYN; app && app->checkQSavePath(filePath)) { continue; } auto newFilePath = FS::getUniqueResourceName(filePath); if (newFilePath != filePath) { FS::move(filePath, newFilePath); entry = QFileInfo(newFilePath); } Resource* resource = m_create_func(entry); if (resource->enabled()) { if (m_result->resources.contains(resource->internal_id())) { m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); // Delete the object we just created, since a valid one is already in the mods list. delete resource; } else { m_result->resources[resource->internal_id()].reset(resource); m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); } } else { QString chopped_id = resource->internal_id().chopped(9); if (m_result->resources.contains(chopped_id)) { m_result->resources[resource->internal_id()].reset(resource); auto metadata = m_result->resources[chopped_id]->metadata(); if (metadata) { resource->setMetadata(*metadata); m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); m_result->resources.remove(chopped_id); } } else { m_result->resources[resource->internal_id()].reset(resource); m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); } } } // Remove orphan metadata to prevent issues // See https://github.com/PolyMC/PolyMC/issues/996 if (m_clean_orphan) { QMutableMapIterator iter(m_result->resources); while (iter.hasNext()) { auto resource = iter.next().value(); if (resource->status() == ResourceStatus::NOT_INSTALLED) { resource->destroy(m_index_dir, false, false); iter.remove(); } } } for (auto mod : m_result->resources) mod->moveToThread(m_thread_to_spawn_into); if (m_aborted) emit finished(); else emitSucceeded(); } void ResourceFolderLoadTask::getFromMetadata() { m_index_dir.refresh(); for (auto entry : m_index_dir.entryList(QDir::Files)) { if (!entry.endsWith(".pw.toml")) { continue; } auto metadata = Metadata::get(m_index_dir, entry); if (!metadata.isValid()) continue; auto* resource = m_create_func(QFileInfo(m_resource_dir.filePath(metadata.filename))); resource->setMetadata(metadata); resource->setStatus(ResourceStatus::NOT_INSTALLED); m_result->resources[resource->internal_id()].reset(resource); } } PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalModParseTask.cpp0000644000175100017510000007403015224505336025460 0ustar runnerrunner#include "LocalModParseTask.h" #include #include #include #include #include #include #include #include #include "FileSystem.h" #include "Json.h" #include "archive/ArchiveReader.h" #include "minecraft/mod/ModDetails.h" #include "settings/INIFile.h" static const QRegularExpression s_newlineRegex("\r\n|\n|\r"); namespace ModUtils { // NEW format // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/c8d8f1929aff9979e322af79a59ce81f3e02db6a // OLD format: // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc ModDetails ReadMCModInfo(QByteArray contents) { auto getInfoFromArray = [](QJsonArray arr) -> ModDetails { if (!arr.at(0).isObject()) { return {}; } ModDetails details; auto firstObj = arr.at(0).toObject(); details.mod_id = firstObj.value("modid").toString(); auto name = firstObj.value("name").toString(); // NOTE: ignore stupid example mods copies where the author didn't even bother to change the name if (name != "Example Mod") { details.name = name; } details.version = firstObj.value("version").toString(); auto homeurl = firstObj.value("url").toString().trimmed(); if (!homeurl.isEmpty()) { // fix up url. if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) { homeurl.prepend("http://"); } } details.homeurl = homeurl; details.description = firstObj.value("description").toString(); QJsonArray authors = firstObj.value("authorList").toArray(); if (authors.size() == 0) { // FIXME: what is the format of this? is there any? authors = firstObj.value("authors").toArray(); } if (firstObj.contains("logoFile")) { details.icon_file = firstObj.value("logoFile").toString(); } for (auto author : authors) { details.authors.append(author.toString()); } if (details.mod_id.startsWith("mod_")) { details.mod_id = details.mod_id.mid(4); } auto addDep = [&details](QString dep) { if (dep == "mod_MinecraftForge" || dep == "Forge") return; if (dep.contains(":")) { dep = dep.section(":", 1); } if (dep.contains("@")) { dep = dep.section("@", 0, 0); } if (dep.startsWith("mod_")) { dep = dep.mid(4); } details.dependencies.append(dep); }; if (firstObj.contains("requiredMods")) { for (auto dep : firstObj.value("requiredMods").toArray()) { addDep(dep.toString()); } } else if (firstObj.contains("dependencies")) { for (auto dep : firstObj.value("dependencies").toArray()) { addDep(dep.toString()); } } return details; }; QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); // this is the very old format that had just the array if (jsonDoc.isArray()) { return getInfoFromArray(jsonDoc.array()); } else if (jsonDoc.isObject()) { auto val = jsonDoc.object().value("modinfoversion"); if (val.isUndefined()) { val = jsonDoc.object().value("modListVersion"); } int version = val.toInt(-1); // Some mods set the number with "", so it's a String instead if (version < 0) version = val.toString("").toInt(); if (version != 2) { qWarning() << QString(R"(The value of 'modListVersion' is "%1" (expected "2")! The file may be corrupted.)").arg(version); qWarning() << "The contents of 'mcmod.info' are as follows:"; qWarning() << contents; } auto arrVal = jsonDoc.object().value("modlist"); if (arrVal.isUndefined()) { arrVal = jsonDoc.object().value("modList"); } if (arrVal.isArray()) { return getInfoFromArray(arrVal.toArray()); } } return {}; } // https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md ModDetails ReadMCModTOML(QByteArray contents) { ModDetails details; toml::table tomlData; #if TOML_EXCEPTIONS try { tomlData = toml::parse(contents.toStdString()); } catch ([[maybe_unused]] const toml::parse_error& err) { return {}; } #else toml::parse_result result = toml::parse(contents.toStdString()); if (!result) { return {}; } tomlData = result.table(); #endif // array defined by [[mods]] auto tomlModsArr = tomlData["mods"].as_array(); if (!tomlModsArr) { qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!"; return {}; } // we only really care about the first element, since multiple mods in one file is not supported by us at the moment auto tomlModsTable0 = tomlModsArr->get(0); if (!tomlModsTable0) { qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!"; return {}; } auto modsTable = tomlModsTable0->as_table(); if (!modsTable) { qWarning() << "Corrupted mods.toml? [[mods]] was not a table!"; return {}; } // mandatory properties - always in [[mods]] if (auto modIdDatum = (*modsTable)["modId"].as_string()) { details.mod_id = QString::fromStdString(modIdDatum->get()); } if (auto versionDatum = (*modsTable)["version"].as_string()) { details.version = QString::fromStdString(versionDatum->get()); } if (auto displayNameDatum = (*modsTable)["displayName"].as_string()) { details.name = QString::fromStdString(displayNameDatum->get()); } if (auto descriptionDatum = (*modsTable)["description"].as_string()) { details.description = QString::fromStdString(descriptionDatum->get()); } // optional properties - can be in the root table or [[mods]] QString authors = ""; if (auto authorsDatum = tomlData["authors"].as_string()) { authors = QString::fromStdString(authorsDatum->get()); } else if (auto authorsDatumMods = (*modsTable)["authors"].as_string()) { authors = QString::fromStdString(authorsDatumMods->get()); } if (!authors.isEmpty()) { details.authors.append(authors); } QString homeurl = ""; if (auto homeurlDatum = tomlData["displayURL"].as_string()) { homeurl = QString::fromStdString(homeurlDatum->get()); } else if (auto homeurlDatumMods = (*modsTable)["displayURL"].as_string()) { homeurl = QString::fromStdString(homeurlDatumMods->get()); } // fix up url. if (!homeurl.isEmpty() && !homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) { homeurl.prepend("http://"); } details.homeurl = homeurl; QString issueTrackerURL = ""; if (auto issueTrackerURLDatum = tomlData["issueTrackerURL"].as_string()) { issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get()); } else if (auto issueTrackerURLDatumMods = (*modsTable)["issueTrackerURL"].as_string()) { issueTrackerURL = QString::fromStdString(issueTrackerURLDatumMods->get()); } details.issue_tracker = issueTrackerURL; QString license = ""; if (auto licenseDatum = tomlData["license"].as_string()) { license = QString::fromStdString(licenseDatum->get()); } else if (auto licenseDatumMods = (*modsTable)["license"].as_string()) { license = QString::fromStdString(licenseDatumMods->get()); } if (!license.isEmpty()) details.licenses.append(ModLicense(license)); QString logoFile = ""; if (auto logoFileDatum = tomlData["logoFile"].as_string()) { logoFile = QString::fromStdString(logoFileDatum->get()); } else if (auto logoFileDatumMods = (*modsTable)["logoFile"].as_string()) { logoFile = QString::fromStdString(logoFileDatumMods->get()); } details.icon_file = logoFile; auto parseDep = [&details](toml::array* dependencies) { static const QStringList ignoreModIds = { "", "forge", "neoforge", "minecraft" }; if (!dependencies) { return; } auto isNeoForgeDep = [](toml::table* t) { auto type = (*t)["type"].as_string(); return type && type->get() == "required"; }; auto isForgeDep = [](toml::table* t) { auto mandatory = (*t)["mandatory"].as_boolean(); return mandatory && mandatory->get(); }; for (auto& dep : *dependencies) { auto dep_table = dep.as_table(); if (!dep_table) { continue; } auto modId = (*dep_table)["modId"].as_string(); if (!modId || ignoreModIds.contains(QString::fromStdString(modId->get()))) { continue; } if (isNeoForgeDep(dep_table) || isForgeDep(dep_table)) { details.dependencies.append(QString::fromStdString(modId->get())); } } }; if (tomlData.contains("dependencies")) { auto depValue = tomlData["dependencies"]; if (auto array = depValue.as_array()) { parseDep(array); } else if (auto depTable = depValue.as_table()) { auto expectedKey = details.mod_id.toStdString(); if (!depTable->contains(expectedKey)) { if (auto it = depTable->begin(); it != depTable->end()) { expectedKey = it->first; } } if ((array = (*depTable)[expectedKey].as_array())) { parseDep(array); } } } return details; } // https://fabricmc.net/wiki/documentation:fabric_mod_json ModDetails ReadFabricModInfo(QByteArray contents) { QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); auto object = jsonDoc.object(); auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0; ModDetails details; details.mod_id = object.value("id").toString(); details.version = object.value("version").toString(); details.name = object.contains("name") ? object.value("name").toString() : details.mod_id; details.description = object.value("description").toString(); if (schemaVersion >= 1) { QJsonArray authors = object.value("authors").toArray(); for (auto author : authors) { if (author.isObject()) { details.authors.append(author.toObject().value("name").toString()); } else { details.authors.append(author.toString()); } } if (object.contains("contact")) { QJsonObject contact = object.value("contact").toObject(); if (contact.contains("homepage")) { details.homeurl = contact.value("homepage").toString(); } if (contact.contains("issues")) { details.issue_tracker = contact.value("issues").toString(); } } if (object.contains("license")) { auto license = object.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 (object.contains("icon")) { auto icon = object.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 if (auto it = obj.begin(); it != obj.end()) { details.icon_file = it->toString(); } } } else if (icon.isString()) { details.icon_file = icon.toString(); } } if (object.contains("depends")) { auto depends = object.value("depends"); if (depends.isObject()) { auto obj = depends.toObject(); for (auto key : obj.keys()) { if (key != "fabricloader" && key != "minecraft" && !key.startsWith("fabric-")) { details.dependencies.append(key); } } } } } return details; } // https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md ModDetails ReadQuiltModInfo(QByteArray contents) { ModDetails details; try { QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); auto object = Json::requireObject(jsonDoc, "quilt.mod.json"); auto schemaVersion = object.value("schema_version").toInt(); // 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"); auto modMetadata = modInfo.value("metadata").toObject(); details.name = modMetadata.value("name").toString(details.mod_id); details.description = modMetadata.value("description").toString(); auto modContributors = modMetadata.value("contributors").toObject(); // We don't really care about the role of a contributor here details.authors += modContributors.keys(); auto modContact = modMetadata.value("contact").toObject(); 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())); } } } 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 if (auto it = obj.begin(); it != obj.end()) { details.icon_file = it->toString(); } } } else if (icon.isString()) { details.icon_file = icon.toString(); } } if (object.contains("depends")) { auto depends = object.value("depends"); if (depends.isArray()) { auto array = depends.toArray(); for (auto obj : array) { QString modId; if (obj.isString()) { modId = obj.toString(); } else if (obj.isObject()) { auto objValue = obj.toObject(); modId = objValue.value("id").toString(); if (objValue.contains("optional") && objValue.value("optional").toBool()) { continue; } } else { continue; } if (modId != "minecraft" && !modId.startsWith("quilt_")) { details.dependencies.append(modId); } } } } } } catch (const Exception& e) { qWarning() << "Unable to parse mod info:" << e.cause(); } return details; } ModDetails ReadForgeInfo(QByteArray contents) { ModDetails details; // Read the data details.name = "Minecraft Forge"; details.mod_id = "Forge"; details.homeurl = "http://www.minecraftforge.net/forum/"; INIFile ini; if (!ini.loadFile(contents)) return details; QString major = ini.get("forge.major.number", "0").toString(); QString minor = ini.get("forge.minor.number", "0").toString(); QString revision = ini.get("forge.revision.number", "0").toString(); QString build = ini.get("forge.build.number", "0").toString(); details.version = major + "." + minor + "." + revision + "." + build; return details; } ModDetails ReadLiteModInfo(QByteArray contents) { ModDetails details; QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); auto object = jsonDoc.object(); if (object.contains("name")) { details.mod_id = details.name = object.value("name").toString(); } if (object.contains("version")) { details.version = object.value("version").toString(""); } else { details.version = object.value("revision").toString(""); } details.mcversion = object.value("mcversion").toString(); auto author = object.value("author").toString(); if (!author.isEmpty()) { details.authors.append(author); } details.description = object.value("description").toString(); details.homeurl = object.value("url").toString(); return details; } // https://git.sleeping.town/unascribed/NilLoader/src/commit/d7fc87b255fc31019ff90f80d45894927fac6efc/src/main/java/nilloader/api/NilMetadata.java#L64 ModDetails ReadNilModInfo(QByteArray contents, QString fname) { ModDetails details; QDCSS cssData = QDCSS(contents); auto name = cssData.get("@nilmod.name"); auto desc = cssData.get("@nilmod.description"); auto authors = cssData.get("@nilmod.authors"); if (name->has_value()) { details.name = name->value(); } if (desc->has_value()) { details.description = desc->value(); } if (authors->has_value()) { details.authors.append(authors->value()); } details.version = cssData.get("@nilmod.version")->value_or("?"); details.mod_id = fname.remove(".nilmod.css"); return details; } bool process(Mod& mod, ProcessingLevel level) { switch (mod.type()) { case ResourceType::FOLDER: return processFolder(mod, level); case ResourceType::ZIPFILE: return processZIP(mod, level); case ResourceType::LITEMOD: return processLitemod(mod); default: qWarning() << "Invalid type" << mod.type() << "for mod parse task!"; return false; } } bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level) { ModDetails details; MMCZip::ArchiveReader zip(mod.fileinfo().filePath()); bool baseForgePopulated = false; bool isNilMod = false; bool isValid = false; QString manifestVersion = {}; QByteArray nilData = {}; QString nilFilePath = {}; if (!zip.parse([&details, &baseForgePopulated, &manifestVersion, &isValid, &nilData, &isNilMod, &nilFilePath]( MMCZip::ArchiveReader::File* file, bool& stop) { auto filePath = file->filename(); if (filePath == "META-INF/mods.toml" || filePath == "META-INF/neoforge.mods.toml") { details = ReadMCModTOML(file->readAll()); isValid = true; if (details.version == "${file.jarVersion}" && !manifestVersion.isEmpty()) { details.version = manifestVersion; } stop = details.version != "${file.jarVersion}"; baseForgePopulated = true; return true; } if (filePath == "META-INF/MANIFEST.MF") { // quick and dirty line-by-line parser auto manifestLines = QString(file->readAll()).split(s_newlineRegex); manifestVersion = ""; for (auto& line : manifestLines) { if (line.startsWith("Implementation-Version: ", Qt::CaseInsensitive)) { manifestVersion = line.remove("Implementation-Version: ", Qt::CaseInsensitive); break; } } // some mods use ${projectversion} in their build.gradle, causing this mess to show up in MANIFEST.MF // also keep with forge's behavior of setting the version to "NONE" if none is found if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") { manifestVersion = "NONE"; } if (baseForgePopulated) { details.version = manifestVersion; stop = true; } return true; } if (filePath == "mcmod.info") { details = ReadMCModInfo(file->readAll()); isValid = true; stop = true; return true; } if (filePath == "quilt.mod.json") { details = ReadQuiltModInfo(file->readAll()); isValid = true; stop = true; return true; } if (filePath == "fabric.mod.json") { details = ReadFabricModInfo(file->readAll()); isValid = true; stop = true; return true; } if (filePath == "forgeversion.properties") { details = ReadForgeInfo(file->readAll()); isValid = true; stop = true; return true; } if (filePath == "META-INF/nil/mappings.json") { // nilloader uses the filename of the metadata file for the modid, so we can't know the exact filename // thankfully, there is a good file to use as a canary so we don't look for nil meta all the time isNilMod = true; stop = !nilFilePath.isEmpty(); file->skip(); return true; } // nilmods can shade nilloader to be able to run as a standalone agent - which includes nilloader's own meta file if (filePath.endsWith(".nilmod.css") && filePath != "nilloader.nilmod.css") { nilData = file->readAll(); nilFilePath = filePath; stop = isNilMod; return true; } file->skip(); return true; })) { return false; } if (isNilMod) { details = ReadNilModInfo(nilData, nilFilePath); isValid = true; } if (isValid) { mod.setDetails(details); return true; } return false; // no valid mod found in archive } bool processFolder(Mod& mod, [[maybe_unused]] ProcessingLevel level) { ModDetails details; QFileInfo mcmod_info(FS::PathCombine(mod.fileinfo().filePath(), "mcmod.info")); if (mcmod_info.exists() && mcmod_info.isFile()) { QFile mcmod(mcmod_info.filePath()); if (!mcmod.open(QIODevice::ReadOnly)) return false; auto data = mcmod.readAll(); if (data.isEmpty() || data.isNull()) return false; details = ReadMCModInfo(data); mod.setDetails(details); return true; } return false; // no valid mcmod.info file found } bool processLitemod(Mod& mod, [[maybe_unused]] ProcessingLevel level) { ModDetails details; MMCZip::ArchiveReader zip(mod.fileinfo().filePath()); if (auto file = zip.goToFile("litemod.json"); file) { details = ReadLiteModInfo(file->readAll()); mod.setDetails(details); return true; } return false; // no valid litemod.json found in archive } /** Checks whether a file is valid as a mod or not. */ bool validate(QFileInfo file) { Mod mod{ file }; return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid(); } bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { *pixmap = mod.setIcon(img); } else { qWarning() << "Failed to parse mod logo:" << mod.iconPath() << "from" << mod.name(); return false; } return true; } bool loadIconFile(const Mod& mod, QPixmap* pixmap) { if (mod.iconPath().isEmpty()) { qWarning() << "No Iconfile set, be sure to parse the mod first"; return false; } auto png_invalid = [&mod](const QString& reason) { qWarning() << "Mod at" << mod.fileinfo().filePath() << "does not have a valid icon:" << reason; return false; }; switch (mod.type()) { case ResourceType::FOLDER: { QFileInfo icon_info(FS::PathCombine(mod.fileinfo().filePath(), mod.iconPath())); if (icon_info.exists() && icon_info.isFile()) { QFile icon(icon_info.filePath()); if (!icon.open(QIODevice::ReadOnly)) { return png_invalid("failed to open file " + icon_info.filePath() + " " + icon.errorString()); } auto data = icon.readAll(); bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); icon.close(); if (!icon_result) { return png_invalid("invalid png image"); // icon invalid } return true; } return png_invalid("file '" + icon_info.filePath() + "' does not exists or is not a file"); } case ResourceType::ZIPFILE: { MMCZip::ArchiveReader zip(mod.fileinfo().filePath()); auto file = zip.goToFile(mod.iconPath()); if (file) { auto data = file->readAll(); bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); if (!icon_result) { return png_invalid("invalid png image"); // icon png invalid } return true; } return png_invalid("Failed to set '" + mod.iconPath() + "' as current file in zip archive"); // could not set icon as current file. } case ResourceType::LITEMOD: { return png_invalid("litemods do not have icons"); // can lightmods even have icons? } default: return png_invalid("Invalid type for mod, can not load icon."); } } } // namespace ModUtils LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) : Task(false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) {} bool LocalModParseTask::abort() { m_aborted.store(true); return true; } void LocalModParseTask::executeTask() { Mod mod{ m_modFile }; ModUtils::process(mod, ModUtils::ProcessingLevel::Full); m_result->details = mod.details(); if (m_aborted) emitAborted(); else emitSucceeded(); } PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp0000644000175100017510000003144715224505336026416 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "LocalDataPackParseTask.h" #include "FileSystem.h" #include "Json.h" #include "archive/ArchiveReader.h" #include "minecraft/mod/ResourcePack.h" #include #include namespace DataPackUtils { bool process(DataPack* pack, ProcessingLevel level) { switch (pack->type()) { case ResourceType::FOLDER: return DataPackUtils::processFolder(pack, level); case ResourceType::ZIPFILE: return DataPackUtils::processZIP(pack, level); default: qWarning() << "Invalid type for data pack parse task!"; return false; } } bool processFolder(DataPack* pack, ProcessingLevel level) { Q_ASSERT(pack->type() == ResourceType::FOLDER); auto mcmeta_invalid = [&pack]() { qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta"; return false; // the mcmeta is not optional }; QFileInfo mcmeta_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.mcmeta")); if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) { QFile mcmeta_file(mcmeta_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) return mcmeta_invalid(); // can't open mcmeta file auto data = mcmeta_file.readAll(); bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); mcmeta_file.close(); if (!mcmeta_result) { return mcmeta_invalid(); // mcmeta invalid } } else { return mcmeta_invalid(); // mcmeta file isn't a valid file } if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } auto png_invalid = [&pack]() { qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; return true; // the png is optional }; QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); if (image_file_info.exists() && image_file_info.isFile()) { QFile pack_png_file(image_file_info.filePath()); if (!pack_png_file.open(QIODevice::ReadOnly)) return png_invalid(); // can't open pack.png file auto data = pack_png_file.readAll(); bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); pack_png_file.close(); if (!pack_png_result) { return png_invalid(); // pack.png invalid } } else { return png_invalid(); // pack.png does not exists or is not a valid file. } return true; // all tests passed } bool processZIP(DataPack* pack, ProcessingLevel level) { Q_ASSERT(pack->type() == ResourceType::ZIPFILE); MMCZip::ArchiveReader zip(pack->fileinfo().filePath()); bool metaParsed = false; bool iconParsed = false; bool mcmeta_result = false; bool pack_png_result = false; if (!zip.parse( [&metaParsed, &iconParsed, &mcmeta_result, &pack_png_result, pack, level](MMCZip::ArchiveReader::File* f, bool& breakControl) { bool skip = true; if (!metaParsed && f->filename() == "pack.mcmeta") { metaParsed = true; skip = false; auto data = f->readAll(); mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); if (!mcmeta_result) { breakControl = true; return true; // mcmeta invalid } } if (!iconParsed && level != ProcessingLevel::BasicInfoOnly && f->filename() == "pack.png") { iconParsed = true; skip = false; auto data = f->readAll(); pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); if (!pack_png_result) { breakControl = true; return true; // pack.png invalid } } if (skip) { f->skip(); } if (metaParsed && (level == ProcessingLevel::BasicInfoOnly || iconParsed)) { breakControl = true; } return true; })) { return false; // can't open zip file } if (!mcmeta_result) { qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta"; return false; // the mcmeta is not optional } if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } if (!pack_png_result) { qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; return true; // the png is optional } return true; } std::pair parseVersion(const QJsonValue& value) { if (value.isDouble()) { // Single integer -> [major, 0] return std::make_pair(value.toInt(), 0); } std::pair version; if (value.isArray()) { QJsonArray arr = value.toArray(); if (arr.size() >= 1) { version.first = arr.at(0).toInt(); } if (arr.size() >= 2) { version.second = arr.at(1).toInt(); } } return version; } // https://minecraft.wiki/w/Data_pack#pack.mcmeta // https://minecraft.wiki/w/Raw_JSON_text_format // https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta bool processMCMeta(DataPack* pack, QByteArray&& raw_data) { QJsonParseError parse_error; auto json_doc = Json::parseUntilGarbage(raw_data, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Failed to parse JSON:" << parse_error.errorString(); return false; } try { auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); int pack_format = 0; std::pair min_format; std::pair max_format; if (pack_obj.contains("pack_format")) { pack_format = pack_obj.value("pack_format").toInt(); } if (pack_obj.contains("min_format")) { min_format = parseVersion(pack_obj.value("min_format")); } if (pack_obj.contains("max_format")) { max_format = parseVersion(pack_obj.value("max_format")); } pack->setPackFormat(pack_format, min_format, max_format); pack->setDescription(DataPackUtils::processComponent(pack_obj.value("description"))); } catch (Json::JsonException& e) { qWarning() << "JsonException:" << e.what() << e.cause(); return false; } return true; } QString buildStyle(const QJsonObject& obj) { QStringList styles; if (auto color = obj["color"].toString(); !color.isEmpty()) { styles << QString("color: %1;").arg(color); } if (obj.contains("bold")) { QString weight = "normal"; if (obj["bold"].toBool()) { weight = "bold"; } styles << QString("font-weight: %1;").arg(weight); } if (obj.contains("italic")) { QString style = "normal"; if (obj["italic"].toBool()) { style = "italic"; } styles << QString("font-style: %1;").arg(style); } return styles.isEmpty() ? "" : QString("style=\"%1\"").arg(styles.join(" ")); } QString processComponent(const QJsonArray& value, bool strikethrough, bool underline) { QString result; for (auto current : value) result += processComponent(current, strikethrough, underline); return result; } QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline) { underline = obj["underlined"].toBool(underline); strikethrough = obj["strikethrough"].toBool(strikethrough); QString result = obj["text"].toString(); if (underline) { result = QString("%1").arg(result); } if (strikethrough) { result = QString("%1").arg(result); } // the extra needs to be a array result += processComponent(obj["extra"].toArray(), strikethrough, underline); if (auto style = buildStyle(obj); !style.isEmpty()) { result = QString("%2").arg(style, result); } if (obj.contains("clickEvent")) { auto click_event = obj["clickEvent"].toObject(); auto action = click_event["action"].toString(); auto value = click_event["value"].toString(); if (action == "open_url" && !value.isEmpty()) { result = QString("%2").arg(value, result); } } return result; } QString processComponent(const QJsonValue& value, bool strikethrough, bool underline) { if (value.isString()) { return value.toString(); } if (value.isBool()) { return value.toBool() ? "true" : "false"; } if (value.isDouble()) { return QString::number(value.toDouble()); } if (value.isArray()) { return processComponent(value.toArray(), strikethrough, underline); } if (value.isObject()) { return processComponent(value.toObject(), strikethrough, underline); } qWarning() << "Invalid component type!"; return {}; } bool processPackPNG(const DataPack* pack, QByteArray&& raw_data) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { pack->setImage(img); } else { qWarning() << "Failed to parse pack.png."; return false; } return true; } bool processPackPNG(const DataPack* pack) { auto png_invalid = [&pack]() { qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; return false; }; switch (pack->type()) { case ResourceType::FOLDER: { QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); if (image_file_info.exists() && image_file_info.isFile()) { QFile pack_png_file(image_file_info.filePath()); if (!pack_png_file.open(QIODevice::ReadOnly)) return png_invalid(); // can't open pack.png file auto data = pack_png_file.readAll(); bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); pack_png_file.close(); if (!pack_png_result) { return png_invalid(); // pack.png invalid } } else { return png_invalid(); // pack.png does not exists or is not a valid file. } return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 } case ResourceType::ZIPFILE: { MMCZip::ArchiveReader zip(pack->fileinfo().filePath()); auto f = zip.goToFile("pack.png"); if (!f) { return png_invalid(); } auto data = f->readAll(); bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); if (!pack_png_result) { return png_invalid(); // pack.png invalid } return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 } default: qWarning() << "Invalid type for data pack parse task!"; return false; } } bool validate(QFileInfo file) { DataPack dp{ file }; return DataPackUtils::process(&dp, ProcessingLevel::BasicInfoOnly) && dp.valid(); } bool validateResourcePack(QFileInfo file) { ResourcePack rp{ file }; return DataPackUtils::process(&rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); } } // namespace DataPackUtils LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack* dp) : Task(false), m_token(token), m_data_pack(dp) {} void LocalDataPackParseTask::executeTask() { if (!DataPackUtils::process(m_data_pack)) { emitFailed("process failed"); return; } emitSucceeded(); } PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp0000644000175100017510000000756715224505336026761 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "LocalShaderPackParseTask.h" #include "FileSystem.h" #include "archive/ArchiveReader.h" namespace ShaderPackUtils { bool process(ShaderPack& pack, ProcessingLevel level) { switch (pack.type()) { case ResourceType::FOLDER: return ShaderPackUtils::processFolder(pack, level); case ResourceType::ZIPFILE: return ShaderPackUtils::processZIP(pack, level); default: qWarning() << "Invalid type for shader pack parse task!"; return false; } } bool processFolder(ShaderPack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::FOLDER); QFileInfo shaders_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "shaders")); if (!shaders_dir_info.exists() || !shaders_dir_info.isDir()) { return false; // assets dir does not exists or isn't valid } pack.setPackFormat(ShaderPackFormat::VALID); if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } return true; // all tests passed } bool processZIP(ShaderPack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); MMCZip::ArchiveReader zip(pack.fileinfo().filePath()); if (!zip.collectFiles(false)) return false; // can't open zip file if (!zip.exists("/shaders")) { // assets dir does not exists at zip root, but shader packs // will sometimes be a zip file containing a folder with the // actual contents in it. This happens // e.g. when the shader pack is downloaded as code // from Github. so other than "/shaders", we // could also check for a "shaders" folder one level deep. QStringList files = zip.getFiles(); // the assumption here is that there is just one // folder with the "shader" subfolder. In case // there are multiple, the first one is picked. bool isShaderPresent = false; for (QString f : files) { if (f.contains("/shaders/", Qt::CaseInsensitive)) { isShaderPresent = true; break; } } if (!isShaderPresent) // assets dir does not exist. return false; } pack.setPackFormat(ShaderPackFormat::VALID); if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } return true; } bool validate(QFileInfo file) { ShaderPack sp{ file }; return ShaderPackUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid(); } } // namespace ShaderPackUtils LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) : Task(false), m_token(token), m_shader_pack(sp) {} bool LocalShaderPackParseTask::abort() { m_aborted = true; return true; } void LocalShaderPackParseTask::executeTask() { if (!ShaderPackUtils::process(m_shader_pack)) { emitFailed("this is not a shader pack"); return; } if (m_aborted) emitAborted(); else emitSucceeded(); } PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalResourceParse.h0000644000175100017510000000200715224505336025345 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include "modplatform/ResourceType.h" namespace ResourceUtils { ModPlatform::ResourceType identify(QFileInfo file); } // namespace ResourceUtils PrismLauncher-11.0.3/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h0000644000175100017510000000372715224505336026652 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "minecraft/mod/TexturePack.h" #include "tasks/Task.h" namespace TexturePackUtils { enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processPackTXT(TexturePack& pack, QByteArray&& raw_data); bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data); /// processes ONLY the pack.png (rest of the pack may be invalid) bool processPackPNG(const TexturePack& pack); /** Checks whether a file is valid as a texture pack or not. */ bool validate(QFileInfo file); } // namespace TexturePackUtils class LocalTexturePackParseTask : public Task { Q_OBJECT public: LocalTexturePackParseTask(int token, TexturePack& rp); bool canAbort() const override { return true; } bool abort() override; void executeTask() override; int token() const { return m_token; } private: int m_token; TexturePack& m_texture_pack; bool m_aborted = false; }; PrismLauncher-11.0.3/launcher/minecraft/mod/ModFolderModel.h0000644000175100017510000000702415224505336023323 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include "Mod.h" #include "ResourceFolderModel.h" #include "minecraft/Component.h" #include "minecraft/mod/Resource.h" class BaseInstance; class QFileSystemWatcher; /** * A legacy mod list. * Backed by a folder. */ class ModFolderModel : public ResourceFolderModel { Q_OBJECT public: enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, VersionColumn, DateColumn, ProviderColumn, SizeColumn, SideColumn, LoadersColumn, McVersionsColumn, ReleaseTypeColumn, RequiresColumn, RequiredByColumn, NUM_COLUMNS }; ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); virtual QString id() const override { return "mods"; } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new Mod(file); } [[nodiscard]] Task* createParseTask(Resource&) override; bool isValid(); bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action) override; bool deleteResources(const QModelIndexList& indexes) override; QModelIndexList getAffectedMods(const QModelIndexList& indexes, EnableAction action); RESOURCE_HELPERS(Mod) public: QStringList requiresList(QString id); QStringList requiredByList(QString id); private slots: void onParseSucceeded(int ticket, QString resource_id) override; void onParseFinished(); private: QHash> m_requiredBy; QHash> m_requires; }; PrismLauncher-11.0.3/launcher/minecraft/mod/ModFolderModel.cpp0000644000175100017510000004533515224505336023665 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ModFolderModel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "minecraft/Component.h" #include "minecraft/mod/Resource.h" #include "minecraft/mod/ResourceFolderModel.h" #include "minecraft/mod/tasks/LocalModParseTask.h" #include "modplatform/ModIndex.h" #include "ui/dialogs/CustomMessageBox.h" ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders", "Minecraft Versions", "Release Type", "Requires", "Required By" }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("Side"), tr("Loaders"), tr("Minecraft Versions"), tr("Release Type"), tr("Requires"), tr("Required By") }); m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER, SortType::SIZE, SortType::SIDE, SortType::LOADERS, SortType::MC_VERSIONS, SortType::RELEASE_TYPE, SortType::REQUIRES, SortType::REQUIRED_BY }; m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true, true, true }; connect(this, &ModFolderModel::parseFinished, this, &ModFolderModel::onParseFinished); } QVariant ModFolderModel::data(const QModelIndex& index, int role) const { if (!validateIndex(index)) return {}; int row = index.row(); int column = index.column(); switch (role) { case Qt::BackgroundRole: return rowBackground(row); case Qt::DisplayRole: switch (column) { case VersionColumn: { switch (at(row).type()) { case ResourceType::FOLDER: return tr("Folder"); case ResourceType::SINGLEFILE: return tr("File"); default: return at(row).version(); } } case SideColumn: { return at(row).side(); } case LoadersColumn: { return at(row).loaders(); } case McVersionsColumn: { return at(row).mcVersions(); } case ReleaseTypeColumn: { return at(row).releaseType(); } case RequiredByColumn: { return at(row).requiredByCount(); } case RequiresColumn: { return at(row).requiresCount(); } } break; case Qt::DecorationRole: { if (column == ImageColumn) { return at(row).icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } break; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } break; default: break; } // map the columns to the base equivilents QModelIndex mappedIndex; switch (column) { case ActiveColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); break; case NameColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); break; case DateColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); break; case ProviderColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); break; case SizeColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); break; } if (mappedIndex.isValid()) { return ResourceFolderModel::data(mappedIndex, role); } return {}; } QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case ActiveColumn: case NameColumn: case VersionColumn: case DateColumn: case ProviderColumn: case ImageColumn: case SideColumn: case LoadersColumn: case McVersionsColumn: case ReleaseTypeColumn: case SizeColumn: case RequiredByColumn: case RequiresColumn: return columnNames().at(section); default: return QVariant(); } case Qt::ToolTipRole: switch (section) { case ActiveColumn: return tr("Is the mod enabled?"); case NameColumn: return tr("The name of the mod."); case VersionColumn: return tr("The version of the mod."); case DateColumn: return tr("The date and time this mod was last changed (or added)."); case ProviderColumn: return tr("The source provider of the mod."); case SideColumn: return tr("On what environment the mod is running."); case LoadersColumn: return tr("The mod loader."); case McVersionsColumn: return tr("The supported minecraft versions."); case ReleaseTypeColumn: return tr("The release type."); case SizeColumn: return tr("The size of the mod."); case RequiredByColumn: return tr("For each mod, the number of other mods which depend on it."); case RequiresColumn: return tr("For each mod, the number of other mods it depends on."); default: return QVariant(); } default: return QVariant(); } } int ModFolderModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : NUM_COLUMNS; } Task* ModFolderModel::createParseTask(Resource& resource) { return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); } bool ModFolderModel::isValid() { return m_dir.exists() && m_dir.isReadable(); } void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) { auto iter = m_active_parse_tasks.constFind(ticket); if (iter == m_active_parse_tasks.constEnd()) return; int row = m_resources_index[mod_id]; auto parse_task = *iter; auto cast_task = static_cast(parse_task.get()); Q_ASSERT(cast_task->token() == ticket); auto resource = find(mod_id); auto result = cast_task->result(); if (result && resource) { auto* mod = static_cast(resource.get()); mod->finishResolvingWithDetails(std::move(result->details)); } emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn)); } Mod* findById(QSet mods, QString modId) { auto found = std::find_if(mods.begin(), mods.end(), [modId](Mod* m) { return m->mod_id() == modId; }); return found != mods.end() ? *found : nullptr; } void ModFolderModel::onParseFinished() { if (hasPendingParseTasks()) { return; } auto modsList = allMods(); auto mods = QSet(modsList.begin(), modsList.end()); m_requires.clear(); m_requiredBy.clear(); auto findByProjectID = [mods](QVariant modId, ModPlatform::ResourceProvider provider) -> Mod* { auto found = std::find_if(mods.begin(), mods.end(), [modId, provider](Mod* m) { return m->metadata() && m->metadata()->provider == provider && m->metadata()->project_id == modId; }); return found != mods.end() ? *found : nullptr; }; for (auto mod : mods) { auto id = mod->mod_id(); for (auto dep : mod->dependencies()) { auto d = findById(mods, dep); if (d) { m_requires[id] << d; m_requiredBy[d->mod_id()] << mod; } } if (mod->metadata()) { for (auto dep : mod->metadata()->dependencies) { if (dep.type == ModPlatform::DependencyType::REQUIRED) { auto d = findByProjectID(dep.addonId, mod->metadata()->provider); if (d) { m_requires[id] << d; m_requiredBy[d->mod_id()] << mod; } } } } } for (auto mod : mods) { auto id = mod->mod_id(); if (mod->requiredByCount() != m_requiredBy[id].count() || mod->requiresCount() != m_requires[id].count()) { mod->setRequiredByCount(m_requiredBy[id].count()); mod->setRequiresCount(m_requires[id].count()); int row = m_resources_index[mod->internal_id()]; emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } } } QSet collectMods(QSet mods, QHash> relation, std::set& seen, bool shouldBeEnabled) { QSet affectedList = {}; QSet needToCheck = {}; for (auto* mod : mods) { auto id = mod->mod_id(); if (!seen.contains(id)) { seen.insert(id); for (auto* affected : relation[id]) { auto affectedId = affected->mod_id(); if (findById(mods, affectedId) == nullptr && !seen.contains(affectedId)) { if (shouldBeEnabled != affected->enabled()) { affectedList << affected; } needToCheck << affected; } } } } // collect the affected mods until all of them are included in the list if (!needToCheck.isEmpty()) { affectedList += collectMods(needToCheck, relation, seen, shouldBeEnabled); } return affectedList; } QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, EnableAction action) { if (indexes.isEmpty()) return {}; QModelIndexList affectedList = {}; auto affectedModsList = selectedMods(indexes); auto affectedMods = QSet(affectedModsList.begin(), affectedModsList.end()); std::set seen; switch (action) { case EnableAction::ENABLE: { affectedMods = collectMods(affectedMods, m_requires, seen, true); break; } case EnableAction::DISABLE: { affectedMods = collectMods(affectedMods, m_requiredBy, seen, false); break; } case EnableAction::TOGGLE: { return {}; // this function should not be called with TOGGLE } } for (auto affected : affectedMods) { auto affectedId = affected->mod_id(); auto row = m_resources_index[affected->internal_id()]; affectedList << index(row, 0); } return affectedList; } bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) { if (indexes.isEmpty()) return {}; auto indexedModsList = selectedMods(indexes); auto indexedMods = QSet(indexedModsList.begin(), indexedModsList.end()); QSet toEnable = {}; QSet toDisable = {}; std::set seen; switch (action) { case EnableAction::ENABLE: { toEnable = indexedMods; break; } case EnableAction::DISABLE: { toDisable = indexedMods; break; } case EnableAction::TOGGLE: { for (auto mod : indexedMods) { if (mod->enabled()) { toDisable << mod; } else { toEnable << mod; } } break; } } auto requiredToEnable = collectMods(toEnable, m_requires, seen, true); auto requiredToDisable = collectMods(toDisable, m_requiredBy, seen, false); toDisable.removeIf([toEnable](Mod* m) { return toEnable.contains(m); }); auto toList = [this](QSet mods) { QModelIndexList list; for (auto mod : mods) { auto row = m_resources_index[mod->internal_id()]; list << index(row, 0); } return list; }; if (requiredToEnable.size() > 0 || requiredToDisable.size() > 0) { QString title; QString message; QString noButton; QString yesButton; if (requiredToEnable.size() > 0 && requiredToDisable.size() > 0) { title = tr("Confirm toggle"); message = tr("Toggling these mod(s) will cause changes to other mods.\n") + tr("%n mod(s) will be enabled\n", "", requiredToEnable.size()) + tr("%n mod(s) will be disabled\n", "", requiredToDisable.size()) + tr("Do you want to automatically apply these related changes?\nIgnoring them may break the game."); noButton = tr("Only Toggle Selected"); yesButton = tr("Toggle Required Mods"); } else if (requiredToEnable.size() > 0) { title = tr("Confirm enable"); message = tr("The enabled mod(s) require %n mod(s).\n", "", requiredToEnable.size()) + tr("Would you like to enable them as well?\nIgnoring them may break the game."); noButton = tr("Only Enable Selected"); yesButton = tr("Enable Required"); } else { title = tr("Confirm disable"); message = tr("The disabled mod(s) are required by %n mod(s).\n", "", requiredToDisable.size()) + tr("Would you like to disable them as well?\nIgnoring them may break the game."); noButton = tr("Only Disable Selected"); yesButton = tr("Disable Required"); } auto box = CustomMessageBox::selectable(nullptr, title, message, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No); box->button(QMessageBox::No)->setText(noButton); box->button(QMessageBox::Yes)->setText(yesButton); auto response = box->exec(); if (response == QMessageBox::Yes) { toEnable |= requiredToEnable; toDisable |= requiredToDisable; } else if (response == QMessageBox::Cancel) { return false; } } auto disableStatus = ResourceFolderModel::setResourceEnabled(toList(toDisable), EnableAction::DISABLE); auto enableStatus = ResourceFolderModel::setResourceEnabled(toList(toEnable), EnableAction::ENABLE); return disableStatus && enableStatus; } QStringList reqToList(QSet l) { QStringList req; for (auto m : l) { req << m->name(); } return req; } QStringList ModFolderModel::requiresList(QString id) { return reqToList(m_requires[id]); } QStringList ModFolderModel::requiredByList(QString id) { return reqToList(m_requiredBy[id]); } bool ModFolderModel::deleteResources(const QModelIndexList& indexes) { auto deleteInvalid = [](QSet& mods) { for (auto it = mods.begin(); it != mods.end();) { auto mod = *it; // the QFileInfo::exists is used instead of mod->fileinfo().exists // because the later somehow caches that the file exists if (!mod || !QFileInfo::exists(mod->fileinfo().absoluteFilePath())) { it = mods.erase(it); } else { ++it; } } }; auto rsp = ResourceFolderModel::deleteResources(indexes); for (auto mod : allMods()) { auto id = mod->mod_id(); deleteInvalid(m_requiredBy[id]); deleteInvalid(m_requires[id]); if (mod->requiredByCount() != m_requiredBy[id].count() || mod->requiresCount() != m_requires[id].count()) { mod->setRequiredByCount(m_requiredBy[id].count()); mod->setRequiresCount(m_requires[id].count()); int row = m_resources_index[mod->internal_id()]; emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn)); } } return rsp; } PrismLauncher-11.0.3/launcher/minecraft/mod/ShaderPack.h0000644000175100017510000000377415224505336022504 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "Resource.h" /* Info: * Currently For Optifine / Iris shader packs, * could be expanded to support others should they exist? * * This class and enum are mostly here as placeholders for validating * that a shaderpack exists and is in the right format, * namely that they contain a folder named 'shaders'. * * In the technical sense it would be possible to parse files like `shaders/shaders.properties` * to get information like the available profiles but this is not all that useful without more knowledge of the * shader mod used to be able to change settings. */ #include enum class ShaderPackFormat { VALID, INVALID }; class ShaderPack : public Resource { Q_OBJECT public: using Ptr = shared_qobject_ptr; ShaderPackFormat packFormat() const { return m_pack_format; } ShaderPack(QObject* parent = nullptr) : Resource(parent) {} ShaderPack(QFileInfo file_info) : Resource(file_info) {} /** Thread-safe. */ void setPackFormat(ShaderPackFormat new_format); bool valid() const override; protected: mutable QMutex m_data_lock; ShaderPackFormat m_pack_format = ShaderPackFormat::INVALID; }; PrismLauncher-11.0.3/launcher/minecraft/mod/ResourcePackFolderModel.cpp0000644000175100017510000001521515224505336025526 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ResourcePackFolderModel.h" #include #include #include "Version.h" #include "minecraft/mod/tasks/LocalDataPackParseTask.h" ResourcePackFolderModel::ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) { m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Provider", "Size" }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Provider"), tr("Size") }); m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; m_columnsHideable = { false, true, false, true, true, true, true }; } QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const { if (!validateIndex(index)) return {}; int row = index.row(); int column = index.column(); switch (role) { case Qt::BackgroundRole: return rowBackground(row); case Qt::DisplayRole: { if (column == PackFormatColumn) { const auto& resource = at(row); return resource.packFormatStr(); } break; } case Qt::DecorationRole: { if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } break; } case Qt::ToolTipRole: { if (column == PackFormatColumn) { //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); } break; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } break; } // map the columns to the base equivilents QModelIndex mappedIndex; switch (column) { case ActiveColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); break; case NameColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); break; case DateColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); break; case ProviderColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); break; case SizeColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); break; } if (mappedIndex.isValid()) { return ResourceFolderModel::data(mappedIndex, role); } return {}; } QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case ActiveColumn: case NameColumn: case PackFormatColumn: case DateColumn: case ImageColumn: case ProviderColumn: case SizeColumn: return columnNames().at(section); default: return {}; } case Qt::ToolTipRole: switch (section) { case ActiveColumn: return tr("Is the resource pack enabled?"); case NameColumn: return tr("The name of the resource pack."); case PackFormatColumn: //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); case DateColumn: return tr("The date and time this resource pack was last changed (or added)."); case ProviderColumn: return tr("The source provider of the resource pack."); case SizeColumn: return tr("The size of the resource pack."); default: return {}; } case Qt::SizeHintRole: if (section == ImageColumn) { return QSize(64, 0); } return {}; default: return {}; } } int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : NUM_COLUMNS; } Task* ResourcePackFolderModel::createParseTask(Resource& resource) { return new LocalDataPackParseTask(m_next_resolution_ticket, dynamic_cast(&resource)); } PrismLauncher-11.0.3/launcher/minecraft/ComponentUpdateTask.h0000644000175100017510000000201715224505336023635 0ustar runnerrunner#pragma once #include "minecraft/Component.h" #include "net/Mode.h" #include "tasks/Task.h" #include class PackProfile; struct ComponentUpdateTaskData; class ComponentUpdateTask : public Task { Q_OBJECT public: enum class Mode { Launch, Resolution }; public: explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list); virtual ~ComponentUpdateTask(); bool canAbort() const override; bool abort() override; Net::Mode netMode(); protected: void executeTask() override; private: void loadComponents(); /// collects components that are dependent on or dependencies of the component QList collectTreeLinked(const QString& uid); void resolveDependencies(bool checkOnly); void performUpdateActions(); void finalizeComponents(); void remoteLoadSucceeded(size_t index); void remoteLoadFailed(size_t index, const QString& msg); void checkIfAllFinished(); private: std::unique_ptr d; }; PrismLauncher-11.0.3/launcher/minecraft/ProfileUtils.h0000644000175100017510000000424215224505336022330 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Library.h" #include "VersionFile.h" namespace ProfileUtils { using PatchOrder = QStringList; /// Read and parse a OneSix format order file bool readOverrideOrders(QString path, PatchOrder& order); /// Write a OneSix format order file bool writeOverrideOrders(QString path, const PatchOrder& order); /// Parse a version file in JSON format VersionFilePtr parseJsonFile(const QFileInfo& fileInfo, bool requireOrder); /// Save a JSON file (in any format) bool saveJsonFile(const QJsonDocument& doc, const QString& filename); /// Remove LWJGL from a patch file. This is applied to all Mojang-like profile files. void removeLwjglFromPatch(VersionFilePtr patch); } // namespace ProfileUtils PrismLauncher-11.0.3/launcher/minecraft/WorldList.h0000644000175100017510000000634615224505336021641 0ustar runnerrunner/* Copyright 2015-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 #include #include #include #include #include "BaseInstance.h" #include "minecraft/World.h" class QFileSystemWatcher; class WorldList : public QAbstractListModel { Q_OBJECT public: enum Columns { NameColumn, GameModeColumn, LastPlayedColumn, SizeColumn, InfoColumn }; enum Roles { ObjectRole = Qt::UserRole + 1, FolderRole, SeedRole, NameRole, GameModeRole, LastPlayedRole, SizeRole, IconFileRole }; WorldList(const QString& dir, BaseInstance* instance); virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const { return parent.isValid() ? 0 : static_cast(size()); }; virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; virtual int columnCount(const QModelIndex& parent) const; size_t size() const { return m_worlds.size(); }; bool empty() const { return size() == 0; } World& operator[](size_t index) { return m_worlds[index]; } /// Reloads the mod list and returns true if the list changed. virtual bool update(); /// Install a world from location void installWorld(QFileInfo filename); /// Deletes the mod at the given index. virtual bool deleteWorld(int index); /// Removes the world icon, if any virtual bool resetIcon(int index); /// Deletes all the selected mods virtual bool deleteWorlds(int first, int last); /// flags, mostly to support drag&drop virtual Qt::ItemFlags flags(const QModelIndex& index) const; /// get data for drag action virtual QMimeData* mimeData(const QModelIndexList& indexes) const; /// get the supported mime types virtual QStringList mimeTypes() const; /// process data from drop action virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent); /// what drag actions do we support? virtual Qt::DropActions supportedDragActions() const; /// what drop actions do we support? virtual Qt::DropActions supportedDropActions() const; void startWatching(); void stopWatching(); virtual bool isValid(); QDir dir() const { return m_dir; } QString instDirPath() const; const QList& allWorlds() const { return m_worlds; } private slots: void directoryChanged(QString path); void loadWorldsAsync(); signals: void changed(); protected: BaseInstance* m_instance; QFileSystemWatcher* m_watcher; bool m_isWatching; QDir m_dir; QList m_worlds; }; PrismLauncher-11.0.3/launcher/minecraft/VersionFilterData.cpp0000644000175100017510000000774415224505336023641 0ustar runnerrunner#include "VersionFilterData.h" #include "ParseUtils.h" VersionFilterData g_VersionFilterData = VersionFilterData(); VersionFilterData::VersionFilterData() { // 1.3.* auto libs13 = QList{ { "argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b" }, { "guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f" }, { "asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82" } }; fmlLibsMapping["1.3.2"] = libs13; // 1.4.* auto libs14 = QList{ { "argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b" }, { "guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f" }, { "asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82" }, { "bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb" } }; fmlLibsMapping["1.4"] = libs14; fmlLibsMapping["1.4.1"] = libs14; fmlLibsMapping["1.4.2"] = libs14; fmlLibsMapping["1.4.3"] = libs14; fmlLibsMapping["1.4.4"] = libs14; fmlLibsMapping["1.4.5"] = libs14; fmlLibsMapping["1.4.6"] = libs14; fmlLibsMapping["1.4.7"] = libs14; // 1.5 fmlLibsMapping["1.5"] = QList{ { "argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51" }, { "guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a" }, { "asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58" }, { "bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65" }, { "deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8" }, { "scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85" } }; // 1.5.1 fmlLibsMapping["1.5.1"] = QList{ { "argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51" }, { "guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a" }, { "asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58" }, { "bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65" }, { "deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6" }, { "scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85" } }; // 1.5.2 fmlLibsMapping["1.5.2"] = QList{ { "argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51" }, { "guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a" }, { "asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58" }, { "bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65" }, { "deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9" }, { "scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85" } }; // don't use installers for those. forgeInstallerBlacklist = QSet({ "1.5.2" }); // FIXME: remove, used for deciding when core mods should display legacyCutoffDate = timeFromS3Time("2013-06-25T15:08:56+02:00"); lwjglWhitelist = QSet{ "net.java.jinput:jinput", "net.java.jinput:jinput-platform", "net.java.jutils:jutils", "org.lwjgl.lwjgl:lwjgl", "org.lwjgl.lwjgl:lwjgl_util", "org.lwjgl.lwjgl:lwjgl-platform" }; java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00"); java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00"); java17BeginsDate = timeFromS3Time("2021-11-16T17:04:48+00:00"); } PrismLauncher-11.0.3/launcher/minecraft/VersionFilterData.h0000644000175100017510000000171615224505336023277 0ustar runnerrunner#pragma once #include #include #include #include struct FMLlib { QString filename; QString checksum; }; struct VersionFilterData { VersionFilterData(); // mapping between minecraft versions and FML libraries required QMap> fmlLibsMapping; // set of minecraft versions for which using forge installers is blacklisted QSet forgeInstallerBlacklist; // no new versions below this date will be accepted from Mojang servers QDateTime legacyCutoffDate; // Libraries that belong to LWJGL QSet lwjglWhitelist; // release date of first version to require Java 8 (17w13a) QDateTime java8BeginsDate; // release data of first version to require Java 16 (21w19a) QDateTime java16BeginsDate; // release data of first version to require Java 17 (1.18 Pre Release 2) QDateTime java17BeginsDate; }; extern VersionFilterData g_VersionFilterData; PrismLauncher-11.0.3/launcher/minecraft/Component.cpp0000644000175100017510000003041015224505336022200 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Component.h" #include #include #include #include "Application.h" #include "FileSystem.h" #include "OneSixVersionFormat.h" #include "VersionFile.h" #include "meta/Version.h" #include "minecraft/Component.h" #include "minecraft/PackProfile.h" #include const QMap Component::KNOWN_MODLOADERS = { { "net.neoforged", { ModPlatform::NeoForge, { "net.minecraftforge", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader" } } }, { "net.minecraftforge", { ModPlatform::Forge, { "net.neoforged", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader" } } }, { "net.fabricmc.fabric-loader", { ModPlatform::Fabric, { "net.minecraftforge", "net.neoforged", "org.quiltmc.quilt-loader" } } }, { "org.quiltmc.quilt-loader", { ModPlatform::Quilt, { "net.minecraftforge", "net.neoforged", "net.fabricmc.fabric-loader" } } }, { "com.mumfrey.liteloader", { ModPlatform::LiteLoader, {} } } }; Component::Component(PackProfile* parent, const QString& uid) { assert(parent); m_parent = parent; m_uid = uid; } Component::Component(PackProfile* parent, const QString& uid, std::shared_ptr file) { assert(parent); m_parent = parent; m_file = file; m_uid = uid; m_cachedVersion = m_file->version; m_cachedName = m_file->name; m_loaded = true; } std::shared_ptr Component::getMeta() { return m_metaVersion; } void Component::applyTo(LaunchProfile* profile) { // do not apply disabled components if (!isEnabled()) { return; } auto vfile = getVersionFile(); if (vfile) { vfile->applyTo(profile, m_parent->runtimeContext()); } else { profile->applyProblemSeverity(getProblemSeverity()); } } std::shared_ptr Component::getVersionFile() const { if (m_metaVersion) { return m_metaVersion->data(); } else { return m_file; } } std::shared_ptr Component::getVersionList() const { // FIXME: what if the metadata index isn't loaded yet? if (APPLICATION->metadataIndex()->hasUid(m_uid)) { return APPLICATION->metadataIndex()->get(m_uid); } return nullptr; } int Component::getOrder() { if (m_orderOverride) return m_order; auto vfile = getVersionFile(); if (vfile) { return vfile->order; } return 0; } void Component::setOrder(int order) { m_orderOverride = true; m_order = order; } QString Component::getID() { return m_uid; } QString Component::getName() { if (!m_cachedName.isEmpty()) return m_cachedName; return m_uid; } QString Component::getVersion() { return m_cachedVersion; } QString Component::getFilename() { return m_parent->patchFilePathForUid(m_uid); } QDateTime Component::getReleaseDateTime() { if (m_metaVersion) { return m_metaVersion->time(); } auto vfile = getVersionFile(); if (vfile) { return vfile->releaseTime; } // FIXME: fake return QDateTime::currentDateTime(); } bool Component::isEnabled() { return !canBeDisabled() || !m_disabled; } bool Component::canBeDisabled() { return isRemovable() && !m_dependencyOnly; } bool Component::setEnabled(bool state) { bool intendedDisabled = !state; if (!canBeDisabled()) { intendedDisabled = false; } if (intendedDisabled != m_disabled) { m_disabled = intendedDisabled; emit dataChanged(); return true; } return false; } bool Component::isCustom() { return m_file != nullptr; } bool Component::isCustomizable() { return m_metaVersion && getVersionFile(); } bool Component::isRemovable() { return !m_important; } bool Component::isRevertible() { if (isCustom()) { if (APPLICATION->metadataIndex()->hasUid(m_uid)) { return true; } } return false; } bool Component::isMoveable() { // HACK, FIXME: this was too dumb and wouldn't follow dependency constraints anyway. For now hardcoded to 'true'. return true; } bool Component::isVersionChangeable(bool wait) { auto list = getVersionList(); if (list) { if (wait) list->waitToLoad(); return list->count() != 0; } return false; } bool Component::isKnownModloader() { auto iter = KNOWN_MODLOADERS.find(m_uid); return iter != KNOWN_MODLOADERS.cend(); } QStringList Component::knownConflictingComponents() { auto iter = KNOWN_MODLOADERS.find(m_uid); if (iter != KNOWN_MODLOADERS.cend()) { return (*iter).knownConflictingComponents; } else { return {}; } } void Component::setImportant(bool state) { if (m_important != state) { m_important = state; emit dataChanged(); } } ProblemSeverity Component::getProblemSeverity() const { auto file = getVersionFile(); if (file) { auto severity = file->getProblemSeverity(); return m_componentProblemSeverity > severity ? m_componentProblemSeverity : severity; } return ProblemSeverity::Error; } const QList Component::getProblems() const { auto file = getVersionFile(); if (file) { auto problems = file->getProblems(); problems.append(m_componentProblems); return problems; } return { { ProblemSeverity::Error, QObject::tr("Patch is not loaded yet.") } }; } void Component::addComponentProblem(ProblemSeverity severity, const QString& description) { if (severity > m_componentProblemSeverity) { m_componentProblemSeverity = severity; } m_componentProblems.append({ severity, description }); emit dataChanged(); } void Component::resetComponentProblems() { m_componentProblems.clear(); m_componentProblemSeverity = ProblemSeverity::None; emit dataChanged(); } void Component::setVersion(const QString& version) { if (version == m_version) { return; } m_version = version; if (m_loaded) { // we are loaded and potentially have state to invalidate if (m_file) { // we have a file... explicit version has been changed and there is nothing else to do. } else { // we don't have a file, therefore we are loaded with metadata m_cachedVersion = version; // see if the meta version is loaded auto metaVersion = APPLICATION->metadataIndex()->get(m_uid, version); if (metaVersion->isLoaded()) { // if yes, we can continue with that. m_metaVersion = metaVersion; } else { // if not, we need loading m_metaVersion.reset(); m_loaded = false; } updateCachedData(); } } else { // not loaded... assume it will be sorted out later by the update task } emit dataChanged(); } bool Component::customize() { if (isCustom()) { return false; } auto filename = getFilename(); if (!FS::ensureFilePathExists(filename)) { return false; } // FIXME: get rid of this try-catch. try { QSaveFile jsonFile(filename); if (!jsonFile.open(QIODevice::WriteOnly)) { return false; } auto vfile = getVersionFile(); if (!vfile) { return false; } auto document = OneSixVersionFormat::versionFileToJson(vfile); jsonFile.write(document.toJson()); if (!jsonFile.commit()) { return false; } m_file = vfile; m_metaVersion.reset(); emit dataChanged(); } catch (const Exception& error) { qWarning() << "Version could not be loaded:" << error.cause(); } return true; } bool Component::revert() { if (!isCustom()) { // already not custom return true; } auto filename = getFilename(); bool result = true; // just kill the file and reload if (QFile::exists(filename)) { result = FS::deletePath(filename); } if (result) { // file gone... m_file.reset(); // check local cache for metadata... auto version = APPLICATION->metadataIndex()->get(m_uid, m_version); if (version->isLoaded()) { m_metaVersion = version; } else { m_metaVersion.reset(); m_loaded = false; } emit dataChanged(); } return result; } /** * deep inspecting compare for requirement sets * By default, only uids are compared for set operations. * This compares all fields of the Require structs in the sets. */ static bool deepCompare(const std::set& a, const std::set& b) { // NOTE: this needs to be rewritten if the type of Meta::RequireSet changes if (a.size() != b.size()) { return false; } for (const auto& reqA : a) { const auto& iter2 = b.find(reqA); if (iter2 == b.cend()) { return false; } const auto& reqB = *iter2; if (!reqA.deepEquals(reqB)) { return false; } } return true; } void Component::updateCachedData() { auto file = getVersionFile(); if (file) { bool changed = false; if (m_cachedName != file->name) { m_cachedName = file->name; changed = true; } if (m_cachedVersion != file->version) { m_cachedVersion = file->version; changed = true; } if (m_cachedVolatile != file->m_volatile) { m_cachedVolatile = file->m_volatile; changed = true; } if (!deepCompare(m_cachedRequires, file->m_requires)) { m_cachedRequires = file->m_requires; changed = true; } if (!deepCompare(m_cachedConflicts, file->conflicts)) { m_cachedConflicts = file->conflicts; changed = true; } if (changed) { emit dataChanged(); } } else { // in case we removed all the metadata m_cachedRequires.clear(); m_cachedConflicts.clear(); emit dataChanged(); } } void Component::waitLoadMeta() { if (!m_loaded) { if (!m_metaVersion || !m_metaVersion->isLoaded()) { // wait for the loaded version from meta m_metaVersion = APPLICATION->metadataIndex()->getLoadedVersion(m_uid, m_version); } m_loaded = true; updateCachedData(); } } void Component::setUpdateAction(const UpdateAction& action) { m_updateAction = action; } UpdateAction Component::getUpdateAction() { return m_updateAction; } void Component::clearUpdateAction() { m_updateAction = UpdateAction{ UpdateActionNone{} }; } QDebug operator<<(QDebug d, const Component& comp) { QDebugStateSaver saver(d); d.nospace() << "Component(" << comp.m_uid << " : " << comp.m_cachedVersion << ")"; return d; } PrismLauncher-11.0.3/launcher/minecraft/PackProfile.cpp0000644000175100017510000010541215224505336022442 0ustar runnerrunner// SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022-2023 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include #include #include #include #include #include #include "Application.h" #include "Exception.h" #include "FileSystem.h" #include "Json.h" #include "meta/Index.h" #include "meta/JsonFormat.h" #include "minecraft/Component.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/OneSixVersionFormat.h" #include "minecraft/ProfileUtils.h" #include "ComponentUpdateTask.h" #include "PackProfile.h" #include "PackProfile_p.h" #include "modplatform/ModIndex.h" #include "minecraft/Logging.h" #include "ui/dialogs/CustomMessageBox.h" PackProfile::PackProfile(MinecraftInstance* instance) : QAbstractListModel() { d.reset(new PackProfileData); d->m_instance = instance; d->m_saveTimer.setSingleShot(true); d->m_saveTimer.setInterval(5000); d->interactionDisabled = instance->isRunning(); connect(d->m_instance, &BaseInstance::runningStatusChanged, this, &PackProfile::disableInteraction); connect(&d->m_saveTimer, &QTimer::timeout, this, &PackProfile::save_internal); } PackProfile::~PackProfile() { saveNow(); } // BEGIN: component file format static const int currentComponentsFileVersion = 1; static QJsonObject componentToJsonV1(ComponentPtr component) { QJsonObject obj; // critical obj.insert("uid", component->m_uid); if (!component->m_version.isEmpty()) { obj.insert("version", component->m_version); } if (component->m_dependencyOnly) { obj.insert("dependencyOnly", true); } if (component->m_important) { obj.insert("important", true); } if (component->m_disabled) { obj.insert("disabled", true); } // cached if (!component->m_cachedVersion.isEmpty()) { obj.insert("cachedVersion", component->m_cachedVersion); } if (!component->m_cachedName.isEmpty()) { obj.insert("cachedName", component->m_cachedName); } Meta::serializeRequires(obj, &component->m_cachedRequires, "cachedRequires"); Meta::serializeRequires(obj, &component->m_cachedConflicts, "cachedConflicts"); if (component->m_cachedVolatile) { obj.insert("cachedVolatile", true); } return obj; } static ComponentPtr componentFromJsonV1(PackProfile* parent, const QString& componentJsonPattern, const QJsonObject& obj) { // critical auto uid = Json::requireString(obj.value("uid")); auto filePath = componentJsonPattern.arg(uid); auto component = makeShared(parent, uid); component->m_version = obj.value("version").toString(); component->m_dependencyOnly = obj.value("dependencyOnly").toBool(); component->m_important = obj.value("important").toBool(); // cached // TODO @RESILIENCE: ignore invalid values/structure here? component->m_cachedVersion = obj.value("cachedVersion").toString(); component->m_cachedName = obj.value("cachedName").toString(); Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires"); Meta::parseRequires(obj, &component->m_cachedConflicts, "cachedConflicts"); component->m_cachedVolatile = obj.value("volatile").toBool(); bool disabled = obj.value("disabled").toBool(); component->setEnabled(!disabled); return component; } // Save the given component container data to a file static bool savePackProfile(const QString& filename, const ComponentContainer& container) { QJsonObject obj; obj.insert("formatVersion", currentComponentsFileVersion); QJsonArray orderArray; for (auto component : container) { orderArray.append(componentToJsonV1(component)); } obj.insert("components", orderArray); QSaveFile outFile(filename); if (!outFile.open(QFile::WriteOnly)) { qCCritical(instanceProfileC) << "Couldn't open" << outFile.fileName() << "for writing:" << outFile.errorString(); return false; } auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented); if (outFile.write(data) != data.size()) { qCCritical(instanceProfileC) << "Couldn't write all the data into" << outFile.fileName() << "because:" << outFile.errorString(); return false; } if (!outFile.commit()) { qCCritical(instanceProfileC) << "Couldn't save" << outFile.fileName() << "because:" << outFile.errorString(); return false; } return true; } // Read the given file into component containers static PackProfile::Result loadPackProfile(PackProfile* parent, const QString& filename, const QString& componentJsonPattern, ComponentContainer& container) { QFile componentsFile(filename); if (!componentsFile.exists()) { 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)) { 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 PackProfile::Result::Error(message); } // and it's valid JSON QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error); if (error.error != QJsonParseError::NoError) { 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 PackProfile::Result::Error(message); } // and then read it and process it if all above is true. try { auto obj = Json::requireObject(doc); // check order file version. auto version = Json::requireInteger(obj.value("formatVersion")); if (version != currentComponentsFileVersion) { throw JSONValidationError(QObject::tr("Invalid component file version, expected %1").arg(currentComponentsFileVersion)); } auto orderArray = Json::requireArray(obj.value("components")); for (auto item : orderArray) { auto comp_obj = Json::requireObject(item, "Component must be an object."); container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj)); } } catch ([[maybe_unused]] const JSONValidationError& err) { 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 PackProfile::Result::Error(message); } return PackProfile::Result::Success(); } // END: component file format // BEGIN: save/load logic void PackProfile::saveNow() { if (saveIsScheduled() && save_internal()) { d->m_saveTimer.stop(); } } bool PackProfile::saveIsScheduled() const { return d->dirty; } void PackProfile::buildingFromScratch() { d->loaded = true; d->dirty = true; } void PackProfile::scheduleSave() { if (!d->loaded) { qDebug() << d->m_instance->name() << "|" << "Component list should never save if it didn't successfully load"; return; } if (!d->dirty) { d->dirty = true; qDebug() << d->m_instance->name() << "|" << "Component list save is scheduled"; } d->m_saveTimer.start(); } RuntimeContext PackProfile::runtimeContext() { return d->m_instance->runtimeContext(); } QString PackProfile::componentsFilePath() const { return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json"); } QString PackProfile::patchesPattern() const { return FS::PathCombine(d->m_instance->instanceRoot(), "patches", "%1.json"); } QString PackProfile::patchFilePathForUid(const QString& uid) const { return patchesPattern().arg(uid); } bool PackProfile::save_internal() { qDebug() << d->m_instance->name() << "|" << "Component list save performed now"; auto filename = componentsFilePath(); if (savePackProfile(filename, d->components)) { d->dirty = false; return true; } return false; } PackProfile::Result PackProfile::load() { auto filename = componentsFilePath(); // load the new component list and swap it with the current one... ComponentContainer newComponents; if (auto result = loadPackProfile(this, filename, patchesPattern(), newComponents); !result) { qCritical() << d->m_instance->name() << "|" << "Failed to load the component config"; 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(); } 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) { if (d->m_updateTask->netMode() == netmode) { return Result::Success(); } // https://github.com/PrismLauncher/PrismLauncher/issues/5209 // FIXME: HACK HACK HACK disconnect(d->m_updateTask.get(), &ComponentUpdateTask::aborted, nullptr, nullptr); d->m_updateTask->abort(); d->m_updateTask.reset(); } // flush any scheduled saves to not lose state saveNow(); // FIXME: differentiate when a reapply is required by propagating state from components invalidateLaunchProfile(); if (auto result = load(); !result) { return result; } resolve(netmode); return Result::Success(); } Task::Ptr PackProfile::getCurrentTask() { return d->m_updateTask; } void PackProfile::resolve(Net::Mode netmode) { auto updateTask = new ComponentUpdateTask(ComponentUpdateTask::Mode::Resolution, netmode, this); d->m_updateTask.reset(updateTask); connect(updateTask, &ComponentUpdateTask::succeeded, this, &PackProfile::updateSucceeded); connect(updateTask, &ComponentUpdateTask::failed, this, &PackProfile::updateFailed); connect(updateTask, &ComponentUpdateTask::aborted, this, [this] { updateFailed(tr("Aborted")); }); d->m_updateTask->start(); } void PackProfile::updateSucceeded() { qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Component list update/resolve task succeeded"; d->m_updateTask.reset(); invalidateLaunchProfile(); } void PackProfile::updateFailed(const QString& error) { qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Component list update/resolve task failed. Reason:" << error; d->m_updateTask.reset(); invalidateLaunchProfile(); } // END: save/load void PackProfile::appendComponent(ComponentPtr component) { insertComponent(d->components.size(), component); } void PackProfile::insertComponent(size_t index, ComponentPtr component) { auto id = component->getID(); if (id.isEmpty()) { qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Attempt to add a component with empty ID!"; return; } if (d->componentIndex.contains(id)) { qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Attempt to add a component that is already present!"; return; } beginInsertRows(QModelIndex(), static_cast(index), static_cast(index)); d->components.insert(index, component); d->componentIndex[id] = component; endInsertRows(); connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); scheduleSave(); } void PackProfile::componentDataChanged() { auto objPtr = qobject_cast(sender()); if (!objPtr) { qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "PackProfile got dataChanged signal from a non-Component!"; return; } if (objPtr->getID() == "net.minecraft") { emit minecraftChanged(); } // figure out which one is it... in a seriously dumb way. int index = 0; for (auto component : d->components) { if (component.get() == objPtr) { emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); scheduleSave(); return; } index++; } qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "PackProfile got dataChanged signal from a Component which does not belong to it!"; } bool PackProfile::remove(const int index) { auto patch = getComponent(index); if (!patch->isRemovable()) { qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "is non-removable"; return false; } if (!removeComponent_internal(patch)) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "could not be removed"; return false; } beginRemoveRows(QModelIndex(), index, index); d->components.removeAt(index); d->componentIndex.remove(patch->getID()); endRemoveRows(); invalidateLaunchProfile(); scheduleSave(); return true; } bool PackProfile::remove(const QString& id) { int i = 0; for (auto patch : d->components) { if (patch->getID() == id) { return remove(i); } i++; } return false; } bool PackProfile::customize(int index) { auto patch = getComponent(index); if (!patch->isCustomizable()) { qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "is not customizable"; return false; } if (!patch->customize()) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "could not be customized"; return false; } invalidateLaunchProfile(); scheduleSave(); return true; } bool PackProfile::revertToBase(int index) { auto patch = getComponent(index); if (!patch->isRevertible()) { qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "is not revertible"; return false; } if (!patch->revert()) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "could not be reverted"; return false; } invalidateLaunchProfile(); scheduleSave(); return true; } ComponentPtr PackProfile::getComponent(const QString& id) { auto iter = d->componentIndex.find(id); if (iter == d->componentIndex.end()) { return nullptr; } return (*iter); } ComponentPtr PackProfile::getComponent(size_t index) { if (index >= static_cast(d->components.size())) { return nullptr; } return d->components[index]; } QVariant PackProfile::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); int row = index.row(); int column = index.column(); if (row < 0 || row >= d->components.size()) return QVariant(); auto patch = d->components.at(row); switch (role) { case Qt::CheckStateRole: { if (column == NameColumn) return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; return QVariant(); } case Qt::DisplayRole: { switch (column) { case NameColumn: return patch->getName(); case VersionColumn: { if (patch->isCustom()) { return QString("%1 (Custom)").arg(patch->getVersion()); } else { return patch->getVersion(); } } default: return QVariant(); } } case Qt::DecorationRole: { if (column == NameColumn) { auto severity = patch->getProblemSeverity(); switch (severity) { case ProblemSeverity::Warning: return "warning"; case ProblemSeverity::Error: return "error"; default: return QVariant(); } } return QVariant(); } } return QVariant(); } bool PackProfile::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) { if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index.parent())) { return false; } if (role == Qt::CheckStateRole) { auto component = d->components[index.row()]; if (component->setEnabled(!component->isEnabled())) { return true; } } return false; } QVariant PackProfile::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (role == Qt::DisplayRole) { switch (section) { case NameColumn: return tr("Name"); case VersionColumn: return tr("Version"); default: return QVariant(); } } } return QVariant(); } // FIXME: zero precision mess Qt::ItemFlags PackProfile::flags(const QModelIndex& index) const { if (!index.isValid()) { return Qt::NoItemFlags; } Qt::ItemFlags outFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; int row = index.row(); if (row < 0 || row >= d->components.size()) { return Qt::NoItemFlags; } auto patch = d->components.at(row); // TODO: this will need fine-tuning later... if (patch->canBeDisabled() && !d->interactionDisabled) { outFlags |= Qt::ItemIsUserCheckable; } return outFlags; } int PackProfile::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : d->components.size(); } int PackProfile::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : NUM_COLUMNS; } void PackProfile::move(const int index, const MoveDirection direction) { int theirIndex; if (direction == MoveUp) { theirIndex = index - 1; } else { theirIndex = index + 1; } if (index < 0 || index >= d->components.size()) return; if (theirIndex >= rowCount()) theirIndex = rowCount() - 1; if (theirIndex == -1) theirIndex = rowCount() - 1; if (index == theirIndex) return; int togap = theirIndex > index ? theirIndex + 1 : theirIndex; auto from = getComponent(index); auto to = getComponent(theirIndex); if (!from || !to || !to->isMoveable() || !from->isMoveable()) { return; } beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); d->components.swapItemsAt(index, theirIndex); endMoveRows(); invalidateLaunchProfile(); scheduleSave(); } void PackProfile::invalidateLaunchProfile() { d->m_profile.reset(); } void PackProfile::installJarMods(QStringList selectedFiles) { // FIXME: get rid of _internal installJarMods_internal(selectedFiles); } void PackProfile::installCustomJar(QString selectedFile) { // FIXME: get rid of _internal installCustomJar_internal(selectedFile); } bool PackProfile::installComponents(QStringList selectedFiles) { const QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if (!FS::ensureFolderPathExists(patchDir)) return false; bool result = true; for (const QString& source : selectedFiles) { const QFileInfo sourceInfo(source); auto versionFile = ProfileUtils::parseJsonFile(sourceInfo, false); const QString target = FS::PathCombine(patchDir, versionFile->uid + ".json"); if (!QFile::copy(source, target)) { qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Component" << source << "could not be copied to target" << target; result = false; continue; } appendComponent(makeShared(this, versionFile->uid, versionFile)); } scheduleSave(); invalidateLaunchProfile(); return result; } void PackProfile::installAgents(QStringList selectedFiles) { // FIXME: get rid of _internal installAgents_internal(selectedFiles); } bool PackProfile::installEmpty(const QString& uid, const QString& name) { QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if (!FS::ensureFolderPathExists(patchDir)) { return false; } auto f = std::make_shared(); f->name = name; f->uid = uid; f->version = "1"; QString patchFileName = FS::PathCombine(patchDir, uid + ".json"); QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Error opening" << file.fileName() << "for reading:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); appendComponent(makeShared(this, f->uid, f)); scheduleSave(); invalidateLaunchProfile(); return true; } bool PackProfile::removeComponent_internal(ComponentPtr patch) { bool ok = true; // first, remove the patch file. this ensures it's not used anymore auto fileName = patch->getFilename(); if (fileName.size()) { QFile patchFile(fileName); if (patchFile.exists() && !patchFile.remove()) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "File" << fileName << "could not be removed because:" << patchFile.errorString(); return false; } } // FIXME: we need a generic way of removing local resources, not just jar mods... auto preRemoveJarMod = [this](LibraryPtr jarMod) -> bool { if (!jarMod->isLocal()) { return true; } QStringList jar, temp1, temp2, temp3; jarMod->getApplicableFiles(d->m_instance->runtimeContext(), jar, temp1, temp2, temp3, d->m_instance->jarmodsPath().absolutePath()); QFileInfo finfo(jar[0]); if (finfo.exists()) { QFile jarModFile(jar[0]); if (!jarModFile.remove()) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "File" << jar[0] << "could not be removed because:" << jarModFile.errorString(); return false; } return true; } return true; }; auto vFile = patch->getVersionFile(); if (vFile) { auto& jarMods = vFile->jarMods; for (auto& jarmod : jarMods) { ok &= preRemoveJarMod(jarmod); } } return ok; } bool PackProfile::installJarMods_internal(QStringList filepaths) { QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if (!FS::ensureFolderPathExists(patchDir)) { return false; } if (!FS::ensureFolderPathExists(d->m_instance->jarModsDir())) { return false; } for (auto filepath : filepaths) { QFileInfo sourceInfo(filepath); QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); QString target_filename = id + ".jar"; QString target_id = "custom.jarmod." + id; QString target_name = sourceInfo.completeBaseName() + " (jar mod)"; QString finalPath = FS::PathCombine(d->m_instance->jarModsDir(), target_filename); QFileInfo targetInfo(finalPath); Q_ASSERT(!targetInfo.exists()); if (!QFile::copy(sourceInfo.absoluteFilePath(), QFileInfo(finalPath).absoluteFilePath())) { return false; } auto f = std::make_shared(); auto jarMod = std::make_shared(); jarMod->setRawName(GradleSpecifier("custom.jarmods:" + id + ":1")); jarMod->setFilename(target_filename); jarMod->setDisplayName(sourceInfo.completeBaseName()); jarMod->setHint("local"); f->jarMods.append(jarMod); f->name = target_name; f->uid = target_id; QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Error opening" << file.fileName() << "for reading:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); appendComponent(makeShared(this, f->uid, f)); } scheduleSave(); invalidateLaunchProfile(); return true; } bool PackProfile::installCustomJar_internal(QString filepath) { QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if (!FS::ensureFolderPathExists(patchDir)) { return false; } QString libDir = d->m_instance->getLocalLibraryPath(); if (!FS::ensureFolderPathExists(libDir)) { return false; } auto specifier = GradleSpecifier("custom:customjar:1"); QFileInfo sourceInfo(filepath); QString target_filename = specifier.getFileName(); QString target_id = specifier.artifactId(); QString target_name = sourceInfo.completeBaseName() + " (custom jar)"; QString finalPath = FS::PathCombine(libDir, target_filename); QFileInfo jarInfo(finalPath); if (jarInfo.exists()) { if (!FS::deletePath(finalPath)) { return false; } } if (!QFile::copy(filepath, finalPath)) { return false; } auto f = std::make_shared(); auto jarMod = std::make_shared(); jarMod->setRawName(specifier); jarMod->setDisplayName(sourceInfo.completeBaseName()); jarMod->setHint("local"); f->mainJar = jarMod; f->name = target_name; f->uid = target_id; QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Error opening" << file.fileName() << "for reading:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); appendComponent(makeShared(this, f->uid, f)); scheduleSave(); invalidateLaunchProfile(); return true; } bool PackProfile::installAgents_internal(QStringList filepaths) { // FIXME code duplication const QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if (!FS::ensureFolderPathExists(patchDir)) return false; const QString libDir = d->m_instance->getLocalLibraryPath(); if (!FS::ensureFolderPathExists(libDir)) return false; for (const QString& source : filepaths) { const QFileInfo sourceInfo(source); const QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); const QString targetBaseName = id + ".jar"; const QString targetId = "custom.agent." + id; const QString targetName = sourceInfo.completeBaseName() + " (agent)"; const QString target = FS::PathCombine(d->m_instance->getLocalLibraryPath(), targetBaseName); const QFileInfo targetInfo(target); Q_ASSERT(!targetInfo.exists()); if (!QFile::copy(source, target)) return false; auto versionFile = std::make_shared(); auto agent = std::make_shared(); agent->setRawName("custom.agents:" + id + ":1"); agent->setFilename(targetBaseName); agent->setDisplayName(sourceInfo.completeBaseName()); agent->setHint("local"); versionFile->agents.append(Agent{agent, QString()}); versionFile->name = targetName; versionFile->uid = targetId; QFile patchFile(FS::PathCombine(patchDir, targetId + ".json")); if (!patchFile.open(QFile::WriteOnly)) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Error opening" << patchFile.fileName() << "for reading:" << patchFile.errorString(); return false; } patchFile.write(OneSixVersionFormat::versionFileToJson(versionFile).toJson()); patchFile.close(); appendComponent(makeShared(this, versionFile->uid, versionFile)); } scheduleSave(); invalidateLaunchProfile(); return true; } std::shared_ptr PackProfile::getProfile() const { if (!d->m_profile) { try { auto profile = std::make_shared(); for (auto file : d->components) { qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Applying" << file->getID() << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD"); file->applyTo(profile.get()); } d->m_profile = profile; } catch (const Exception& error) { qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Couldn't apply profile patches because:" << error.cause(); } } return d->m_profile; } bool PackProfile::setComponentVersion(const QString& uid, const QString& version, bool important) { auto iter = d->componentIndex.find(uid); if (iter != d->componentIndex.end()) { ComponentPtr component = *iter; // set existing if (component->revert()) { // set new version auto oldVersion = component->getVersion(); component->setVersion(version); component->setImportant(important); if (important) { component->setUpdateAction(UpdateAction{ UpdateActionImportantChanged{ oldVersion } }); resolve(Net::Mode::Online); } return true; } return false; } else { // add new auto component = makeShared(this, uid); component->m_version = version; component->m_important = important; appendComponent(component); return true; } } QString PackProfile::getComponentVersion(const QString& uid) const { const auto iter = d->componentIndex.find(uid); if (iter != d->componentIndex.end()) { return (*iter)->getVersion(); } return QString(); } void PackProfile::disableInteraction(bool disable) { if (d->interactionDisabled != disable) { d->interactionDisabled = disable; auto size = d->components.size(); if (size) { emit dataChanged(index(0), index(size - 1)); } } } std::optional PackProfile::getModLoaders() { ModPlatform::ModLoaderTypes result; bool has_any_loader = false; QMapIterator i(Component::KNOWN_MODLOADERS); while (i.hasNext()) { i.next(); if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled()) { result |= i.value().type; has_any_loader = true; } } if (!has_any_loader) return {}; return result; } std::optional PackProfile::getSupportedModLoaders() { auto loadersOpt = getModLoaders(); if (!loadersOpt.has_value()) return loadersOpt; auto loaders = loadersOpt.value(); // TODO: remove this or add version condition once Quilt drops official Fabric support if (loaders & ModPlatform::Quilt) loaders |= ModPlatform::Fabric; if (getComponentVersion("net.minecraft") == "1.20.1" && (loaders & ModPlatform::NeoForge)) loaders |= ModPlatform::Forge; return loaders; } QList PackProfile::getModLoadersList() { QList result; for (auto c : d->components) { if (c->isEnabled() && Component::KNOWN_MODLOADERS.contains(c->getID())) { result.append(Component::KNOWN_MODLOADERS[c->getID()].type); } } // TODO: remove this or add version condition once Quilt drops official Fabric support if (result.contains(ModPlatform::Quilt) && !result.contains(ModPlatform::Fabric)) { result.append(ModPlatform::Fabric); } if (getComponentVersion("net.minecraft") == "1.20.1" && result.contains(ModPlatform::NeoForge) && !result.contains(ModPlatform::Forge)) { result.append(ModPlatform::Forge); } return result; } PrismLauncher-11.0.3/launcher/minecraft/AssetsUtils.h0000644000175100017510000000271615224505336022176 0ustar runnerrunner/* 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 #include #include "net/NetJob.h" #include "net/NetRequest.h" struct AssetObject { QString getRelPath(); QUrl getUrl(); QString getLocalPath(); Net::NetRequest::Ptr getDownloadAction(); QString hash; qint64 size; }; struct AssetsIndex { NetJob::Ptr getDownloadJob(); QString id; QMap objects; bool isVirtual = false; bool mapToResources = false; }; /// FIXME: this is absolutely horrendous. REDO!!!! namespace AssetsUtils { bool loadAssetsIndexJson(const QString& id, const QString& file, AssetsIndex& index); QDir getAssetsDir(const QString& assetsId, const QString& resourcesFolder); /// Reconstruct a virtual assets folder for the given assets ID and return the folder bool reconstructAssets(QString assetsId, QString resourcesFolder); } // namespace AssetsUtils PrismLauncher-11.0.3/launcher/minecraft/GradleSpecifier.h0000644000175100017510000001152015224505336022734 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include struct GradleSpecifier { GradleSpecifier() { m_valid = false; } GradleSpecifier(const QString& value) { /* org.gradle.test.classifiers : service : 1.0 : jdk15 @ jar 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar" 1 "org.gradle.test.classifiers" 2 "service" 3 "1.0" 4 "jdk15" 5 "jar" */ static const QRegularExpression s_matcher( QRegularExpression::anchoredPattern("([^:@]+):([^:@]+):([^:@]+)" "(?::([^:@]+))?" "(?:@([^:@]+))?")); QRegularExpressionMatch match = s_matcher.match(value); m_valid = match.hasMatch(); if (!m_valid) { m_invalidValue = value; return; } auto elements = match.captured(); m_groupId = match.captured(1); m_artifactId = match.captured(2); m_version = match.captured(3); m_classifier = match.captured(4); if (match.lastCapturedIndex() >= 5) { m_extension = match.captured(5); } } QString serialize() const { if (!m_valid) { return m_invalidValue; } QString retval = m_groupId + ":" + m_artifactId + ":" + m_version; if (!m_classifier.isEmpty()) { retval += ":" + m_classifier; } if (m_extension.has_value()) { retval += "@" + m_extension.value(); } return retval; } QString getFileName() const { if (!m_valid) { return QString(); } QString filename = m_artifactId + '-' + m_version; if (!m_classifier.isEmpty()) { filename += "-" + m_classifier; } filename += "." + m_extension.value_or("jar"); return filename; } QString toPath(const QString& filenameOverride = QString()) const { if (!m_valid) { return QString(); } QString filename; if (filenameOverride.isEmpty()) { filename = getFileName(); } else { filename = filenameOverride; } QString path = m_groupId; path.replace('.', '/'); path += '/' + m_artifactId + '/' + m_version + '/' + filename; return path; } inline bool valid() const { return m_valid; } inline QString version() const { return m_version; } inline QString groupId() const { return m_groupId; } inline QString artifactId() const { return m_artifactId; } inline void setClassifier(const QString& classifier) { m_classifier = classifier; } inline QString classifier() const { return m_classifier; } inline std::optional extension() const { return m_extension; } inline QString artifactPrefix() const { return m_groupId + ":" + m_artifactId; } bool matchName(const GradleSpecifier& other) const { return other.artifactId() == artifactId() && other.groupId() == groupId() && other.classifier() == classifier(); } bool operator ==(const GradleSpecifier &other) const = default; private: QString m_invalidValue; QString m_groupId; QString m_artifactId; QString m_version; QString m_classifier; std::optional m_extension; bool m_valid = false; }; PrismLauncher-11.0.3/launcher/minecraft/Logging.cpp0000644000175100017510000000170715224505336021633 0ustar runnerrunner // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "minecraft/Logging.h" Q_LOGGING_CATEGORY(instanceProfileC, "launcher.instance.profile") Q_LOGGING_CATEGORY(instanceProfileResolveC, "launcher.instance.profile.resolve") PrismLauncher-11.0.3/launcher/minecraft/MinecraftInstance.cpp0000644000175100017510000013506215224505336023644 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Jamie Mansfield * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "MinecraftInstance.h" #include "Application.h" #include "BuildConfig.h" #include "Json.h" #include "QObjectPtr.h" #include "settings/Setting.h" #include "settings/SettingsObject.h" #include "FileSystem.h" #include "MMCTime.h" #include "java/JavaVersion.h" #include "launch/LaunchTask.h" #include "launch/TaskStepWrapper.h" #include "launch/steps/CheckJava.h" #include "launch/steps/LookupServerAddress.h" #include "launch/steps/PostLaunchCommand.h" #include "launch/steps/PreLaunchCommand.h" #include "launch/steps/QuitAfterGameStop.h" #include "launch/steps/TextPrint.h" #include "minecraft/launch/AutoInstallJava.h" #include "minecraft/launch/ClaimAccount.h" #include "minecraft/launch/CreateGameFolders.h" #include "minecraft/launch/EnsureAvailableMemory.h" #include "minecraft/launch/EnsureOfflineLibraries.h" #include "minecraft/launch/ExtractNatives.h" #include "minecraft/launch/LauncherPartLaunch.h" #include "minecraft/launch/ModMinecraftJar.h" #include "minecraft/launch/PrintInstanceInfo.h" #include "minecraft/launch/ReconstructAssets.h" #include "minecraft/launch/ScanModFolders.h" #include "minecraft/launch/VerifyJavaInstall.h" #include "minecraft/update/AssetUpdateTask.h" #include "minecraft/update/FoldersTask.h" #include "minecraft/update/LegacyFMLLibrariesTask.h" #include "minecraft/update/LibrariesTask.h" #include "java/JavaUtils.h" #include "icons/IconList.h" #include "mod/ModFolderModel.h" #include "mod/ResourcePackFolderModel.h" #include "mod/ShaderPackFolderModel.h" #include "mod/TexturePackFolderModel.h" #include "WorldList.h" #include "AssetsUtils.h" #include "MinecraftLoadAndCheck.h" #include "PackProfile.h" #include "tools/BaseProfiler.h" #include #include #include #include #include #ifdef Q_OS_LINUX #include "LibraryUtils.h" #endif #ifdef WITH_QTDBUS #include #endif #define IBUS "@im=ibus" [[maybe_unused]] static bool switcherooSetupGPU(QProcessEnvironment& env) { #ifdef WITH_QTDBUS if (!QDBusConnection::systemBus().isConnected()) return false; QDBusInterface switcheroo("net.hadess.SwitcherooControl", "/net/hadess/SwitcherooControl", "org.freedesktop.DBus.Properties", QDBusConnection::systemBus()); if (!switcheroo.isValid()) return false; QDBusReply reply = switcheroo.call(QStringLiteral("Get"), QStringLiteral("net.hadess.SwitcherooControl"), QStringLiteral("GPUs")); if (!reply.isValid()) return false; QDBusArgument arg = qvariant_cast(reply.value().variant()); QList gpus; arg >> gpus; for (const auto& gpu : gpus) { QString name = qvariant_cast(gpu[QStringLiteral("Name")]); bool defaultGpu = qvariant_cast(gpu[QStringLiteral("Default")]); bool discrete = qvariant_cast(gpu.value(QStringLiteral("Discrete"), !defaultGpu)); if (discrete) { QStringList envList = qvariant_cast(gpu[QStringLiteral("Environment")]); for (int i = 0; i + 1 < envList.size(); i += 2) { env.insert(envList[i], envList[i + 1]); } return true; } } #endif return false; } // all of this because keeping things compatible with deprecated old settings // if either of the settings {a, b} is true, this also resolves to true class OrSetting : public Setting { Q_OBJECT public: OrSetting(QString id, std::shared_ptr a, std::shared_ptr b) : Setting({ id }, false), m_a(a), m_b(b) {} virtual QVariant get() const { bool a = m_a->get().toBool(); bool b = m_b->get().toBool(); return a || b; } virtual void reset() {} virtual void set(QVariant value) {} private: std::shared_ptr m_a; std::shared_ptr m_b; }; MinecraftInstance::MinecraftInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir) : BaseInstance(globalSettings, std::move(settings), rootDir) { m_components.reset(new PackProfile(this)); } MinecraftInstance::~MinecraftInstance() {} void MinecraftInstance::saveNow() { m_components->saveNow(); } void MinecraftInstance::loadSpecificSettings() { if (isSpecificSettingsLoaded()) return; // Java Settings auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false); auto argsOverride = m_settings->registerSetting("OverrideJavaArgs", false); m_settings->registerSetting("AutomaticJava", false); if (auto global_settings = globalSettings()) { m_settings->registerOverride(global_settings->getSetting("JavaPath"), locationOverride); m_settings->registerOverride(global_settings->getSetting("JvmArgs"), argsOverride); m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), locationOverride); // special! m_settings->registerPassthrough(global_settings->getSetting("JavaSignature"), locationOverride); m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), locationOverride); m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), locationOverride); m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), locationOverride); m_settings->registerPassthrough(global_settings->getSetting("JavaVendor"), locationOverride); // Window Size auto windowSetting = m_settings->registerSetting("OverrideWindow", false); m_settings->registerOverride(global_settings->getSetting("LaunchMaximized"), windowSetting); m_settings->registerOverride(global_settings->getSetting("MinecraftWinWidth"), windowSetting); m_settings->registerOverride(global_settings->getSetting("MinecraftWinHeight"), windowSetting); // Memory auto memorySetting = m_settings->registerSetting("OverrideMemory", false); m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting); m_settings->registerOverride(global_settings->getSetting("LowMemWarning"), memorySetting); // Native library workarounds auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false); m_settings->registerOverride(global_settings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(global_settings->getSetting("CustomOpenALPath"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(global_settings->getSetting("CustomGLFWPath"), nativeLibraryWorkaroundsOverride); // Performance related options auto performanceOverride = m_settings->registerSetting("OverridePerformance", false); m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("UseZink"), performanceOverride); // Miscellaneous auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false); m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride); m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride); // Legacy-related options auto legacySettings = m_settings->registerSetting("OverrideLegacySettings", false); m_settings->registerOverride(global_settings->getSetting("OnlineFixes"), legacySettings); auto envSetting = m_settings->registerSetting("OverrideEnv", false); m_settings->registerOverride(global_settings->getSetting("Env"), envSetting); m_settings->set("InstanceType", "OneSix"); } // Join server on launch, this does not have a global override m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunchAddress", ""); m_settings->registerSetting("JoinWorldOnLaunch", ""); // Use account for instance, this does not have a global override m_settings->registerSetting("UseAccountForInstance", false); m_settings->registerSetting("InstanceAccountId", ""); m_settings->registerSetting("ExportName", ""); m_settings->registerSetting("ExportVersion", "1.0.0"); m_settings->registerSetting("ExportSummary", ""); m_settings->registerSetting("ExportAuthor", ""); m_settings->registerSetting("ExportOptionalFiles", true); m_settings->registerSetting("ExportRecommendedRAM"); auto dataPacksEnabled = m_settings->registerSetting("GlobalDataPacksEnabled", false); auto dataPacksPath = m_settings->registerSetting("GlobalDataPacksPath", ""); connect(dataPacksEnabled.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); connect(dataPacksPath.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); // Join server on launch, this does not have a global override m_settings->registerSetting("OverrideModDownloadLoaders", false); m_settings->registerSetting("ModDownloadLoaders", "[]"); qDebug() << "Instance-type specific settings were loaded!"; setSpecificSettingsLoaded(true); updateRuntimeContext(); } void MinecraftInstance::updateRuntimeContext() { m_runtimeContext.updateFromInstanceSettings(m_settings.get()); m_components->invalidateLaunchProfile(); } QString MinecraftInstance::typeName() const { return "Minecraft"; } PackProfile* MinecraftInstance::getPackProfile() const { return m_components.get(); } QSet MinecraftInstance::traits() const { auto components = getPackProfile(); if (!components) { return { "version-incomplete" }; } auto profile = components->getProfile(); if (!profile) { return { "version-incomplete" }; } return profile->getTraits(); } // FIXME: move UI code out of MinecraftInstance void MinecraftInstance::populateLaunchMenu(QMenu* menu) { QAction* normalLaunch = menu->addAction(tr("&Launch")); normalLaunch->setShortcut(QKeySequence::Open); QAction* normalLaunchOffline = menu->addAction(tr("Launch &Offline")); normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); QAction* normalLaunchDemo = menu->addAction(tr("Launch &Demo")); normalLaunchDemo->setShortcut(QKeySequence(tr("Ctrl+Alt+O"))); normalLaunchDemo->setEnabled(supportsDemo()); connect(normalLaunch, &QAction::triggered, [this] { APPLICATION->launch(this); }); connect(normalLaunchOffline, &QAction::triggered, [this] { APPLICATION->launch(this, LaunchMode::Offline); }); connect(normalLaunchDemo, &QAction::triggered, [this] { APPLICATION->launch(this, LaunchMode::Demo); }); QString profilersTitle = tr("Profilers"); menu->addSeparator()->setText(profilersTitle); auto profilers = new QActionGroup(menu); profilers->setExclusive(true); connect(profilers, &QActionGroup::triggered, [this](QAction* action) { settings()->set("Profiler", action->data()); emit profilerChanged(); }); QAction* noProfilerAction = menu->addAction(tr("&No Profiler")); noProfilerAction->setData(""); noProfilerAction->setCheckable(true); noProfilerAction->setChecked(true); profilers->addAction(noProfilerAction); for (auto profiler = APPLICATION->profilers().begin(); profiler != APPLICATION->profilers().end(); profiler++) { QAction* profilerAction = menu->addAction(profiler.value()->name()); profilers->addAction(profilerAction); profilerAction->setData(profiler.key()); profilerAction->setCheckable(true); profilerAction->setChecked(settings()->get("Profiler").toString() == profiler.key()); QString error; profilerAction->setEnabled(profiler.value()->check(&error)); } } QString MinecraftInstance::gameRoot() const { QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft")); if (dotMCDir.exists() && !mcDir.exists()) return dotMCDir.filePath(); else return mcDir.filePath(); } QString MinecraftInstance::binRoot() const { return FS::PathCombine(gameRoot(), "bin"); } QString MinecraftInstance::getNativePath() const { QDir natives_dir(FS::PathCombine(instanceRoot(), "natives/")); return natives_dir.absolutePath(); } QString MinecraftInstance::getLocalLibraryPath() const { QDir libraries_dir(FS::PathCombine(instanceRoot(), "libraries/")); return libraries_dir.absolutePath(); } bool MinecraftInstance::supportsDemo() const { Version instance_ver{ getPackProfile()->getComponentVersion("net.minecraft") }; // Demo mode was introduced in 1.3.1: https://minecraft.wiki/w/Demo_mode#History // FIXME: Due to Version constraints atm, this can't handle well non-release versions return instance_ver >= Version("1.3.1"); } QString MinecraftInstance::jarModsDir() const { QDir jarmods_dir(FS::PathCombine(instanceRoot(), "jarmods/")); return jarmods_dir.absolutePath(); } QString MinecraftInstance::modsRoot() const { return FS::PathCombine(gameRoot(), "mods"); } QString MinecraftInstance::modsCacheLocation() const { return FS::PathCombine(instanceRoot(), "mods.cache"); } QString MinecraftInstance::coreModsDir() const { return FS::PathCombine(gameRoot(), "coremods"); } QString MinecraftInstance::nilModsDir() const { return FS::PathCombine(gameRoot(), "nilmods"); } QString MinecraftInstance::dataPacksDir() { QString relativePath = settings()->get("GlobalDataPacksPath").toString(); if (relativePath.isEmpty()) relativePath = "datapacks"; return QDir(gameRoot()).filePath(relativePath); } QString MinecraftInstance::resourcePacksDir() const { return FS::PathCombine(gameRoot(), "resourcepacks"); } QString MinecraftInstance::texturePacksDir() const { return FS::PathCombine(gameRoot(), "texturepacks"); } QString MinecraftInstance::shaderPacksDir() const { return FS::PathCombine(gameRoot(), "shaderpacks"); } QString MinecraftInstance::instanceConfigFolder() const { return FS::PathCombine(gameRoot(), "config"); } QString MinecraftInstance::libDir() const { return FS::PathCombine(gameRoot(), "lib"); } QString MinecraftInstance::worldDir() const { return FS::PathCombine(gameRoot(), "saves"); } QString MinecraftInstance::resourcesDir() const { return FS::PathCombine(gameRoot(), "resources"); } QDir MinecraftInstance::librariesPath() const { return QDir::current().absoluteFilePath("libraries"); } QDir MinecraftInstance::jarmodsPath() const { return QDir(jarModsDir()); } QDir MinecraftInstance::versionsPath() const { return QDir::current().absoluteFilePath("versions"); } QStringList MinecraftInstance::getClassPath() { QStringList jars, nativeJars; auto profile = m_components->getProfile(); profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); return jars; } QString MinecraftInstance::getMainClass() const { auto profile = m_components->getProfile(); return profile->getMainClass(); } QStringList MinecraftInstance::getNativeJars() { QStringList jars, nativeJars; auto profile = m_components->getProfile(); profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); return nativeJars; } static QString replaceTokensIn(const QString& text, const QMap& with) { // TODO: does this still work?? QString result; static const QRegularExpression s_token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); QStringList list; QRegularExpressionMatchIterator i = s_token_regexp.globalMatch(text); int lastCapturedEnd = 0; while (i.hasNext()) { QRegularExpressionMatch match = i.next(); result.append(text.mid(lastCapturedEnd, match.capturedStart())); QString key = match.captured(1); auto iter = with.find(key); if (iter != with.end()) { result.append(*iter); } lastCapturedEnd = match.capturedEnd(); } result.append(text.mid(lastCapturedEnd)); return result; } QStringList MinecraftInstance::extraArguments() { auto list = BaseInstance::extraArguments(); auto version = getPackProfile(); if (!version) return list; auto jarMods = getJarMods(); if (!jarMods.isEmpty()) { list.append({ "-Dfml.ignoreInvalidMinecraftCertificates=true", "-Dfml.ignorePatchDiscrepancies=true" }); } auto addn = m_components->getProfile()->getAddnJvmArguments(); if (!addn.isEmpty()) { QMap tokenMapping = makeProfileVarMapping(m_components->getProfile()); for (const QString& item : addn) { list.append(replaceTokensIn(item, tokenMapping)); } } auto agents = m_components->getProfile()->getAgents(); for (const auto& agent : agents) { QStringList jar, temp1, temp2, temp3; agent.library->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, getLocalLibraryPath()); list.append("-javaagent:" + jar[0] + (agent.argument.isEmpty() ? "" : "=" + agent.argument)); } { QString openALPath; QString glfwPath; if (settings()->get("UseNativeOpenAL").toBool()) { openALPath = APPLICATION->m_detectedOpenALPath; auto customPath = settings()->get("CustomOpenALPath").toString(); if (!customPath.isEmpty()) openALPath = customPath; } if (settings()->get("UseNativeGLFW").toBool()) { glfwPath = APPLICATION->m_detectedGLFWPath; auto customPath = settings()->get("CustomGLFWPath").toString(); if (!customPath.isEmpty()) glfwPath = customPath; } QFileInfo openALInfo(openALPath); QFileInfo glfwInfo(glfwPath); if (!openALPath.isEmpty() && openALInfo.exists()) list.append("-Dorg.lwjgl.openal.libname=" + openALInfo.absoluteFilePath()); if (!glfwPath.isEmpty() && glfwInfo.exists()) list.append("-Dorg.lwjgl.glfw.libname=" + glfwInfo.absoluteFilePath()); } return list; } QStringList MinecraftInstance::javaArguments() { QStringList args; args << "-Duser.language=en"; // custom args go first. we want to override them if we have our own here. args.append(extraArguments()); // OSX dock icon and name #ifdef Q_OS_MAC args << "-Xdock:icon=icon.png"; args << QString("-Xdock:name=\"%1\"").arg(windowTitle()); #endif auto traits_ = traits(); // HACK: fix issues on macOS with 1.13 snapshots // NOTE: Oracle Java option. if there are alternate jvm implementations, this would be the place to customize this for them #ifdef Q_OS_MAC if (traits_.contains("FirstThreadOnMacOS")) { args << QString("-XstartOnFirstThread"); } #endif // HACK: Stupid hack for Intel drivers. See: https://mojang.atlassian.net/browse/MCL-767 #ifdef Q_OS_WIN32 args << QString( "-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_" "minecraft.exe.heapdump"); #endif // LWJGL2 reads `LWJGL_DISABLE_XRANDR` to force disable xrandr usage and fall back to xf86videomode. // It *SHOULD* check for the executable to exist before trying to use it for queries but it doesnt, // so WE can and force disable xrandr if it is not available. #ifdef Q_OS_LINUX // LWJGL2 is "org.lwjgl" LWJGL3 is "org.lwjgl3" if (m_components->getComponent("org.lwjgl") != nullptr && QStandardPaths::findExecutable("xrandr").isEmpty()) { args << QString("-DLWJGL_DISABLE_XRANDR=true"); } #endif int min = settings()->get("MinMemAlloc").toInt(); int max = settings()->get("MaxMemAlloc").toInt(); if (min < max) { args << QString("-Xms%1m").arg(min); args << QString("-Xmx%1m").arg(max); } else { args << QString("-Xms%1m").arg(max); args << QString("-Xmx%1m").arg(min); } // No PermGen in newer java. JavaVersion javaVersion = getJavaVersion(); if (javaVersion.requiresPermGen()) { auto permgen = settings()->get("PermGen").toInt(); if (permgen != 64) { args << QString("-XX:PermSize=%1m").arg(permgen); } } if (javaVersion.isModular() && shouldApplyOnlineFixes()) // allow reflective access to java.net - required by the skin fix args << "--add-opens" << "java.base/java.net=ALL-UNNAMED"; return args; } QString MinecraftInstance::getLauncher() { // use legacy launcher if the traits are set if (isLegacy()) return "legacy"; return "standard"; } bool MinecraftInstance::shouldApplyOnlineFixes() { return traits().contains("legacyServices") && settings()->get("OnlineFixes").toBool(); } QMap MinecraftInstance::getVariables() { QMap out; out.insert("INST_NAME", name()); out.insert("INST_ID", id()); out.insert("INST_DIR", QDir::toNativeSeparators(QDir(instanceRoot()).absolutePath())); out.insert("INST_MC_DIR", QDir::toNativeSeparators(QDir(gameRoot()).absolutePath())); out.insert("INST_JAVA", QDir::toNativeSeparators(QDir(settings()->get("JavaPath").toString()).absolutePath())); out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); out.insert("NO_COLOR", "1"); #ifdef Q_OS_MACOS // get library for Steam overlay support QString steamDyldInsertLibraries = qEnvironmentVariable("STEAM_DYLD_INSERT_LIBRARIES"); if (!steamDyldInsertLibraries.isEmpty()) { out.insert("DYLD_INSERT_LIBRARIES", steamDyldInsertLibraries); } #endif return out; } QProcessEnvironment MinecraftInstance::createEnvironment() { // prepare the process environment QProcessEnvironment env = CleanEnviroment(); // export some infos auto variables = getVariables(); for (auto it = variables.begin(); it != variables.end(); ++it) { env.insert(it.key(), it.value()); } // custom env auto insertEnv = [&env](QString value) { auto envMap = Json::toMap(value); if (envMap.isEmpty()) return; for (auto iter = envMap.begin(); iter != envMap.end(); iter++) env.insert(iter.key(), iter.value().toString()); }; insertEnv(settings()->get("Env").toString()); return env; } QProcessEnvironment MinecraftInstance::createLaunchEnvironment() { // prepare the process environment QProcessEnvironment env = createEnvironment(); #ifdef Q_OS_LINUX if (settings()->get("EnableMangoHud").toBool() && APPLICATION->capabilities() & Application::SupportsMangoHud) { QStringList preloadList; if (auto value = env.value("LD_PRELOAD"); !value.isEmpty()) preloadList = value.split(QLatin1String(":")); auto mangoHudLibString = LibraryUtils::findMangoHud(); if (!mangoHudLibString.isEmpty()) { QFileInfo mangoHudLib(mangoHudLibString); QString libPath = mangoHudLib.absolutePath(); auto appendLib = [libPath, &preloadList](QString fileName) { if (QFileInfo(FS::PathCombine(libPath, fileName)).exists()) preloadList << FS::PathCombine(libPath, fileName); }; // 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; } env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":"))); env.insert("MANGOHUD", "1"); } if (settings()->get("UseDiscreteGpu").toBool()) { if (!switcherooSetupGPU(env)) { // Open Source Drivers env.insert("DRI_PRIME", "1"); // Proprietary Nvidia Drivers env.insert("__NV_PRIME_RENDER_OFFLOAD", "1"); env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only"); env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); } } if (settings()->get("UseZink").toBool()) { // taken from https://wiki.archlinux.org/title/OpenGL#OpenGL_over_Vulkan_(Zink) env.insert("__GLX_VENDOR_LIBRARY_NAME", "mesa"); env.insert("MESA_LOADER_DRIVER_OVERRIDE", "zink"); env.insert("GALLIUM_DRIVER", "zink"); env.insert("LIBGL_KOPPER_DRI2", "1"); } #endif return env; } QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) const { auto profile = m_components->getProfile(); auto args = profile->getMinecraftArguments().split(' ', Qt::SkipEmptyParts); for (auto tweaker : profile->getTweakers()) { args << "--tweakClass" << tweaker; } if (targetToJoin) { if (!targetToJoin->address.isEmpty()) { if (profile->hasTrait("feature:is_quick_play_multiplayer")) { args << "--quickPlayMultiplayer" << targetToJoin->address + ':' + QString::number(targetToJoin->port); } else { args << "--server" << targetToJoin->address; args << "--port" << QString::number(targetToJoin->port); } } else if (!targetToJoin->world.isEmpty() && profile->hasTrait("feature:is_quick_play_singleplayer")) { args << "--quickPlaySingleplayer" << targetToJoin->world; } } QMap tokenMapping = makeProfileVarMapping(profile); // yggdrasil! if (session) { // token_mapping["auth_username"] = session->username; tokenMapping["auth_session"] = session->session; tokenMapping["auth_access_token"] = session->access_token; tokenMapping["auth_player_name"] = session->player_name; tokenMapping["auth_uuid"] = session->uuid; tokenMapping["user_properties"] = session->serializeUserProperties(); tokenMapping["user_type"] = session->user_type; if (session->launchMode == LaunchMode::Demo) { args << "--demo"; } } for (int i = 0; i < args.length(); i++) { args[i] = replaceTokensIn(args[i], tokenMapping); } return args; } QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { QString launchScript; if (!m_components) return QString(); auto profile = m_components->getProfile(); if (!profile) return QString(); auto mainClass = getMainClass(); if (!mainClass.isEmpty()) { launchScript += "mainClass " + mainClass + "\n"; } auto appletClass = profile->getAppletClass(); if (!appletClass.isEmpty()) { launchScript += "appletClass " + appletClass + "\n"; } if (targetToJoin) { if (!targetToJoin->address.isEmpty()) { launchScript += "serverAddress " + targetToJoin->address + "\n"; launchScript += "serverPort " + QString::number(targetToJoin->port) + "\n"; } else if (!targetToJoin->world.isEmpty()) { launchScript += "worldName " + targetToJoin->world + "\n"; } } // generic minecraft params for (auto param : processMinecraftArgs(session, nullptr /* When using a launch script, the server parameters are handled by it*/ )) { launchScript += "param " + param + "\n"; } // window size, title and state, legacy { QString windowParams; if (settings()->get("LaunchMaximized").toBool()) { // FIXME doesn't support maximisation if (!isLegacy()) { auto screen = QGuiApplication::primaryScreen(); auto screenGeometry = screen->availableSize(); // small hack to get the widow decorations for (auto w : QApplication::topLevelWidgets()) { auto mainWindow = qobject_cast(w); if (mainWindow) { auto m = mainWindow->windowHandle()->frameMargins(); screenGeometry = screenGeometry.shrunkBy(m); break; } } windowParams = QString("%1x%2").arg(screenGeometry.width()).arg(screenGeometry.height()); } else { windowParams = "maximized"; } } else { windowParams = QString("%1x%2").arg(settings()->get("MinecraftWinWidth").toInt()).arg(settings()->get("MinecraftWinHeight").toInt()); } launchScript += "windowTitle " + windowTitle() + "\n"; launchScript += "windowParams " + windowParams + "\n"; } // launcher info { launchScript += "launcherBrand " + BuildConfig.LAUNCHER_NAME + "\n"; launchScript += "launcherVersion " + BuildConfig.printableVersionString() + "\n"; } // instance info { launchScript += "instanceName " + name() + "\n"; launchScript += "instanceIconKey " + name() + "\n"; launchScript += "instanceIconPath icon.png\n"; // we already save a copy here } // legacy auth if (session) { launchScript += "userName " + session->player_name + "\n"; launchScript += "sessionId " + session->session + "\n"; } for (auto trait : profile->getTraits()) { launchScript += "traits " + trait + "\n"; } if (shouldApplyOnlineFixes()) launchScript += "onlineFixes true\n"; launchScript += "launcher " + getLauncher() + "\n"; // qDebug() << "Generated launch script:" << launchScript; return launchScript; } QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { constexpr auto indent = " "; constexpr auto emptyLine = ""; QStringList out; out << "Components:"; for (int i = 0; i < m_components->rowCount(); ++i) { const auto& component = m_components->getComponent(i); out << indent + QString("%1) %2 (%3) %4").arg(QString::number(i + 1), component->getName(), component->getID(), component->getVersion()); } out << emptyLine; out << "Launcher: " + getLauncher(); out << "Main class: " + getMainClass() << emptyLine; auto profile = m_components->getProfile(); // mods and core mods auto printModList = [&out](const QString& label, ModFolderModel& model) { if (model.size()) { out << QString("%1:").arg(label); auto modList = model.allMods(); std::sort(modList.begin(), modList.end(), [](auto a, auto b) { auto aName = a->fileinfo().completeBaseName(); auto bName = b->fileinfo().completeBaseName(); return aName.localeAwareCompare(bName) < 0; }); for (auto mod : modList) { if (mod->type() == ResourceType::FOLDER) { out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)"; continue; } if (mod->enabled()) { out << u8" [✔] " + mod->fileinfo().completeBaseName(); } else { out << u8" [✘] " + mod->fileinfo().completeBaseName() + " (disabled)"; } } out << emptyLine; } }; printModList("Mods", *loaderModList()); printModList("Core Mods", *coreModList()); // jar mods auto& jarMods = profile->getJarMods(); if (jarMods.size()) { out << "Jar Mods:"; for (auto& jarmod : jarMods) { auto displayname = jarmod->displayName(runtimeContext()); auto realname = jarmod->filename(runtimeContext()); if (displayname != realname) { out << indent + displayname + " (" + realname + ")"; } else { out << indent + realname; } } out << emptyLine; } // traits auto alltraits = traits(); if (alltraits.size()) { out << "Traits:"; for (auto trait : alltraits) { out << indent + trait; } out << emptyLine; } // native libraries auto settings = this->settings(); bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); if (nativeOpenAL || nativeGLFW) { if (nativeOpenAL) out << "Using system OpenAL."; if (nativeGLFW) out << "Using system GLFW."; out << emptyLine; } // libraries and class path. { out << "Libraries:"; QStringList jars, nativeJars; profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); for (auto file : jars) { out << indent + file; } out << emptyLine; out << "Native libraries:"; for (auto file : nativeJars) { out << indent + file; } out << emptyLine; } out << "Natives path:" << indent + getNativePath() << emptyLine; // minecraft arguments auto params = processMinecraftArgs(nullptr, targetToJoin); out << "Minecraft arguments:"; out << indent + params.join(' '); out << emptyLine; // window size QString windowParams; if (settings->get("LaunchMaximized").toBool()) { out << "Window size: max (if available)"; } else { auto width = settings->get("MinecraftWinWidth").toInt(); auto height = settings->get("MinecraftWinHeight").toInt(); out << "Window size: " + QString::number(width) + " x " + QString::number(height); } out << emptyLine; // environment variables const QString env = settings->get("Env").toString(); if (auto envMap = Json::toMap(env); !envMap.isEmpty()) { out << "Custom environment variables:"; for (auto [key, value] : envMap.asKeyValueRange()) { out << indent + key + "=" + value.toString(); } out << emptyLine; } return out; } QMap MinecraftInstance::createCensorFilterFromSession(AuthSessionPtr session) { if (!session) { return QMap(); } auto& sessionRef = *session.get(); QMap filter; auto addToFilter = [&filter](QString key, QString value) { if (key.trimmed().size()) { filter[key] = value; } }; if (sessionRef.session != "-") { addToFilter(sessionRef.session, tr("")); } if (sessionRef.access_token != "0") { addToFilter(sessionRef.access_token, tr("")); } addToFilter(sessionRef.uuid, tr("")); return filter; } QMap MinecraftInstance::makeProfileVarMapping(std::shared_ptr profile) const { QMap result; result["profile_name"] = name(); result["version_name"] = profile->getMinecraftVersion(); result["version_type"] = profile->getMinecraftVersionType(); QString absRootDir = QDir(gameRoot()).absolutePath(); result["game_directory"] = absRootDir; QString absAssetsDir = QDir("assets/").absolutePath(); auto assets = profile->getMinecraftAssets(); result["game_assets"] = AssetsUtils::getAssetsDir(assets->id, resourcesDir()).absolutePath(); // 1.7.3+ assets tokens result["assets_root"] = absAssetsDir; result["assets_index_name"] = assets->id; result["library_directory"] = APPLICATION->metacache()->getBasePath("libraries"); return result; } QStringList MinecraftInstance::getLogFileSearchPaths() { return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() }; } QString MinecraftInstance::getStatusbarDescription() { QStringList traits; if (hasVersionBroken()) { traits.append(tr("broken")); } QString mcVersion = m_components->getComponentVersion("net.minecraft"); if (mcVersion.isEmpty()) { // Load component info if needed m_components->reload(Net::Mode::Offline); mcVersion = m_components->getComponentVersion("net.minecraft"); } QString description; description.append(tr("Minecraft %1").arg(mcVersion)); if (m_settings->get("ShowGameTime").toBool()) { if (lastTimePlayed() > 0 && lastLaunch() > 0) { QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch()); description.append( tr(", last played on %1 for %2") .arg(QLocale().toString(lastLaunchTime, QLocale::ShortFormat)) .arg(Time::prettifyDuration(lastTimePlayed(), APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); } if (totalTimePlayed() > 0) { description.append( tr(", total played for %1") .arg(Time::prettifyDuration(totalTimePlayed(), APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); } } if (hasCrashed()) { description.append(tr(", has crashed.")); } return description; } QList MinecraftInstance::createUpdateTask() { return { // create folders makeShared(this), // libraries download makeShared(this), // FML libraries download and copy into the instance makeShared(this), // assets update makeShared(this), }; } LaunchTask* MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { updateRuntimeContext(); auto process = LaunchTask::create(this); auto pptr = process.get(); APPLICATION->icons()->saveIcon(iconKey(), FS::PathCombine(gameRoot(), "icon.png"), "PNG"); // print a header { process->appendStep(makeShared(pptr, "Minecraft folder is:\n " + gameRoot() + "\n", MessageLevel::Launcher)); } // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) { process->appendStep(makeShared(pptr)); } if (!targetToJoin && settings()->get("JoinServerOnLaunch").toBool()) { QString fullAddress = settings()->get("JoinServerOnLaunchAddress").toString(); if (!fullAddress.isEmpty()) { targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(fullAddress, false))); } else { QString world = settings()->get("JoinWorldOnLaunch").toString(); if (!world.isEmpty()) { targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(world, true))); } } } if (targetToJoin && targetToJoin->port == 25565) { // Resolve server address to join on launch auto step = makeShared(pptr); step->setLookupAddress(targetToJoin->address); step->setOutputAddressPtr(targetToJoin); process->appendStep(step); } // load meta { auto mode = session->launchMode != LaunchMode::Offline ? Net::Mode::Online : Net::Mode::Offline; process->appendStep(makeShared(pptr, makeShared(this, mode))); } // check java { process->appendStep(makeShared(pptr)); process->appendStep(makeShared(pptr)); // verify that minimum Java requirements are met process->appendStep(makeShared(pptr)); } // run pre-launch command if that's needed if (getPreLaunchCommand().size()) { auto step = makeShared(pptr); step->setWorkingDirectory(gameRoot()); process->appendStep(step); } // if we aren't in offline mode if (session->launchMode != LaunchMode::Offline) { process->appendStep(makeShared(pptr, session)); for (auto t : createUpdateTask()) { process->appendStep(makeShared(pptr, t)); } } else { process->appendStep(makeShared(pptr, this)); } // if there are any jar mods { process->appendStep(makeShared(pptr)); } // Scan mods folders for mods { process->appendStep(makeShared(pptr)); } // make sure we have enough RAM, warn the user if we don't { process->appendStep(makeShared(pptr, this)); } // print some instance info here... { process->appendStep(makeShared(pptr, session, targetToJoin)); } // extract native jars if needed { process->appendStep(makeShared(pptr)); } // reconstruct assets if needed { process->appendStep(makeShared(pptr)); } { // actually launch the game auto step = makeShared(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); step->setTargetToJoin(targetToJoin); process->appendStep(step); } // run post-exit command if that's needed if (getPostExitCommand().size()) { auto step = makeShared(pptr); step->setWorkingDirectory(gameRoot()); process->appendStep(step); } if (session) { process->setCensorFilter(createCensorFilterFromSession(session)); } if (m_settings->get("QuitAfterGameStop").toBool()) { process->appendStep(makeShared(pptr)); } m_launchProcess = std::move(process); emit launchTaskChanged(m_launchProcess.get()); return m_launchProcess.get(); } JavaVersion MinecraftInstance::getJavaVersion() { return JavaVersion(settings()->get("JavaVersion").toString()); } ModFolderModel* MinecraftInstance::loaderModList() { if (!m_loader_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed, true)); } return m_loader_mod_list.get(); } ModFolderModel* MinecraftInstance::coreModList() { if (!m_core_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed, true)); } return m_core_mod_list.get(); } ModFolderModel* MinecraftInstance::nilModList() { if (!m_nil_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false)); } return m_nil_mod_list.get(); } ResourcePackFolderModel* MinecraftInstance::resourcePackList() { if (!m_resource_pack_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), this, is_indexed, true)); } return m_resource_pack_list.get(); } TexturePackFolderModel* MinecraftInstance::texturePackList() { if (!m_texture_pack_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), this, is_indexed, true)); } return m_texture_pack_list.get(); } ShaderPackFolderModel* MinecraftInstance::shaderPackList() { if (!m_shader_pack_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), this, is_indexed, true)); } return m_shader_pack_list.get(); } DataPackFolderModel* MinecraftInstance::dataPackList() { if (!m_data_pack_list && settings()->get("GlobalDataPacksEnabled").toBool()) { bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_data_pack_list.reset(new DataPackFolderModel(dataPacksDir(), this, isIndexed, true)); } return m_data_pack_list.get(); } QList MinecraftInstance::resourceLists() { return { loaderModList(), coreModList(), nilModList(), resourcePackList(), texturePackList(), shaderPackList(), dataPackList() }; } WorldList* MinecraftInstance::worldList() { if (!m_world_list) { m_world_list.reset(new WorldList(worldDir(), this)); } return m_world_list.get(); } QList MinecraftInstance::getJarMods() const { auto profile = m_components->getProfile(); QList mods; for (auto jarmod : profile->getJarMods()) { QStringList jar, temp1, temp2, temp3; jarmod->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, jarmodsPath().absolutePath()); // QString filePath = jarmodsPath().absoluteFilePath(jarmod->filename(currentSystem)); mods.push_back(new Mod(QFileInfo(jar[0]))); } return mods; } #include "MinecraftInstance.moc" PrismLauncher-11.0.3/launcher/minecraft/MojangVersionFormat.h0000644000175100017510000000153715224505336023645 0ustar runnerrunner#pragma once #include #include #include #include class MojangVersionFormat { friend class OneSixVersionFormat; protected: // does not include libraries static void readVersionProperties(const QJsonObject& in, VersionFile* out); // does not include libraries static void writeVersionProperties(const VersionFile* in, QJsonObject& out); public: // version files / profile patches static VersionFilePtr versionFileFromJson(const QJsonDocument& doc, const QString& filename); static QJsonDocument versionFileToJson(const VersionFilePtr& patch); // libraries static LibraryPtr libraryFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); static QJsonObject libraryToJson(Library* library); }; PrismLauncher-11.0.3/launcher/minecraft/MinecraftLoadAndCheck.h0000644000175100017510000000212415224505336023775 0ustar runnerrunner/* 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 "net/Mode.h" #include "tasks/Task.h" class MinecraftInstance; class MinecraftLoadAndCheck : public Task { Q_OBJECT public: explicit MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode); virtual ~MinecraftLoadAndCheck() = default; void executeTask() override; bool canAbort() const override; public slots: bool abort() override; private: MinecraftInstance* m_inst = nullptr; Task::Ptr m_task; Net::Mode m_netmode; }; PrismLauncher-11.0.3/launcher/minecraft/VanillaInstanceCreationTask.h0000644000175100017510000000116615224505336025274 0ustar runnerrunner#pragma once #include "InstanceCreationTask.h" #include class VanillaCreationTask final : public InstanceCreationTask { Q_OBJECT public: VanillaCreationTask(BaseVersion::Ptr version) : InstanceCreationTask(), m_version(std::move(version)) {} VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version); std::unique_ptr createInstance() override; private: // Version to update to / create of the instance. BaseVersion::Ptr m_version; bool m_using_loader = false; QString m_loader; BaseVersion::Ptr m_loader_version; }; PrismLauncher-11.0.3/launcher/minecraft/OneSixVersionFormat.h0000644000175100017510000000257115224505336023636 0ustar runnerrunner#pragma once #include #include #include #include #include class OneSixVersionFormat { public: // version files / profile patches static VersionFilePtr versionFileFromJson(const QJsonDocument& doc, const QString& filename, bool requireOrder); static QJsonDocument versionFileToJson(const VersionFilePtr& patch); // libraries static LibraryPtr libraryFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); static QJsonObject libraryToJson(Library* library); // DEPRECATED: old 'plus' jar mods generated by the application static LibraryPtr plusJarModFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename, const QString& originalName); // new jar mods derived from libraries static LibraryPtr jarModFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); static QJsonObject jarModtoJson(Library* jarmod); // mods, also derived from libraries static LibraryPtr modFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); static QJsonObject modtoJson(Library* jarmod); }; PrismLauncher-11.0.3/launcher/minecraft/skins/0000755000175100017510000000000015224505336020663 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/minecraft/skins/SkinList.h0000644000175100017510000000520515224505336022576 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include "QObjectPtr.h" #include "SkinModel.h" #include "minecraft/auth/MinecraftAccount.h" class SkinList : public QAbstractListModel { Q_OBJECT public: explicit SkinList(QObject* parent, QString path, MinecraftAccountPtr acct); virtual ~SkinList() { save(); }; int getSkinIndex(const QString& key) const; virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex& idx, const QVariant& value, int role) override; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; virtual QStringList mimeTypes() const override; virtual Qt::DropActions supportedDropActions() const override; virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; virtual Qt::ItemFlags flags(const QModelIndex& index) const override; bool deleteSkin(const QString& key, bool trash); void installSkins(const QStringList& iconFiles); QString installSkin(const QString& file, const QString& name = {}); const SkinModel* skin(const QString& key) const; SkinModel* skin(const QString& key); void startWatching(); void stopWatching(); QString getDir() const { return m_dir.absolutePath(); } void save(); int getSelectedAccountSkin(); void updateSkin(SkinModel* s); private: // hide copy constructor SkinList(const SkinList&) = delete; // hide assign op SkinList& operator=(const SkinList&) = delete; protected slots: void directoryChanged(const QString& path); void fileChanged(const QString& path); bool update(); private: shared_qobject_ptr m_watcher; bool m_isWatching; QList m_skinList; QDir m_dir; MinecraftAccountPtr m_acct; }; PrismLauncher-11.0.3/launcher/minecraft/skins/SkinUpload.cpp0000644000175100017510000000577615224505336023457 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "SkinUpload.h" #include #include "FileSystem.h" #include "net/DummySink.h" #include "net/RawHeaderProxy.h" SkinUpload::SkinUpload(QString path, QString variant) : NetRequest(), m_path(path), m_variant(variant) { logCat = taskMCSkinsLogC; } QNetworkReply* SkinUpload::getReply(QNetworkRequest& request) { QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this); QHttpPart skin; skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); skin.setBody(FS::read(m_path)); QHttpPart model; model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); model.setBody(m_variant.toUtf8()); multiPart->append(skin); multiPart->append(model); setStatus(tr("Uploading skin")); return m_network->post(request, multiPart); } SkinUpload::Ptr SkinUpload::make(QString token, QString path, QString variant) { auto up = makeShared(path, variant); up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins"); up->setObjectName(QString("BYTES:") + up->m_url.toString()); up->m_sink.reset(new Net::DummySink()); up->addHeaderProxy(std::make_unique(QList{ { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, })); return up; } PrismLauncher-11.0.3/launcher/minecraft/skins/CapeChange.cpp0000644000175100017510000000505315224505336023350 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "CapeChange.h" #include #include #include "net/RawHeaderProxy.h" CapeChange::CapeChange(QString cape) : NetRequest(), m_capeId(cape) { logCat = taskMCSkinsLogC; } QNetworkReply* CapeChange::getReply(QNetworkRequest& request) { if (m_capeId.isEmpty()) { setStatus(tr("Removing cape")); return m_network->deleteResource(request); } else { setStatus(tr("Equipping cape")); return m_network->put(request, QString("{\"capeId\":\"%1\"}").arg(m_capeId).toUtf8()); } } CapeChange::Ptr CapeChange::make(QString token, QString capeId) { auto up = makeShared(capeId); up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"); up->setObjectName(QString("BYTES:") + up->m_url.toString()); up->m_sink.reset(new Net::DummySink()); up->addHeaderProxy(std::make_unique(QList{ { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, })); return up; } PrismLauncher-11.0.3/launcher/minecraft/skins/CapeChange.h0000644000175100017510000000220415224505336023010 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "net/NetRequest.h" class CapeChange : public Net::NetRequest { Q_OBJECT public: using Ptr = shared_qobject_ptr; CapeChange(QString capeId); virtual ~CapeChange() = default; static CapeChange::Ptr make(QString token, QString capeId); protected: virtual QNetworkReply* getReply(QNetworkRequest&) override; private: QString m_capeId; }; PrismLauncher-11.0.3/launcher/minecraft/skins/SkinDelete.h0000644000175100017510000000210315224505336023057 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "net/NetRequest.h" class SkinDelete : public Net::NetRequest { Q_OBJECT public: using Ptr = shared_qobject_ptr; SkinDelete(); virtual ~SkinDelete() = default; static SkinDelete::Ptr make(QString token); protected: virtual QNetworkReply* getReply(QNetworkRequest&) override; }; PrismLauncher-11.0.3/launcher/minecraft/skins/SkinModel.h0000644000175100017510000000334715224505336022730 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include class SkinModel { public: enum Model { CLASSIC, SLIM }; SkinModel() = default; SkinModel(QString path); SkinModel(QDir skinDir, QJsonObject obj); virtual ~SkinModel() = default; QString name() const; QString getModelString() const; bool isValid() const; QString getPath() const { return m_path; } QImage getTexture() const { return m_texture; } QImage getPreview() const { return m_preview; } QString getCapeId() const { return m_capeId; } Model getModel() const { return m_model; } QString getURL() const { return m_url; } bool rename(QString newName); void setCapeId(QString capeID) { m_capeId = capeID; } void setModel(Model model); void setURL(QString url) { m_url = url; } void refresh(); QJsonObject toJSON() const; private: QString m_path; QImage m_texture; QImage m_preview; QString m_capeId; Model m_model; QString m_url; }; PrismLauncher-11.0.3/launcher/minecraft/skins/SkinUpload.h0000644000175100017510000000235415224505336023111 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "net/NetRequest.h" class SkinUpload : public Net::NetRequest { Q_OBJECT public: using Ptr = shared_qobject_ptr; // Note this class takes ownership of the file. SkinUpload(QString path, QString variant); virtual ~SkinUpload() = default; static SkinUpload::Ptr make(QString token, QString path, QString variant); protected: virtual QNetworkReply* getReply(QNetworkRequest&) override; private: QString m_path; QString m_variant; }; PrismLauncher-11.0.3/launcher/minecraft/skins/SkinModel.cpp0000644000175100017510000001663415224505336023266 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2025 Trial97 * Copyright (c) 2025 Rinth, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "SkinModel.h" #include #include #include "FileSystem.h" static void setAlpha(QImage& image, const QRect& region, const int alpha) { for (int y = region.top(); y < region.bottom(); ++y) { QRgb* line = reinterpret_cast(image.scanLine(y)); for (int x = region.left(); x < region.right(); ++x) { QRgb pixel = line[x]; line[x] = qRgba(qRed(pixel), qGreen(pixel), qBlue(pixel), alpha); } } } static void doNotchTransparencyHack(QImage& image) { for (int y = 0; y < 32; y++) { QRgb* line = reinterpret_cast(image.scanLine(y)); for (int x = 32; x < 64; x++) { if (qAlpha(line[x]) < 128) { return; } } } setAlpha(image, { 32, 0, 32, 32 }, 0); } static QImage improveSkin(QImage skin) { int height = skin.height(); int width = skin.width(); if (width != 64 || (height != 32 && height != 64)) { // this is no minecraft skin return skin; } // It seems some older skins may use this format, which can't be drawn onto // https://github.com/PrismLauncher/PrismLauncher/issues/4032 // https://doc.qt.io/qt-6/qpainter.html#begin if (skin.format() <= QImage::Format_Indexed8 || !skin.hasAlphaChannel()) { skin = skin.convertToFormat(QImage::Format_ARGB32); } auto isLegacy = height == 32; // old format if (isLegacy) { auto newSkin = QImage(QSize(64, 64), skin.format()); newSkin.fill(Qt::transparent); QPainter p(&newSkin); p.drawImage(0, 0, skin); auto copyRect = [&p, &newSkin](int startX, int startY, int offsetX, int offsetY, int sizeX, int sizeY) { QImage region = newSkin.copy(startX, startY, sizeX, sizeY); region = region.mirrored(true, false); p.drawImage(startX + offsetX, startY + offsetY, region); }; static const struct { int x; int y; int offsetX; int offsetY; int width; int height; } faces[] = { { 4, 16, 16, 32, 4, 4 }, { 8, 16, 16, 32, 4, 4 }, { 0, 20, 24, 32, 4, 12 }, { 4, 20, 16, 32, 4, 12 }, { 8, 20, 8, 32, 4, 12 }, { 12, 20, 16, 32, 4, 12 }, { 44, 16, -8, 32, 4, 4 }, { 48, 16, -8, 32, 4, 4 }, { 40, 20, 0, 32, 4, 12 }, { 44, 20, -8, 32, 4, 12 }, { 48, 20, -16, 32, 4, 12 }, { 52, 20, -8, 32, 4, 12 }, }; for (const auto& face : faces) { copyRect(face.x, face.y, face.offsetX, face.offsetY, face.width, face.height); } doNotchTransparencyHack(newSkin); skin = newSkin; } static const QRect opaqueParts[] = { { 0, 0, 32, 16 }, { 0, 16, 64, 16 }, { 16, 48, 32, 16 }, }; for (const auto& p : opaqueParts) { setAlpha(skin, p, 255); } return skin; } static QImage getSkin(const QString path) { return improveSkin(QImage(path)); } static QImage generatePreviews(QImage texture, bool slim) { QImage preview(36, 36, QImage::Format_ARGB32); preview.fill(Qt::transparent); QPainter paint(&preview); // head paint.drawImage(4, 2, texture.copy(8, 8, 8, 8)); paint.drawImage(4, 2, texture.copy(40, 8, 8, 8)); // torso paint.drawImage(4, 10, texture.copy(20, 20, 8, 12)); paint.drawImage(4, 10, texture.copy(20, 36, 8, 12)); // right leg paint.drawImage(4, 22, texture.copy(4, 20, 4, 12)); paint.drawImage(4, 22, texture.copy(4, 36, 4, 12)); // left leg paint.drawImage(8, 22, texture.copy(20, 52, 4, 12)); paint.drawImage(8, 22, texture.copy(4, 52, 4, 12)); auto armWidth = slim ? 3 : 4; auto armPosX = slim ? 1 : 0; // right arm paint.drawImage(armPosX, 10, texture.copy(44, 20, armWidth, 12)); paint.drawImage(armPosX, 10, texture.copy(44, 36, armWidth, 12)); // left arm paint.drawImage(12, 10, texture.copy(36, 52, armWidth, 12)); paint.drawImage(12, 10, texture.copy(52, 52, armWidth, 12)); // back // head paint.drawImage(24, 2, texture.copy(24, 8, 8, 8)); paint.drawImage(24, 2, texture.copy(56, 8, 8, 8)); // torso paint.drawImage(24, 10, texture.copy(32, 20, 8, 12)); paint.drawImage(24, 10, texture.copy(32, 36, 8, 12)); // right leg paint.drawImage(24, 22, texture.copy(12, 20, 4, 12)); paint.drawImage(24, 22, texture.copy(12, 36, 4, 12)); // left leg paint.drawImage(28, 22, texture.copy(28, 52, 4, 12)); paint.drawImage(28, 22, texture.copy(12, 52, 4, 12)); // right arm paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 20, armWidth, 12)); paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 36, armWidth, 12)); // left arm paint.drawImage(32, 10, texture.copy(40 + armWidth, 52, armWidth, 12)); paint.drawImage(32, 10, texture.copy(56 + armWidth, 52, armWidth, 12)); return preview; } SkinModel::SkinModel(QString path) : m_path(path), m_texture(getSkin(path)), m_model(Model::CLASSIC) { m_preview = generatePreviews(m_texture, false); } SkinModel::SkinModel(QDir skinDir, QJsonObject obj) : m_capeId(obj["capeId"].toString()), m_model(Model::CLASSIC), m_url(obj["url"].toString()) { auto name = obj["name"].toString(); if (auto model = obj["model"].toString(); model == "SLIM") { m_model = Model::SLIM; } m_path = skinDir.absoluteFilePath(name) + ".png"; m_texture = getSkin(m_path); m_preview = generatePreviews(m_texture, m_model == Model::SLIM); } QString SkinModel::name() const { return QFileInfo(m_path).completeBaseName(); } bool SkinModel::rename(QString newName) { auto info = QFileInfo(m_path); auto new_path = FS::PathCombine(info.absolutePath(), newName + ".png"); if (QFileInfo::exists(new_path)) { return false; } m_path = new_path; return FS::move(info.absoluteFilePath(), m_path); } QJsonObject SkinModel::toJSON() const { QJsonObject obj; obj["name"] = name(); obj["capeId"] = m_capeId; obj["url"] = m_url; obj["model"] = getModelString(); return obj; } QString SkinModel::getModelString() const { switch (m_model) { case CLASSIC: return "CLASSIC"; case SLIM: return "SLIM"; } return {}; } bool SkinModel::isValid() const { return !m_texture.isNull() && (m_texture.size().height() == 32 || m_texture.size().height() == 64) && m_texture.size().width() == 64; } void SkinModel::refresh() { m_texture = getSkin(m_path); m_preview = generatePreviews(m_texture, m_model == Model::SLIM); } void SkinModel::setModel(Model model) { m_model = model; m_preview = generatePreviews(m_texture, m_model == Model::SLIM); } PrismLauncher-11.0.3/launcher/minecraft/skins/SkinDelete.cpp0000644000175100017510000000434515224505336023424 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "SkinDelete.h" #include #include "net/RawHeaderProxy.h" SkinDelete::SkinDelete() : NetRequest() { logCat = taskMCSkinsLogC; } QNetworkReply* SkinDelete::getReply(QNetworkRequest& request) { setStatus(tr("Deleting skin")); return m_network->deleteResource(request); } SkinDelete::Ptr SkinDelete::make(QString token) { auto up = makeShared(); up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"); up->m_sink.reset(new Net::DummySink()); up->addHeaderProxy(std::make_unique(QList{ { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, })); return up; } PrismLauncher-11.0.3/launcher/minecraft/skins/SkinList.cpp0000644000175100017510000002761415224505336023141 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "SkinList.h" #include #include #include "FileSystem.h" #include "Json.h" #include "minecraft/skins/SkinModel.h" SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QAbstractListModel(parent), m_acct(acct) { FS::ensureFolderPathExists(m_dir.absolutePath()); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher.reset(new QFileSystemWatcher(this)); m_isWatching = false; connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &SkinList::directoryChanged); connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &SkinList::fileChanged); directoryChanged(path); } void SkinList::startWatching() { if (m_isWatching) { return; } update(); m_isWatching = m_watcher->addPath(m_dir.absolutePath()); if (m_isWatching) { qDebug() << "Started watching" << m_dir.absolutePath(); } else { qDebug() << "Failed to start watching" << m_dir.absolutePath(); } } void SkinList::stopWatching() { save(); if (!m_isWatching) { return; } m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); if (!m_isWatching) { qDebug() << "Stopped watching" << m_dir.absolutePath(); } else { qDebug() << "Failed to stop watching" << m_dir.absolutePath(); } } bool SkinList::update() { QList newSkins; m_dir.refresh(); auto manifestInfo = QFileInfo(m_dir.absoluteFilePath("index.json")); if (manifestInfo.exists()) { try { auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file"); const auto root = doc.object(); auto skins = root["skins"].toArray(); for (auto jSkin : skins) { SkinModel s(m_dir, jSkin.toObject()); if (s.isValid()) { newSkins << s; } } } catch (const Exception& e) { qCritical() << "Couldn't load skins json:" << e.cause(); } } bool needsSave = false; const auto& skin = m_acct->accountData()->minecraftProfile.skin; if (!skin.url.isEmpty() && !skin.data.isEmpty()) { QPixmap skinTexture; SkinModel* nskin = nullptr; for (auto i = 0; i < newSkins.size(); i++) { if (newSkins[i].getURL() == skin.url) { nskin = &newSkins[i]; break; } } if (!nskin) { auto name = m_acct->profileName() + ".png"; if (QFileInfo(m_dir.absoluteFilePath(name)).exists()) { name = QUrl(skin.url).fileName() + ".png"; } auto path = m_dir.absoluteFilePath(name); if (skinTexture.loadFromData(skin.data, "PNG") && skinTexture.save(path)) { SkinModel s(path); s.setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); s.setCapeId(m_acct->accountData()->minecraftProfile.currentCape); s.setURL(skin.url); newSkins << s; needsSave = true; } } else { nskin->setCapeId(m_acct->accountData()->minecraftProfile.currentCape); nskin->setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); } } auto folderContents = m_dir.entryInfoList(); // if there are any untracked files... for (QFileInfo entry : folderContents) { if (!entry.isFile() || entry.suffix() != "png") continue; SkinModel w(entry.absoluteFilePath()); if (w.isValid()) { auto add = true; for (auto s : newSkins) { if (s.name() == w.name()) { add = false; break; } } if (add) { newSkins.append(w); needsSave = true; } } } std::sort(newSkins.begin(), newSkins.end(), [](const SkinModel& a, const SkinModel& b) { return a.getPath().localeAwareCompare(b.getPath()) < 0; }); beginResetModel(); m_skinList.swap(newSkins); endResetModel(); if (needsSave) save(); return true; } void SkinList::directoryChanged(const QString& path) { QDir new_dir(path); if (!new_dir.exists()) if (!FS::ensureFolderPathExists(new_dir.absolutePath())) return; if (m_dir.absolutePath() != new_dir.absolutePath()) { m_dir.setPath(path); m_dir.refresh(); if (m_isWatching) stopWatching(); startWatching(); } update(); } void SkinList::fileChanged(const QString& path) { qDebug() << "Checking" << path; QFileInfo checkfile(path); if (!checkfile.exists()) return; for (int i = 0; i < m_skinList.count(); i++) { if (m_skinList[i].getPath() == checkfile.absoluteFilePath()) { m_skinList[i].refresh(); dataChanged(index(i), index(i)); break; } } } QStringList SkinList::mimeTypes() const { return { "text/uri-list" }; } Qt::DropActions SkinList::supportedDropActions() const { return Qt::CopyAction; } bool SkinList::dropMimeData(const QMimeData* data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex& parent) { if (action == Qt::IgnoreAction) return true; // check if the action is supported if (!data || !(action & supportedDropActions())) return false; // files dropped from outside? if (data->hasUrls()) { auto urls = data->urls(); QStringList skinFiles; for (auto url : urls) { // only local files may be dropped... if (!url.isLocalFile()) continue; skinFiles << url.toLocalFile(); } installSkins(skinFiles); return true; } return false; } Qt::ItemFlags SkinList::flags(const QModelIndex& index) const { Qt::ItemFlags f = Qt::ItemIsDropEnabled | QAbstractListModel::flags(index); if (index.isValid()) { f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); } return f; } QVariant SkinList::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); int row = index.row(); if (row < 0 || row >= m_skinList.size()) return QVariant(); auto skin = m_skinList[row]; switch (role) { case Qt::DecorationRole: { auto preview = skin.getPreview(); if (preview.isNull()) { preview = skin.getTexture(); } return preview; } case Qt::DisplayRole: return skin.name(); case Qt::UserRole: return skin.name(); case Qt::EditRole: return skin.name(); default: return QVariant(); } } int SkinList::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_skinList.size(); } void SkinList::installSkins(const QStringList& iconFiles) { for (QString file : iconFiles) installSkin(file); } QString getUniqueFile(const QString& root, const QString& file) { auto result = FS::PathCombine(root, file); if (!QFileInfo::exists(result)) { return result; } QString baseName = QFileInfo(file).completeBaseName(); QString extension = QFileInfo(file).suffix(); int tries = 0; while (QFileInfo::exists(result)) { if (++tries > 256) return {}; QString key = QString("%1%2.%3").arg(baseName).arg(tries).arg(extension); result = FS::PathCombine(root, key); } return result; } QString SkinList::installSkin(const QString& file, const QString& name) { if (file.isEmpty()) return tr("Path is empty."); QFileInfo fileinfo(file); if (!fileinfo.exists()) return tr("File doesn't exist."); if (!fileinfo.isFile()) return tr("Not a file."); if (!fileinfo.isReadable()) return tr("File is not readable."); if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid()) return tr("Skin images must be 64x64 or 64x32 pixel PNG files."); QString target = getUniqueFile(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name); return QFile::copy(file, target) ? "" : tr("Unable to copy file"); } int SkinList::getSkinIndex(const QString& key) const { for (int i = 0; i < m_skinList.count(); i++) { if (m_skinList[i].name() == key) { return i; } } return -1; } const SkinModel* SkinList::skin(const QString& key) const { int idx = getSkinIndex(key); if (idx == -1) return nullptr; return &m_skinList[idx]; } SkinModel* SkinList::skin(const QString& key) { int idx = getSkinIndex(key); if (idx == -1) return nullptr; return &m_skinList[idx]; } bool SkinList::deleteSkin(const QString& key, bool trash) { int idx = getSkinIndex(key); if (idx != -1) { auto s = m_skinList[idx]; if (trash) { if (FS::trash(s.getPath(), nullptr)) { m_skinList.remove(idx); save(); return true; } } else if (QFile::remove(s.getPath())) { m_skinList.remove(idx); save(); return true; } } return false; } void SkinList::save() { QJsonObject doc; QJsonArray arr; for (auto s : m_skinList) { arr << s.toJSON(); } doc["skins"] = arr; try { Json::write(doc, m_dir.absoluteFilePath("index.json")); } catch (const FS::FileSystemException& e) { qCritical() << "Failed to write skin index file :" << e.cause(); } } int SkinList::getSelectedAccountSkin() { const auto& skin = m_acct->accountData()->minecraftProfile.skin; for (int i = 0; i < m_skinList.count(); i++) { if (m_skinList[i].getURL() == skin.url) { return i; } } return -1; } bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) { if (!idx.isValid() || role != Qt::EditRole) { return false; } int row = idx.row(); if (row < 0 || row >= m_skinList.size()) return false; auto& skin = m_skinList[row]; auto newName = value.toString(); if (skin.name() != newName) { if (!skin.rename(newName)) return false; save(); } return true; } void SkinList::updateSkin(SkinModel* s) { auto done = false; for (auto i = 0; i < m_skinList.size(); i++) { if (m_skinList[i].getPath() == s->getPath()) { m_skinList[i].setCapeId(s->getCapeId()); m_skinList[i].setModel(s->getModel()); m_skinList[i].setURL(s->getURL()); done = true; break; } } if (!done) { beginInsertRows(QModelIndex(), m_skinList.count(), m_skinList.count() + 1); m_skinList.append(*s); endInsertRows(); } save(); } PrismLauncher-11.0.3/launcher/minecraft/VersionFile.cpp0000644000175100017510000000634015224505336022470 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include "ParseUtils.h" #include "minecraft/Library.h" #include "minecraft/PackProfile.h" #include "minecraft/VersionFile.h" #include static bool isMinecraftVersion(const QString& uid) { return uid == "net.minecraft"; } void VersionFile::applyTo(LaunchProfile* profile, const RuntimeContext& runtimeContext) { // Only real Minecraft can set those. Don't let anything override them. if (isMinecraftVersion(uid)) { profile->applyMinecraftVersion(version); profile->applyMinecraftVersionType(type); // HACK: ignore assets from other version files than Minecraft // workaround for stupid assets issue caused by amazon: // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/ profile->applyMinecraftAssets(mojangAssetIndex); } profile->applyMainJar(mainJar); profile->applyMainClass(mainClass); profile->applyAppletClass(appletClass); profile->applyMinecraftArguments(minecraftArguments); profile->applyAddnJvmArguments(addnJvmArguments); profile->applyTweakers(addTweakers); profile->applyJarMods(jarMods); profile->applyMods(mods); profile->applyTraits(traits); profile->applyCompatibleJavaMajors(compatibleJavaMajors); profile->applyCompatibleJavaName(compatibleJavaName); for (auto library : libraries) { profile->applyLibrary(library, runtimeContext); } for (auto mavenFile : mavenFiles) { profile->applyMavenFile(mavenFile, runtimeContext); } for (auto agent : agents) { profile->applyAgent(agent, runtimeContext); } profile->applyProblemSeverity(getProblemSeverity()); } PrismLauncher-11.0.3/launcher/minecraft/OneSixVersionFormat.cpp0000644000175100017510000003626215224505336024175 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "OneSixVersionFormat.h" #include #include #include #include "java/JavaMetadata.h" #include "minecraft/Agent.h" #include "minecraft/ParseUtils.h" #include using namespace Json; static void readString(const QJsonObject& root, const QString& key, QString& variable) { if (root.contains(key)) { variable = requireString(root.value(key)); } } LibraryPtr OneSixVersionFormat::libraryFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename) { LibraryPtr out = MojangVersionFormat::libraryFromJson(problems, libObj, filename); readString(libObj, "MMC-hint", out->m_hint); readString(libObj, "MMC-absulute_url", out->m_absoluteURL); readString(libObj, "MMC-absoluteUrl", out->m_absoluteURL); readString(libObj, "MMC-filename", out->m_filename); readString(libObj, "MMC-displayname", out->m_displayname); return out; } QJsonObject OneSixVersionFormat::libraryToJson(Library* library) { QJsonObject libRoot = MojangVersionFormat::libraryToJson(library); if (!library->m_absoluteURL.isEmpty()) libRoot.insert("MMC-absoluteUrl", library->m_absoluteURL); if (!library->m_hint.isEmpty()) libRoot.insert("MMC-hint", library->m_hint); if (!library->m_filename.isEmpty()) libRoot.insert("MMC-filename", library->m_filename); if (!library->m_displayname.isEmpty()) libRoot.insert("MMC-displayname", library->m_displayname); return libRoot; } VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc, const QString& filename, const bool requireOrder) { VersionFilePtr out(new VersionFile()); if (doc.isEmpty() || doc.isNull()) { throw JSONValidationError(filename + " is empty or null"); } if (!doc.isObject()) { throw JSONValidationError(filename + " is not an object"); } QJsonObject root = doc.object(); Meta::MetadataVersion formatVersion = Meta::parseFormatVersion(root, false); switch (formatVersion) { case Meta::MetadataVersion::InitialRelease: break; case Meta::MetadataVersion::Invalid: throw JSONValidationError(filename + " does not contain a recognizable version of the metadata format."); } if (requireOrder) { if (root.contains("order")) { out->order = requireInteger(root.value("order")); } else { // FIXME: evaluate if we don't want to throw exceptions here instead qCritical() << filename << "doesn't contain an order field"; } } out->name = root.value("name").toString(); if (root.contains("uid")) { out->uid = root.value("uid").toString(); } else { out->uid = root.value("fileId").toString(); } static const QRegularExpression s_validUidRegex{ QRegularExpression::anchoredPattern( QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) }; if (!s_validUidRegex.match(out->uid).hasMatch()) { qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid; out->addProblem(ProblemSeverity::Error, QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues.")); } out->version = root.value("version").toString(); MojangVersionFormat::readVersionProperties(root, out.get()); // added for legacy Minecraft window embedding, TODO: remove readString(root, "appletClass", out->appletClass); if (root.contains("+tweakers")) { for (auto tweakerVal : requireArray(root.value("+tweakers"))) { out->addTweakers.append(requireString(tweakerVal)); } } if (root.contains("+traits")) { for (auto tweakerVal : requireArray(root.value("+traits"))) { out->traits.insert(requireString(tweakerVal)); } } if (root.contains("+jvmArgs")) { for (auto arg : requireArray(root.value("+jvmArgs"))) { out->addnJvmArguments.append(requireString(arg)); } } if (root.contains("jarMods")) { for (auto libVal : requireArray(root.value("jarMods"))) { QJsonObject libObj = requireObject(libVal); // parse the jarmod auto lib = OneSixVersionFormat::jarModFromJson(*out, libObj, filename); // and add to jar mods out->jarMods.append(lib); } } else if (root.contains("+jarMods")) // DEPRECATED: old style '+jarMods' are only here for backwards compatibility { for (auto libVal : requireArray(root.value("+jarMods"))) { QJsonObject libObj = requireObject(libVal); // parse the jarmod auto lib = OneSixVersionFormat::plusJarModFromJson(*out, libObj, filename, out->name); // and add to jar mods out->jarMods.append(lib); } } if (root.contains("mods")) { for (auto libVal : requireArray(root.value("mods"))) { QJsonObject libObj = requireObject(libVal); // parse the jarmod auto lib = OneSixVersionFormat::modFromJson(*out, libObj, filename); // and add to jar mods out->mods.append(lib); } } auto readLibs = [&root, &out, &filename](const char* which, QList& outList) { for (auto libVal : requireArray(root.value(which))) { QJsonObject libObj = requireObject(libVal); // parse the library auto lib = libraryFromJson(*out, libObj, filename); outList.append(lib); } }; bool hasPlusLibs = root.contains("+libraries"); bool hasLibs = root.contains("libraries"); if (hasPlusLibs && hasLibs) { out->addProblem(ProblemSeverity::Warning, QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported.")); readLibs("libraries", out->libraries); readLibs("+libraries", out->libraries); } else if (hasLibs) { readLibs("libraries", out->libraries); } else if (hasPlusLibs) { readLibs("+libraries", out->libraries); } if (root.contains("mavenFiles")) { readLibs("mavenFiles", out->mavenFiles); } if (root.contains("+agents")) { for (auto agentVal : requireArray(root.value("+agents"))) { QJsonObject agentObj = requireObject(agentVal); auto lib = libraryFromJson(*out, agentObj, filename); QString arg = ""; readString(agentObj, "argument", arg); out->agents.append(Agent{ lib, arg }); } } // if we have mainJar, just use it if (root.contains("mainJar")) { QJsonObject libObj = requireObject(root, "mainJar"); out->mainJar = libraryFromJson(*out, libObj, filename); } // else reconstruct it from downloads and id ... if that's available else if (!out->minecraftVersion.isEmpty()) { auto lib = std::make_shared(); lib->setRawName(GradleSpecifier(QString("com.mojang:minecraft:%1:client").arg(out->minecraftVersion))); // we have a reliable client download, use it. if (out->mojangDownloads.contains("client")) { auto LibDLInfo = std::make_shared(); LibDLInfo->artifact = out->mojangDownloads["client"]; lib->setMojangDownloadInfo(LibDLInfo); } // we got nothing... else { out->addProblem( ProblemSeverity::Error, QObject::tr("URL for the main jar could not be determined - Mojang removed the server that we used as fallback.")); } out->mainJar = lib; } if (root.contains("requires")) { Meta::parseRequires(root, &out->m_requires); } QString dependsOnMinecraftVersion = root.value("mcVersion").toString(); if (!dependsOnMinecraftVersion.isEmpty()) { Meta::Require mcReq; mcReq.uid = "net.minecraft"; mcReq.equalsVersion = dependsOnMinecraftVersion; if (out->m_requires.count(mcReq) == 0) { out->m_requires.insert(mcReq); } } if (root.contains("conflicts")) { Meta::parseRequires(root, &out->conflicts); } if (root.contains("volatile")) { out->m_volatile = requireBoolean(root, "volatile"); } if (root.contains("runtimes")) { out->runtimes = {}; for (auto runtime : root["runtimes"].toArray()) { out->runtimes.append(Java::parseJavaMeta(runtime.toObject())); } } /* removed features that shouldn't be used */ if (root.contains("tweakers")) { out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element 'tweakers'")); } if (root.contains("-libraries")) { out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-libraries'")); } if (root.contains("-tweakers")) { out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-tweakers'")); } if (root.contains("-minecraftArguments")) { out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-minecraftArguments'")); } if (root.contains("+minecraftArguments")) { out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '+minecraftArguments'")); } return out; } QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr& patch) { QJsonObject root; writeString(root, "name", patch->name); writeString(root, "uid", patch->uid); writeString(root, "version", patch->version); Meta::serializeFormatVersion(root, Meta::MetadataVersion::InitialRelease); MojangVersionFormat::writeVersionProperties(patch.get(), root); if (patch->mainJar) { root.insert("mainJar", libraryToJson(patch->mainJar.get())); } writeString(root, "appletClass", patch->appletClass); writeStringList(root, "+tweakers", patch->addTweakers); writeStringList(root, "+traits", patch->traits.values()); writeStringList(root, "+jvmArgs", patch->addnJvmArguments); if (!patch->agents.isEmpty()) { QJsonArray array; for (const auto& value : patch->agents) { QJsonObject agentOut = OneSixVersionFormat::libraryToJson(value.library.get()); if (!value.argument.isEmpty()) agentOut.insert("argument", value.argument); array.append(agentOut); } root.insert("+agents", array); } if (!patch->libraries.isEmpty()) { QJsonArray array; for (auto value : patch->libraries) { array.append(OneSixVersionFormat::libraryToJson(value.get())); } root.insert("libraries", array); } if (!patch->mavenFiles.isEmpty()) { QJsonArray array; for (auto value : patch->mavenFiles) { array.append(OneSixVersionFormat::libraryToJson(value.get())); } root.insert("mavenFiles", array); } if (!patch->jarMods.isEmpty()) { QJsonArray array; for (auto value : patch->jarMods) { array.append(OneSixVersionFormat::jarModtoJson(value.get())); } root.insert("jarMods", array); } if (!patch->mods.isEmpty()) { QJsonArray array; for (auto value : patch->jarMods) { array.append(OneSixVersionFormat::modtoJson(value.get())); } root.insert("mods", array); } if (!patch->m_requires.empty()) { Meta::serializeRequires(root, &patch->m_requires, "requires"); } if (!patch->conflicts.empty()) { Meta::serializeRequires(root, &patch->conflicts, "conflicts"); } if (patch->m_volatile) { root.insert("volatile", true); } // write the contents to a json document. { QJsonDocument out; out.setObject(root); return out; } } LibraryPtr OneSixVersionFormat::plusJarModFromJson([[maybe_unused]] ProblemContainer& problems, const QJsonObject& libObj, const QString& filename, const QString& originalName) { LibraryPtr out(new Library()); if (!libObj.contains("name")) { throw JSONValidationError(filename + "contains a jarmod that doesn't have a 'name' field"); } // just make up something unique on the spot for the library name. QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); out->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1")); // filename override is the old name out->setFilename(libObj.value("name").toString()); // it needs to be local, it is stored in the instance jarmods folder out->setHint("local"); // read the original name if present - some versions did not set it // it is the original jar mod filename before it got renamed at the point of addition auto displayName = libObj.value("originalName").toString(); if (displayName.isEmpty()) { auto fixed = originalName; fixed.remove(" (jar mod)"); out->setDisplayName(fixed); } else { out->setDisplayName(displayName); } return out; } LibraryPtr OneSixVersionFormat::jarModFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename) { return libraryFromJson(problems, libObj, filename); } QJsonObject OneSixVersionFormat::jarModtoJson(Library* jarmod) { return libraryToJson(jarmod); } LibraryPtr OneSixVersionFormat::modFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename) { return libraryFromJson(problems, libObj, filename); } QJsonObject OneSixVersionFormat::modtoJson(Library* jarmod) { return libraryToJson(jarmod); } PrismLauncher-11.0.3/launcher/minecraft/World.h0000644000175100017510000000604415224505336021000 0ustar runnerrunner/* Copyright 2015-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 #include #include struct GameType { GameType() = default; GameType(std::optional original); QString toTranslatedString() const; QString toLogString() const; enum { Unknown = -1, Survival, Creative, Adventure, Spectator } type = Unknown; std::optional original; }; class World { public: World(const QFileInfo& file); QString folderName() const { return m_folderName; } QString name() const { return m_actualName; } QString iconFile() const { return m_iconFile; } int64_t bytes() const { return m_size; } QDateTime lastPlayed() const { return m_lastPlayed; } GameType gameType() const { return m_gameType; } int64_t seed() const { return m_randomSeed; } bool isValid() const { return m_isValid; } bool isOnFS() const { return m_containerFile.isDir(); } QFileInfo container() const { return m_containerFile; } // delete all the files of this world bool destroy(); // replace this world with a copy of the other bool replace(World& with); // change the world's filesystem path (used by world lists for *MAGIC* purposes) void repath(const QFileInfo& file); // remove the icon file, if any bool resetIcon(); bool rename(const QString& to); bool install(const QString& to, const QString& name = QString()); void setSize(int64_t size); // WEAK compare operator - used for replacing worlds bool operator==(const World& other) const; auto isSymLink() const -> bool { return m_containerFile.isSymLink(); } /** * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance * * @param instPath path to an instance directory * @return true * @return false */ bool isSymLinkUnder(const QString& instPath) const; bool isMoreThanOneHardLink() const; QString canonicalFilePath() const { return m_containerFile.canonicalFilePath(); } private: void readFromZip(const QFileInfo& file); void readFromFS(const QFileInfo& file); void loadFromLevelDat(QByteArray data); protected: QFileInfo m_containerFile; QString m_containerOffsetPath; QString m_folderName; QString m_actualName; QString m_iconFile; QDateTime m_levelDatTime; QDateTime m_lastPlayed; int64_t m_size = 0; int64_t m_randomSeed = 0; GameType m_gameType; bool m_isValid = false; }; PrismLauncher-11.0.3/launcher/minecraft/Component.h0000644000175100017510000001141615224505336021652 0ustar runnerrunner#pragma once #include #include #include #include #include #include #include "ProblemProvider.h" #include "QObjectPtr.h" #include "meta/JsonFormat.h" #include "modplatform/ModIndex.h" class PackProfile; class LaunchProfile; namespace Meta { class Version; class VersionList; } // namespace Meta class VersionFile; struct UpdateActionChangeVersion { /// version to change to QString targetVersion; }; struct UpdateActionLatestRecommendedCompatible { /// Parent uid QString parentUid; QString parentName; /// Parent version QString version; /// }; struct UpdateActionRemove {}; struct UpdateActionImportantChanged { QString oldVersion; }; using UpdateActionNone = std::monostate; using UpdateAction = std::variant; struct ModloaderMapEntry { ModPlatform::ModLoaderType type; QStringList knownConflictingComponents; }; class Component : public QObject, public ProblemProvider { Q_OBJECT public: Component(PackProfile* parent, const QString& uid); // DEPRECATED: remove these constructors? Component(PackProfile* parent, const QString& uid, std::shared_ptr file); virtual ~Component() {} static const QMap KNOWN_MODLOADERS; void applyTo(LaunchProfile* profile); bool isEnabled(); bool setEnabled(bool state); bool canBeDisabled(); bool isMoveable(); bool isCustomizable(); bool isRevertible(); bool isRemovable(); bool isCustom(); bool isVersionChangeable(bool wait = true); bool isKnownModloader(); QStringList knownConflictingComponents(); // DEPRECATED: explicit numeric order values, used for loading old non-component config. TODO: refactor and move to migration code void setOrder(int order); int getOrder(); QString getID(); QString getName(); QString getVersion(); std::shared_ptr getMeta(); QDateTime getReleaseDateTime(); QString getFilename(); std::shared_ptr getVersionFile() const; std::shared_ptr getVersionList() const; void setImportant(bool state); const QList getProblems() const override; ProblemSeverity getProblemSeverity() const override; void addComponentProblem(ProblemSeverity severity, const QString& description); void resetComponentProblems(); void setVersion(const QString& version); bool customize(); bool revert(); void updateCachedData(); void waitLoadMeta(); void setUpdateAction(const UpdateAction& action); void clearUpdateAction(); UpdateAction getUpdateAction(); signals: void dataChanged(); public: /* data */ PackProfile* m_parent; // BEGIN: persistent component list properties /// ID of the component QString m_uid; /// version of the component - when there's a custom json override, this is also the version the component reverts to QString m_version; /// if true, this has been added automatically to satisfy dependencies and may be automatically removed bool m_dependencyOnly = false; /// if true, the component is either the main component of the instance, or otherwise important and cannot be removed. bool m_important = false; /// if true, the component is disabled bool m_disabled = false; /// cached name for display purposes, taken from the version file (meta or local override) QString m_cachedName; /// cached version for display AND other purposes, taken from the version file (meta or local override) QString m_cachedVersion; /// cached set of requirements, taken from the version file (meta or local override) Meta::RequireSet m_cachedRequires; Meta::RequireSet m_cachedConflicts; /// if true, the component is volatile and may be automatically removed when no longer needed bool m_cachedVolatile = false; // END: persistent component list properties // DEPRECATED: explicit numeric order values, used for loading old non-component config. TODO: refactor and move to migration code bool m_orderOverride = false; int m_order = 0; // load state std::shared_ptr m_metaVersion; std::shared_ptr m_file; bool m_loaded = false; private: QList m_componentProblems; ProblemSeverity m_componentProblemSeverity = ProblemSeverity::None; UpdateAction m_updateAction = UpdateAction{ UpdateActionNone{} }; }; using ComponentPtr = shared_qobject_ptr; PrismLauncher-11.0.3/launcher/minecraft/ComponentUpdateTask.cpp0000644000175100017510000010467215224505336024202 0ustar runnerrunner#include "ComponentUpdateTask.h" #include #include "Component.h" #include "ComponentUpdateTask_p.h" #include "PackProfile.h" #include "PackProfile_p.h" #include "ProblemProvider.h" #include "Version.h" #include "cassert" #include "meta/Index.h" #include "meta/Version.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/OneSixVersionFormat.h" #include "minecraft/ProfileUtils.h" #include "net/Mode.h" #include "Application.h" #include "tasks/Task.h" #include "minecraft/Logging.h" /* * This is responsible for loading the components of a component list AND resolving dependency issues between them */ /* * FIXME: the 'one shot async task' nature of this does not fit the intended usage * Really, it should be a reactor/state machine that receives input from the application * and dynamically adapts to changing requirements... * * The reactor should be the only entry into manipulating the PackProfile. * See: https://en.wikipedia.org/wiki/Reactor_pattern */ /* * Or make this operate on a snapshot of the PackProfile state, then merge results in as long as the snapshot and PackProfile didn't change? * If the component list changes, start over. */ /* * TODO: This task launches multiple other tasks. As such it should be converted to a ConcurrentTask */ ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list) : Task() { d.reset(new ComponentUpdateTaskData); d->m_profile = list; d->mode = mode; d->netmode = netmode; } ComponentUpdateTask::~ComponentUpdateTask() {} bool ComponentUpdateTask::canAbort() const { for (const auto& status : d->remoteLoadStatusList) { if (status.task && !status.task->canAbort()) { return false; } } return true; } bool ComponentUpdateTask::abort() { bool aborted = true; for (const auto& status : d->remoteLoadStatusList) { if (status.task && !status.task->abort()) { aborted = false; } } return aborted; } Net::Mode ComponentUpdateTask::netMode() { return d->netmode; } void ComponentUpdateTask::executeTask() { qCDebug(instanceProfileResolveC) << "Loading components"; setStatus(tr("Loading components")); loadComponents(); } namespace { enum class LoadResult { LoadedLocal, RequiresRemote, Failed }; LoadResult composeLoadResult(LoadResult a, LoadResult b) { if (a < b) { return b; } return a; } static LoadResult loadComponent(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode) { if (component->m_loaded) { qCDebug(instanceProfileResolveC) << component->getName() << "is already loaded"; return LoadResult::LoadedLocal; } LoadResult result = LoadResult::Failed; auto customPatchFilename = component->getFilename(); if (QFile::exists(customPatchFilename)) { // if local file exists... // check for uid problems inside... bool fileChanged = false; auto file = ProfileUtils::parseJsonFile(QFileInfo(customPatchFilename), false); if (file->uid != component->m_uid) { file->uid = component->m_uid; fileChanged = true; } if (fileChanged) { // FIXME: @QUALITY do not ignore return value ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), customPatchFilename); } component->m_file = file; component->m_loaded = true; result = LoadResult::LoadedLocal; } else { auto metaVersion = APPLICATION->metadataIndex()->get(component->m_uid, component->m_version); component->m_metaVersion = metaVersion; if (metaVersion->isLoaded()) { component->m_loaded = true; result = LoadResult::LoadedLocal; } else { loadTask = APPLICATION->metadataIndex()->loadVersion(component->m_uid, component->m_version, netmode); loadTask->start(); if (netmode == Net::Mode::Online) result = LoadResult::RequiresRemote; else if (metaVersion->isLoaded()) result = LoadResult::LoadedLocal; else result = LoadResult::Failed; } } return result; } // FIXME: dead code. determine if this can still be useful? /* static LoadResult loadPackProfile(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode) { if(component->m_loaded) { qDebug() << component->getName() << "is already loaded"; return LoadResult::LoadedLocal; } LoadResult result = LoadResult::Failed; auto metaList = APPLICATION->metadataIndex()->get(component->m_uid); if(metaList->isLoaded()) { component->m_loaded = true; result = LoadResult::LoadedLocal; } else { metaList->load(netmode); loadTask = metaList->getCurrentTask(); result = LoadResult::RequiresRemote; } return result; } */ } // namespace void ComponentUpdateTask::loadComponents() { LoadResult result = LoadResult::LoadedLocal; size_t taskIndex = 0; size_t componentIndex = 0; d->remoteLoadSuccessful = true; // load all the components OR their lists... for (auto component : d->m_profile->d->components) { Task::Ptr loadTask; LoadResult singleResult; RemoteLoadStatus::Type loadType; component->resetComponentProblems(); // FIXME: to do this right, we need to load the lists and decide on which versions to use during dependency resolution. For now, // ignore all that... #if 0 switch(d->mode) { case Mode::Launch: { singleResult = loadComponent(component, loadTask, d->netmode); loadType = RemoteLoadStatus::Type::Version; break; } case Mode::Resolution: { singleResult = loadPackProfile(component, loadTask, d->netmode); loadType = RemoteLoadStatus::Type::List; break; } } #else singleResult = loadComponent(component, loadTask, d->netmode); loadType = RemoteLoadStatus::Type::Version; #endif if (singleResult == LoadResult::LoadedLocal) { component->updateCachedData(); } result = composeLoadResult(result, singleResult); if (loadTask) { qCDebug(instanceProfileResolveC) << d->m_profile->d->m_instance->name() << "|" << "Remote loading is being run for" << component->getName(); connect(loadTask.get(), &Task::succeeded, this, [this, taskIndex]() { remoteLoadSucceeded(taskIndex); }); connect(loadTask.get(), &Task::failed, this, [this, taskIndex](const QString& error) { remoteLoadFailed(taskIndex, error); }); connect(loadTask.get(), &Task::aborted, this, [this, taskIndex]() { remoteLoadFailed(taskIndex, tr("Aborted")); }); RemoteLoadStatus status; status.type = loadType; status.PackProfileIndex = componentIndex; status.task = loadTask; d->remoteLoadStatusList.append(status); taskIndex++; } componentIndex++; } d->remoteTasksInProgress = taskIndex; m_progressTotal = static_cast(taskIndex); switch (result) { case LoadResult::LoadedLocal: { // Everything got loaded. Advance to dependency resolution. performUpdateActions(); resolveDependencies(d->mode == Mode::Launch || d->netmode == Net::Mode::Offline); return; } case LoadResult::RequiresRemote: { // we wait for signals. break; } case LoadResult::Failed: { emitFailed(tr("Some component metadata load tasks failed.")); return; } } setDetails(tr("Downloading metadata for %1 components").arg(taskIndex)); } namespace { struct RequireEx : public Meta::Require { size_t indexOfFirstDependee = 0; }; struct RequireCompositionResult { bool ok; RequireEx outcome; }; using RequireExSet = std::set; } // namespace static RequireCompositionResult composeRequirement(const RequireEx& a, const RequireEx& b) { assert(a.uid == b.uid); RequireEx out; out.uid = a.uid; out.indexOfFirstDependee = std::min(a.indexOfFirstDependee, b.indexOfFirstDependee); if (a.equalsVersion.isEmpty()) { out.equalsVersion = b.equalsVersion; } else if (b.equalsVersion.isEmpty()) { out.equalsVersion = a.equalsVersion; } else if (a.equalsVersion == b.equalsVersion) { out.equalsVersion = a.equalsVersion; } else { // FIXME: mark error as explicit version conflict return { false, out }; } if (a.suggests.isEmpty()) { out.suggests = b.suggests; } else if (b.suggests.isEmpty()) { out.suggests = a.suggests; } else { Version aVer(a.suggests); Version bVer(b.suggests); out.suggests = (aVer < bVer ? b.suggests : a.suggests); } return { true, out }; } // gather the requirements from all components, finding any obvious conflicts static bool gatherRequirementsFromComponents(const ComponentContainer& input, RequireExSet& output) { bool succeeded = true; size_t componentNum = 0; for (auto component : input) { auto& componentRequires = component->m_cachedRequires; for (const auto& componentRequire : componentRequires) { auto found = std::find_if(output.cbegin(), output.cend(), [componentRequire](const Meta::Require& req) { return req.uid == componentRequire.uid; }); RequireEx componenRequireEx; componenRequireEx.uid = componentRequire.uid; componenRequireEx.suggests = componentRequire.suggests; componenRequireEx.equalsVersion = componentRequire.equalsVersion; componenRequireEx.indexOfFirstDependee = componentNum; if (found != output.cend()) { // found... process it further auto result = composeRequirement(componenRequireEx, *found); if (result.ok) { output.erase(componenRequireEx); output.insert(result.outcome); } else { qCCritical(instanceProfileResolveC) << "Conflicting requirements:" << componentRequire.uid << "versions:" << componentRequire.equalsVersion << ";" << (*found).equalsVersion; } succeeded &= result.ok; } else { // not found, accumulate output.insert(componenRequireEx); } } componentNum++; } return succeeded; } /// Get list of uids that can be trivially removed because nothing is depending on them anymore (and they are installed as deps) static void getTrivialRemovals(const ComponentContainer& components, const RequireExSet& reqs, QStringList& toRemove) { for (const auto& component : components) { if (!component->m_dependencyOnly) continue; if (!component->m_cachedVolatile) continue; RequireEx reqNeedle; reqNeedle.uid = component->m_uid; const auto iter = reqs.find(reqNeedle); if (iter == reqs.cend()) { toRemove.append(component->m_uid); } } } /** * handles: * - trivial addition (there is an unmet requirement and it can be trivially met by adding something) * - trivial version conflict of dependencies == explicit version required and installed is different * * toAdd - set of requirements than mean adding a new component * toChange - set of requirements that mean changing version of an existing component */ static bool getTrivialComponentChanges(const ComponentIndex& index, const RequireExSet& input, RequireExSet& toAdd, RequireExSet& toChange) { enum class Decision { Undetermined, Met, Missing, VersionNotSame, LockedVersionNotSame } decision = Decision::Undetermined; QString reqStr; bool succeeded = true; // list the composed requirements and say if they are met or unmet for (auto& req : input) { do { if (req.equalsVersion.isEmpty()) { reqStr = QString("Req: %1").arg(req.uid); if (index.contains(req.uid)) { decision = Decision::Met; } else { toAdd.insert(req); decision = Decision::Missing; } break; } else { reqStr = QString("Req: %1 == %2").arg(req.uid, req.equalsVersion); const auto& compIter = index.find(req.uid); if (compIter == index.cend()) { toAdd.insert(req); decision = Decision::Missing; break; } auto& comp = (*compIter); if (comp->getVersion() != req.equalsVersion) { if (comp->isCustom()) { decision = Decision::LockedVersionNotSame; } else { if (comp->m_dependencyOnly) { decision = Decision::VersionNotSame; } else { decision = Decision::LockedVersionNotSame; } } break; } decision = Decision::Met; } } while (false); switch (decision) { case Decision::Undetermined: qCCritical(instanceProfileResolveC) << "No decision for" << reqStr; succeeded = false; break; case Decision::Met: qCDebug(instanceProfileResolveC) << reqStr << "Is met."; break; case Decision::Missing: qCDebug(instanceProfileResolveC) << reqStr << "Is missing and should be added at" << req.indexOfFirstDependee; toAdd.insert(req); break; case Decision::VersionNotSame: qCDebug(instanceProfileResolveC) << reqStr << "already has different version that can be changed."; toChange.insert(req); break; case Decision::LockedVersionNotSame: qCDebug(instanceProfileResolveC) << reqStr << "already has different version that cannot be changed."; succeeded = false; break; } } return succeeded; } ComponentContainer ComponentUpdateTask::collectTreeLinked(const QString& uid) { ComponentContainer linked; auto& components = d->m_profile->d->components; auto& componentIndex = d->m_profile->d->componentIndex; auto& instance = d->m_profile->d->m_instance; for (auto comp : components) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << "scanning" << comp->getID() << ":" << comp->getVersion() << "for tree link"; auto dep = std::find_if(comp->m_cachedRequires.cbegin(), comp->m_cachedRequires.cend(), [uid](const Meta::Require& req) -> bool { return req.uid == uid; }); if (dep != comp->m_cachedRequires.cend()) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << comp->getID() << ":" << comp->getVersion() << "depends on" << uid; linked.append(comp); } } auto iter = componentIndex.find(uid); if (iter != componentIndex.end()) { ComponentPtr comp = *iter; comp->updateCachedData(); qCDebug(instanceProfileResolveC) << instance->name() << "|" << comp->getID() << ":" << comp->getVersion() << "has" << comp->m_cachedRequires.size() << "dependencies"; for (auto dep : comp->m_cachedRequires) { qCDebug(instanceProfileC) << instance->name() << "|" << uid << "depends on" << dep.uid; auto found = componentIndex.find(dep.uid); if (found != componentIndex.end()) { qCDebug(instanceProfileC) << instance->name() << "|" << (*found)->getID() << "is present"; linked.append(*found); } } } return linked; } // FIXME, TODO: decouple dependency resolution from loading // FIXME: This works directly with the PackProfile internals. It shouldn't! It needs richer data types than PackProfile uses. // FIXME: throw all this away and use a graph void ComponentUpdateTask::resolveDependencies(bool checkOnly) { qCDebug(instanceProfileResolveC) << "Resolving dependencies"; /* * this is a naive dependency resolving algorithm. all it does is check for following conditions and react in simple ways: * 1. There are conflicting dependencies on the same uid with different exact version numbers * -> hard error * 2. A dependency has non-matching exact version number * -> hard error * 3. A dependency is entirely missing and needs to be injected before the dependee(s) * -> requirements are injected * * NOTE: this is a placeholder and should eventually be replaced with something 'serious' */ auto& components = d->m_profile->d->components; auto& componentIndex = d->m_profile->d->componentIndex; RequireExSet allRequires; QStringList toRemove; do { allRequires.clear(); toRemove.clear(); if (!gatherRequirementsFromComponents(components, allRequires)) { finalizeComponents(); emitFailed(tr("Conflicting requirements detected during dependency checking!")); return; } getTrivialRemovals(components, allRequires, toRemove); if (!toRemove.isEmpty()) { qCDebug(instanceProfileResolveC) << "Removing obsolete components..."; for (auto& remove : toRemove) { qCDebug(instanceProfileResolveC) << "Removing" << remove; d->m_profile->remove(remove); } } } while (!toRemove.isEmpty()); RequireExSet toAdd; RequireExSet toChange; bool succeeded = getTrivialComponentChanges(componentIndex, allRequires, toAdd, toChange); if (!succeeded) { finalizeComponents(); emitFailed(tr("Instance has conflicting dependencies.")); return; } if (checkOnly) { finalizeComponents(); if (toAdd.size() || toChange.size()) { emitFailed(tr("Instance has unresolved dependencies while loading/checking for launch.")); } else { emitSucceeded(); } return; } bool recursionNeeded = false; if (toAdd.size()) { // add stuff... for (auto& add : toAdd) { auto component = makeShared(d->m_profile, add.uid); if (!add.equalsVersion.isEmpty()) { // exact version qCDebug(instanceProfileResolveC) << "Adding" << add.uid << "version" << add.equalsVersion << "at position" << add.indexOfFirstDependee; component->m_version = add.equalsVersion; } else { // version needs to be decided qCDebug(instanceProfileResolveC) << "Adding" << add.uid << "at position" << add.indexOfFirstDependee; // ############################################################################################################ // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. if (!add.suggests.isEmpty()) { component->m_version = add.suggests; } else { if (add.uid == "org.lwjgl") { component->m_version = "2.9.1"; } else if (add.uid == "org.lwjgl3") { component->m_version = "3.1.2"; } else if (add.uid == "net.fabricmc.intermediary" || add.uid == "org.quiltmc.hashed") { auto minecraft = std::find_if(components.begin(), components.end(), [](ComponentPtr& cmp) { return cmp->getID() == "net.minecraft"; }); if (minecraft != components.end()) { component->m_version = (*minecraft)->getVersion(); } } } // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. // ############################################################################################################ } component->m_dependencyOnly = true; // FIXME: this should not work directly with the component list d->m_profile->insertComponent(add.indexOfFirstDependee, component); componentIndex[add.uid] = component; } recursionNeeded = true; } if (toChange.size()) { // change a version of something that exists for (auto& change : toChange) { // FIXME: this should not work directly with the component list qCDebug(instanceProfileResolveC) << "Setting version of" << change.uid << "to" << change.equalsVersion; auto component = componentIndex[change.uid]; component->setVersion(change.equalsVersion); } recursionNeeded = true; } if (recursionNeeded) { loadComponents(); } else { finalizeComponents(); emitSucceeded(); } } // Variant visitation via lambda template struct overload : Ts... { using Ts::operator()...; }; template overload(Ts...) -> overload; void ComponentUpdateTask::performUpdateActions() { auto& instance = d->m_profile->d->m_instance; bool addedActions; QStringList toRemove; do { addedActions = false; toRemove.clear(); auto& components = d->m_profile->d->components; auto& componentIndex = d->m_profile->d->componentIndex; for (auto component : components) { if (!component) { continue; } auto action = component->getUpdateAction(); auto visitor = overload{ [](const UpdateActionNone&) { // noop }, [&component, &instance](const UpdateActionChangeVersion& cv) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << "UpdateActionChangeVersion" << component->getID() << ":" << component->getVersion() << "change to" << cv.targetVersion; component->setVersion(cv.targetVersion); component->waitLoadMeta(); }, [&component, &instance](const UpdateActionLatestRecommendedCompatible& lrc) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << "UpdateActionLatestRecommendedCompatible" << component->getID() << ":" << component->getVersion() << "updating to latest recommend or compatible with" << lrc.parentUid << lrc.version; auto versionList = APPLICATION->metadataIndex()->get(component->getID()); if (versionList) { versionList->waitToLoad(); auto recommended = versionList->getRecommendedForParent(lrc.parentUid, lrc.version); if (!recommended) { recommended = versionList->getLatestForParent(lrc.parentUid, lrc.version); } if (recommended) { component->setVersion(recommended->version()); component->waitLoadMeta(); return; } else { component->addComponentProblem(ProblemSeverity::Error, QObject::tr("No compatible version of %1 found for %2 %3") .arg(component->getName(), lrc.parentName, lrc.version)); } } else { component->addComponentProblem( ProblemSeverity::Error, QObject::tr("No version list in metadata index for %1").arg(component->getID())); } }, [&component, &instance, &toRemove](const UpdateActionRemove&) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << "UpdateActionRemove" << component->getID() << ":" << component->getVersion() << "removing"; toRemove.append(component->getID()); }, [this, &component, &instance, &addedActions, &componentIndex](const UpdateActionImportantChanged& ic) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << "UpdateImportantChanged" << component->getID() << ":" << component->getVersion() << "was changed from" << ic.oldVersion << "updating linked components"; auto oldVersion = APPLICATION->metadataIndex()->getLoadedVersion(component->getID(), ic.oldVersion); for (auto oldReq : oldVersion->requiredSet()) { auto currentlyRequired = component->m_cachedRequires.find(oldReq); if (currentlyRequired == component->m_cachedRequires.cend()) { auto oldReqComp = componentIndex.find(oldReq.uid); if (oldReqComp != componentIndex.cend()) { (*oldReqComp)->setUpdateAction(UpdateAction{ UpdateActionRemove{} }); addedActions = true; } } } auto linked = collectTreeLinked(component->getID()); for (auto comp : linked) { if (comp->isCustom()) { continue; } auto compUid = comp->getID(); auto parentReq = std::find_if(component->m_cachedRequires.begin(), component->m_cachedRequires.end(), [compUid](const Meta::Require& req) { return req.uid == compUid; }); if (parentReq != component->m_cachedRequires.end()) { auto newVersion = parentReq->equalsVersion.isEmpty() ? parentReq->suggests : parentReq->equalsVersion; if (!newVersion.isEmpty()) { comp->setUpdateAction(UpdateAction{ UpdateActionChangeVersion{ newVersion } }); } else { comp->setUpdateAction(UpdateAction{ UpdateActionLatestRecommendedCompatible{ component->getID(), component->getName(), component->getVersion(), } }); } } else { comp->setUpdateAction(UpdateAction{ UpdateActionLatestRecommendedCompatible{ component->getID(), component->getName(), component->getVersion(), } }); } addedActions = true; } } }; std::visit(visitor, action); component->clearUpdateAction(); for (auto uid : toRemove) { d->m_profile->remove(uid); } } } while (addedActions); } void ComponentUpdateTask::finalizeComponents() { auto& components = d->m_profile->d->components; auto& componentIndex = d->m_profile->d->componentIndex; for (auto component : components) { for (auto req : component->m_cachedRequires) { auto found = componentIndex.find(req.uid); if (found == componentIndex.cend()) { component->addComponentProblem( ProblemSeverity::Error, QObject::tr("%1 is missing requirement %2 %3") .arg(component->getName(), req.uid, req.equalsVersion.isEmpty() ? req.suggests : req.equalsVersion)); } else { auto reqComp = *found; if (!reqComp->getProblems().isEmpty()) { component->addComponentProblem( reqComp->getProblemSeverity(), QObject::tr("%1, a dependency of this component, has reported issues").arg(reqComp->getName())); } if (!req.equalsVersion.isEmpty() && req.equalsVersion != reqComp->getVersion()) { component->addComponentProblem(ProblemSeverity::Error, QObject::tr("%1, a dependency of this component, is not the required version %2") .arg(reqComp->getName(), req.equalsVersion)); } else if (!req.suggests.isEmpty() && req.suggests != reqComp->getVersion()) { component->addComponentProblem(ProblemSeverity::Warning, QObject::tr("%1, a dependency of this component, is not the suggested version %2") .arg(reqComp->getName(), req.suggests)); } } } for (auto conflict : component->knownConflictingComponents()) { auto found = componentIndex.find(conflict); if (found != componentIndex.cend()) { auto foundComp = *found; if (foundComp->isCustom()) { continue; } component->addComponentProblem( ProblemSeverity::Warning, QObject::tr("%1 and %2 are known to not work together. It is recommended to remove one of them.") .arg(component->getName(), foundComp->getName())); } } } } void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) { if (static_cast(d->remoteLoadStatusList.size()) < taskIndex) { qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex; return; } auto& taskSlot = d->remoteLoadStatusList[taskIndex]; disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr); disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); if (taskSlot.finished) { qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex; return; } qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "succeeded"; taskSlot.succeeded = false; taskSlot.finished = true; d->remoteTasksInProgress--; // update the cached data of the component from the downloaded version file. if (taskSlot.type == RemoteLoadStatus::Type::Version) { auto component = d->m_profile->getComponent(taskSlot.PackProfileIndex); component->m_loaded = true; component->updateCachedData(); } checkIfAllFinished(); } void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) { if (static_cast(d->remoteLoadStatusList.size()) < taskIndex) { qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex; return; } auto& taskSlot = d->remoteLoadStatusList[taskIndex]; disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr); disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); if (taskSlot.finished) { qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex; return; } qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "failed:" << msg; d->remoteLoadSuccessful = false; taskSlot.succeeded = false; taskSlot.finished = true; d->remoteTasksInProgress--; checkIfAllFinished(); } void ComponentUpdateTask::checkIfAllFinished() { setProgress(m_progress + 1, m_progressTotal); if (d->remoteTasksInProgress) { // not yet... return; } if (d->remoteLoadSuccessful) { // nothing bad happened... clear the temp load status and proceed with looking at dependencies d->remoteLoadStatusList.clear(); performUpdateActions(); resolveDependencies(d->mode == Mode::Launch); } else { // remote load failed... report error and bail QStringList allErrorsList; for (auto& item : d->remoteLoadStatusList) { if (!item.succeeded) { const ComponentPtr component = d->m_profile->getComponent(item.PackProfileIndex); allErrorsList.append(tr("Could not download metadata for %1 %2. Please change the version or try again later.") .arg(component->getName(), component->m_version)); } } d->remoteLoadStatusList.clear(); auto allErrors = allErrorsList.join("\n"); emitFailed(tr("Component metadata update task failed while downloading from remote server:\n%1").arg(allErrors)); } } PrismLauncher-11.0.3/launcher/minecraft/VanillaInstanceCreationTask.cpp0000644000175100017510000000235515224505336025630 0ustar runnerrunner#include "VanillaInstanceCreationTask.h" #include #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "settings/INISettingsObject.h" VanillaCreationTask::VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version) : InstanceCreationTask() , m_version(std::move(version)) , m_using_loader(true) , m_loader(std::move(loader)) , m_loader_version(std::move(loader_version)) {} std::unique_ptr VanillaCreationTask::createInstance() { setStatus(tr("Creating instance from version %1").arg(m_version->name())); auto inst = std::make_unique(m_globalSettings, std::make_unique(FS::PathCombine(m_stagingPath, "instance.cfg")), m_stagingPath); SettingsObject::Lock lock(inst->settings()); auto components = inst->getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_version->descriptor(), true); if (m_using_loader) components->setComponentVersion(m_loader, m_loader_version->descriptor()); inst->setName(name()); inst->setIconKey(m_instIcon); return inst; } PrismLauncher-11.0.3/launcher/minecraft/WorldList.cpp0000644000175100017510000003127715224505336022175 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "WorldList.h" #include #include #include #include #include #include #include #include #include #include WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractListModel(), m_instance(instance), m_dir(dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher = new QFileSystemWatcher(this); m_isWatching = false; connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged); } void WorldList::startWatching() { if (m_isWatching) { return; } update(); m_isWatching = m_watcher->addPath(m_dir.absolutePath()); if (m_isWatching) { qDebug() << "Started watching" << m_dir.absolutePath(); } else { qDebug() << "Failed to start watching" << m_dir.absolutePath(); } } void WorldList::stopWatching() { if (!m_isWatching) { return; } m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); if (!m_isWatching) { qDebug() << "Stopped watching" << m_dir.absolutePath(); } else { qDebug() << "Failed to stop watching" << m_dir.absolutePath(); } } bool WorldList::update() { if (!isValid()) return false; QList newWorlds; m_dir.refresh(); auto folderContents = m_dir.entryInfoList(); // if there are any untracked files... for (QFileInfo entry : folderContents) { if (!entry.isDir()) continue; World w(entry); if (w.isValid()) { newWorlds.append(w); } } beginResetModel(); m_worlds.swap(newWorlds); endResetModel(); loadWorldsAsync(); return true; } void WorldList::directoryChanged(QString) { update(); } bool WorldList::isValid() { return m_dir.exists() && m_dir.isReadable(); } QString WorldList::instDirPath() const { return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); } bool WorldList::deleteWorld(int index) { if (index >= m_worlds.size() || index < 0) return false; World& m = m_worlds[index]; if (m.destroy()) { beginRemoveRows(QModelIndex(), index, index); m_worlds.removeAt(index); endRemoveRows(); emit changed(); return true; } return false; } bool WorldList::deleteWorlds(int first, int last) { for (int i = first; i <= last; i++) { World& m = m_worlds[i]; m.destroy(); } beginRemoveRows(QModelIndex(), first, last); m_worlds.erase(m_worlds.begin() + first, m_worlds.begin() + last + 1); endRemoveRows(); emit changed(); return true; } bool WorldList::resetIcon(int row) { if (row >= m_worlds.size() || row < 0) return false; World& m = m_worlds[row]; if (m.resetIcon()) { QModelIndex modelIndex = index(row, NameColumn); emit dataChanged(modelIndex, modelIndex, { WorldList::IconFileRole }); return true; } return false; } int WorldList::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 5; } QVariant WorldList::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); int row = index.row(); int column = index.column(); if (row < 0 || row >= m_worlds.size()) return QVariant(); QLocale locale; auto& world = m_worlds[row]; switch (role) { case Qt::DisplayRole: switch (column) { case NameColumn: return world.name(); case GameModeColumn: return world.gameType().toTranslatedString(); case LastPlayedColumn: return world.lastPlayed(); case SizeColumn: return locale.formattedDataSize(world.bytes()); case InfoColumn: if (world.isSymLinkUnder(instDirPath())) { return tr("This world is symbolically linked from elsewhere."); } if (world.isMoreThanOneHardLink()) { return tr("\nThis world is hard linked elsewhere."); } return ""; default: return QVariant(); } case Qt::UserRole: if (column == SizeColumn) return QVariant::fromValue(world.bytes()); return data(index, Qt::DisplayRole); case Qt::ToolTipRole: { if (column == InfoColumn) { if (world.isSymLinkUnder(instDirPath())) { return tr("Warning: This world is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") .arg(world.canonicalFilePath()); } if (world.isMoreThanOneHardLink()) { return tr("Warning: This world is hard linked elsewhere. Editing it will also change the original."); } } return world.folderName(); } case ObjectRole: { return QVariant::fromValue((void*)&world); } case FolderRole: { return QDir::toNativeSeparators(dir().absoluteFilePath(world.folderName())); } case SeedRole: { return QVariant::fromValue(world.seed()); } case NameRole: { return world.name(); } case LastPlayedRole: { return world.lastPlayed(); } case SizeRole: { return QVariant::fromValue(world.bytes()); } case IconFileRole: { return world.iconFile(); } default: return QVariant(); } } QVariant WorldList::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case NameColumn: return tr("Name"); case GameModeColumn: return tr("Game Mode"); case LastPlayedColumn: return tr("Last Played"); case SizeColumn: //: World size on disk return tr("Size"); case InfoColumn: //: special warnings? return tr("Info"); default: return QVariant(); } case Qt::ToolTipRole: switch (section) { case NameColumn: return tr("The name of the world."); case GameModeColumn: return tr("Game mode of the world."); case LastPlayedColumn: return tr("Date and time the world was last played."); case SizeColumn: return tr("Size of the world on disk."); case InfoColumn: return tr("Information and warnings about the world."); default: return QVariant(); } default: return QVariant(); } } QStringList WorldList::mimeTypes() const { QStringList types; types << "text/uri-list"; return types; } QMimeData* WorldList::mimeData(const QModelIndexList& indexes) const { QList urls; for (auto idx : indexes) { if (idx.column() != 0) continue; int row = idx.row(); if (row < 0 || row >= this->m_worlds.size()) continue; const World& world = m_worlds[row]; if (!world.isValid() || !world.isOnFS()) continue; QString worldPath = world.container().absoluteFilePath(); qDebug() << worldPath; urls.append(QUrl::fromLocalFile(worldPath)); } auto result = new QMimeData(); result->setUrls(urls); return result; } Qt::ItemFlags WorldList::flags(const QModelIndex& index) const { Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); if (index.isValid()) return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; else return Qt::ItemIsDropEnabled | defaultFlags; } Qt::DropActions WorldList::supportedDragActions() const { // move to other mod lists or VOID return Qt::MoveAction; } Qt::DropActions WorldList::supportedDropActions() const { // copy from outside, move from within and other mod lists return Qt::CopyAction | Qt::MoveAction; } void WorldList::installWorld(QFileInfo filename) { qDebug() << "installing:" << filename.absoluteFilePath(); World w(filename); if (!w.isValid()) { return; } w.install(m_dir.absolutePath()); } bool WorldList::dropMimeData(const QMimeData* data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex& parent) { if (action == Qt::IgnoreAction) return true; // check if the action is supported if (!data || !(action & supportedDropActions())) return false; // files dropped from outside? if (data->hasUrls()) { bool was_watching = m_isWatching; if (was_watching) stopWatching(); auto urls = data->urls(); for (auto url : urls) { // only local files may be dropped... if (!url.isLocalFile()) continue; QString filename = url.toLocalFile(); QFileInfo worldInfo(filename); if (!m_dir.entryInfoList().contains(worldInfo)) { installWorld(worldInfo); } } if (was_watching) startWatching(); return true; } return false; } int64_t calculateWorldSize(const QFileInfo& file) { if (file.isFile() && file.suffix() == "zip") { return file.size(); } else if (file.isDir()) { QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); int64_t total = 0; while (it.hasNext()) { it.next(); total += it.fileInfo().size(); } return total; } return -1; } void WorldList::loadWorldsAsync() { for (int i = 0; i < m_worlds.size(); ++i) { auto file = m_worlds.at(i).container(); int row = i; QThreadPool::globalInstance()->start([this, file, row]() mutable { auto size = calculateWorldSize(file); QMetaObject::invokeMethod( this, [this, size, row, file]() { if (row < m_worlds.size() && m_worlds[row].container() == file) { m_worlds[row].setSize(size); // Notify views QModelIndex modelIndex = index(row, SizeColumn); emit dataChanged(modelIndex, modelIndex, { SizeRole }); } }, Qt::QueuedConnection); }); } } PrismLauncher-11.0.3/launcher/minecraft/AssetsUtils.cpp0000644000175100017510000002371115224505336022527 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include #include #include #include "AssetsUtils.h" #include "BuildConfig.h" #include "FileSystem.h" #include "net/ApiDownload.h" #include "net/ChecksumValidator.h" #include "net/Download.h" #include "Application.h" #include "net/NetRequest.h" #include "update/AssetUpdateTask.h" namespace { QSet collectPathsFromDir(QString dirPath) { QFileInfo dirInfo(dirPath); if (!dirInfo.exists()) { return {}; } QSet out; QDirIterator iter(dirPath, QDirIterator::Subdirectories); while (iter.hasNext()) { QString value = iter.next(); QFileInfo info(value); if (info.isFile()) { out.insert(value); qDebug() << value; } } return out; } } // namespace namespace AssetsUtils { /* * Returns true on success, with index populated * index is undefined otherwise */ bool loadAssetsIndexJson(const QString& assetsId, const QString& path, AssetsIndex& index) { /* { "objects": { "icons/icon_16x16.png": { "hash": "bdf48ef6b5d0d23bbb02e17d04865216179f510a", "size": 3665 }, ... } } } */ QFile file(path); // Try to open the file and fail if we can't. // TODO: We should probably report this error to the user. if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to read assets index file" << path << "error:" << file.errorString(); return false; } index.id = assetsId; // Read the file and close it. QByteArray jsonData = file.readAll(); file.close(); QJsonParseError parseError; QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); // Fail if the JSON is invalid. if (parseError.error != QJsonParseError::NoError) { qCritical() << "Failed to parse assets index file:" << parseError.errorString() << "at offset " << QString::number(parseError.offset); return false; } // Make sure the root is an object. if (!jsonDoc.isObject()) { qCritical() << "Invalid assets index JSON: Root should be an array."; return false; } QJsonObject root = jsonDoc.object(); QJsonValue isVirtual = root.value("virtual"); if (!isVirtual.isUndefined()) { index.isVirtual = isVirtual.toBool(false); } QJsonValue mapToResources = root.value("map_to_resources"); if (!mapToResources.isUndefined()) { index.mapToResources = mapToResources.toBool(false); } QJsonValue objects = root.value("objects"); QVariantMap map = objects.toVariant().toMap(); for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter) { // qDebug() << iter.key(); QVariant variant = iter.value(); QVariantMap nested_objects = variant.toMap(); AssetObject object; for (QVariantMap::const_iterator nested_iter = nested_objects.begin(); nested_iter != nested_objects.end(); ++nested_iter) { // qDebug() << nested_iter.key() << nested_iter.value().toString(); QString key = nested_iter.key(); QVariant value = nested_iter.value(); if (key == "hash") { object.hash = value.toString(); } else if (key == "size") { object.size = value.toLongLong(); } } index.objects.insert(iter.key(), object); } return true; } // FIXME: ugly code duplication QDir getAssetsDir(const QString& assetsId, const QString& resourcesFolder) { QDir assetsDir = QDir("assets/"); QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes")); QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects")); QDir virtualDir = QDir(FS::PathCombine(assetsDir.path(), "virtual")); QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json"); QFile indexFile(indexPath); QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId)); if (!indexFile.exists()) { qCritical() << "No assets index file" << indexPath << "; can't determine assets path!"; return virtualRoot; } AssetsIndex index; if (!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index)) { qCritical() << "Failed to load asset index file" << indexPath << "; can't determine assets path!"; return virtualRoot; } QString targetPath; if (index.isVirtual) { return virtualRoot; } else if (index.mapToResources) { return QDir(resourcesFolder); } return virtualRoot; } // FIXME: ugly code duplication bool reconstructAssets(QString assetsId, QString resourcesFolder) { QDir assetsDir = QDir("assets/"); QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes")); QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects")); QDir virtualDir = QDir(FS::PathCombine(assetsDir.path(), "virtual")); QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json"); QFile indexFile(indexPath); QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId)); if (!indexFile.exists()) { qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets!"; return false; } qDebug() << "reconstructAssets" << assetsDir.path() << indexDir.path() << objectDir.path() << virtualDir.path() << virtualRoot.path(); AssetsIndex index; if (!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index)) { qCritical() << "Failed to load asset index file" << indexPath << "; can't reconstruct assets!"; return false; } QString targetPath; bool removeLeftovers = false; if (index.isVirtual) { targetPath = virtualRoot.path(); removeLeftovers = true; qDebug() << "Reconstructing virtual assets folder at" << targetPath; } else if (index.mapToResources) { targetPath = resourcesFolder; qDebug() << "Reconstructing resources folder at" << targetPath; } if (!targetPath.isNull()) { auto presentFiles = collectPathsFromDir(targetPath); for (QString map : index.objects.keys()) { AssetObject asset_object = index.objects.value(map); QString target_path = FS::PathCombine(targetPath, map); QFile target(target_path); QString tlk = asset_object.hash.left(2); QString original_path = FS::PathCombine(objectDir.path(), tlk, asset_object.hash); QFile original(original_path); if (!original.exists()) continue; presentFiles.remove(target_path); if (!target.exists()) { QFileInfo info(target_path); QDir target_dir = info.dir(); qDebug() << target_dir.path(); FS::ensureFolderPathExists(target_dir.path()); bool couldCopy = original.copy(target_path); qDebug() << " Copying" << original_path << "to" << target_path << QString::number(couldCopy); } } // TODO: Write last used time to virtualRoot/.lastused if (removeLeftovers) { for (auto& file : presentFiles) { qDebug() << "Would remove" << file; } } } return true; } } // namespace AssetsUtils Net::NetRequest::Ptr AssetObject::getDownloadAction() { QFileInfo objectFile(getLocalPath()); if ((!objectFile.isFile()) || (objectFile.size() != size)) { auto objectDL = Net::ApiDownload::makeFile(getUrl(), objectFile.filePath()); if (hash.size()) { objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, hash)); } objectDL->setProgress(objectDL->getProgress(), size); return objectDL; } return nullptr; } QString AssetObject::getLocalPath() { return "assets/objects/" + getRelPath(); } QUrl AssetObject::getUrl() { auto resourceURL = AssetUpdateTask::resourceUrl(); return resourceURL + getRelPath(); } QString AssetObject::getRelPath() { return hash.left(2) + "/" + hash; } NetJob::Ptr AssetsIndex::getDownloadJob() { auto job = makeShared(QObject::tr("Assets for %1").arg(id), APPLICATION->network()); for (auto& object : objects.values()) { auto dl = object.getDownloadAction(); if (dl) { job->addNetAction(dl); } } if (job->size()) return job; return nullptr; } PrismLauncher-11.0.3/launcher/minecraft/MojangDownloadInfo.h0000644000175100017510000000352115224505336023425 0ustar runnerrunner#pragma once #include #include #include struct MojangDownloadInfo { // types using Ptr = std::shared_ptr; // data /// Local filesystem path. WARNING: not used, only here so we can pass through mojang files unmolested! QString path; /// absolute URL of this file QString url; /// sha-1 checksum of the file QString sha1; /// size of the file in bytes int size; }; struct MojangLibraryDownloadInfo { MojangLibraryDownloadInfo(MojangDownloadInfo::Ptr artifact_) : artifact(artifact_) {} MojangLibraryDownloadInfo() {} // types using Ptr = std::shared_ptr; // methods MojangDownloadInfo* getDownloadInfo(QString classifier) { if (classifier.isNull()) { return artifact.get(); } return classifiers[classifier].get(); } // data MojangDownloadInfo::Ptr artifact; QMap classifiers; }; struct MojangAssetIndexInfo : public MojangDownloadInfo { // types using Ptr = std::shared_ptr; // methods MojangAssetIndexInfo() {} MojangAssetIndexInfo(QString id_) { this->id = id_; // HACK: ignore assets from other version files than Minecraft // workaround for stupid assets issue caused by amazon: // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/ if (id_ == "legacy") { url = "https://piston-meta.mojang.com/mc/assets/legacy/c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729/legacy.json"; } // HACK else { url = "https://s3.amazonaws.com/Minecraft.Download/indexes/" + id_ + ".json"; } known = false; } // data int totalSize; QString id; bool known = true; }; PrismLauncher-11.0.3/launcher/minecraft/ShortcutUtils.h0000644000175100017510000000475215224505336022551 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2025 Yihe Li * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Application.h" #include "BaseInstance.h" #include #include namespace ShortcutUtils { /// A struct to hold parameters for creating a shortcut struct Shortcut { BaseInstance* instance; QString name; QString targetString; QWidget* parent = nullptr; QStringList extraArgs = {}; QString iconKey = ""; ShortcutTarget target; }; /// Create an instance shortcut on the specified file path bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath); /// Create an instance shortcut on the desktop bool createInstanceShortcutOnDesktop(const Shortcut& shortcut); /// Create an instance shortcut in the Applications directory bool createInstanceShortcutInApplications(const Shortcut& shortcut); /// Create an instance shortcut in other directories bool createInstanceShortcutInOther(const Shortcut& shortcut); } // namespace ShortcutUtils PrismLauncher-11.0.3/launcher/minecraft/PackProfile.h0000644000175100017510000001473415224505336022115 0ustar runnerrunner// SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022-2023 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include "Component.h" #include "LaunchProfile.h" #include "modplatform/ModIndex.h" #include "net/Mode.h" class MinecraftInstance; struct PackProfileData; class ComponentUpdateTask; class PackProfile : public QAbstractListModel { Q_OBJECT friend ComponentUpdateTask; 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(); virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; virtual int columnCount(const QModelIndex& parent) const override; virtual Qt::ItemFlags flags(const QModelIndex& index) const override; /// call this to explicitly mark the component list as loaded - this is used to build a new component list from scratch. void buildingFromScratch(); /// install more jar mods void installJarMods(QStringList selectedFiles); /// install a jar/zip as a replacement for the main jar void installCustomJar(QString selectedFile); /// install MMC/Prism component files bool installComponents(QStringList selectedFiles); /// install Java agent files void installAgents(QStringList selectedFiles); enum MoveDirection { MoveUp, MoveDown }; /// move component file # up or down the list void move(int index, MoveDirection direction); /// remove component file # - including files/records bool remove(int index); /// remove component file by id - including files/records bool remove(const QString& id); bool customize(int index); bool revertToBase(int index); /// reload the list, reload all components, resolve dependencies Result reload(Net::Mode netmode); // reload all components, resolve dependencies void resolve(Net::Mode netmode); /// get current running task... Task::Ptr getCurrentTask(); std::shared_ptr getProfile() const; // NOTE: used ONLY by MinecraftInstance to provide legacy version mappings from instance config void setOldConfigVersion(const QString& uid, const QString& version); QString getComponentVersion(const QString& uid) const; bool setComponentVersion(const QString& uid, const QString& version, bool important = false); bool installEmpty(const QString& uid, const QString& name); QString patchFilePathForUid(const QString& uid) const; /// if there is a save scheduled, do it now. void saveNow(); /// helper method, returns RuntimeContext of instance RuntimeContext runtimeContext(); signals: void minecraftChanged(); public: /// get the profile component by id ComponentPtr getComponent(const QString& id); /// get the profile component by index ComponentPtr getComponent(size_t index); /// Add the component to the internal list of patches // todo(merged): is this the best approach void appendComponent(ComponentPtr component); std::optional getModLoaders(); // this returns aditional loaders(Quilt supports fabric and NeoForge supports Forge) std::optional getSupportedModLoaders(); QList getModLoadersList(); /// apply the component patches. Catches all the errors and returns true/false for success/failure void invalidateLaunchProfile(); private: void scheduleSave(); bool saveIsScheduled() const; /// insert component so that its index is ideally the specified one (returns real index) void insertComponent(size_t index, ComponentPtr component); QString componentsFilePath() const; QString patchesPattern() const; private slots: bool save_internal(); void updateSucceeded(); void updateFailed(const QString& error); void componentDataChanged(); void disableInteraction(bool disable); private: Result load(); bool installJarMods_internal(QStringList filepaths); bool installCustomJar_internal(QString filepath); bool installAgents_internal(QStringList filepaths); bool removeComponent_internal(ComponentPtr patch); private: /* data */ std::unique_ptr d; }; PrismLauncher-11.0.3/launcher/minecraft/ProfileUtils.cpp0000644000175100017510000001434415224505336022667 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ProfileUtils.h" #include #include "Json.h" #include "minecraft/OneSixVersionFormat.h" #include "minecraft/VersionFilterData.h" #include #include #include namespace ProfileUtils { static const int currentOrderFileVersion = 1; bool readOverrideOrders(QString path, PatchOrder& order) { QFile orderFile(path); if (!orderFile.exists()) { qWarning() << "Order file doesn't exist. Ignoring."; return false; } if (!orderFile.open(QFile::ReadOnly)) { qCritical() << "Couldn't open" << orderFile.fileName() << "for reading:" << orderFile.errorString(); qWarning() << "Ignoring overridden order"; return false; } // and it's valid JSON QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(orderFile.readAll(), &error); if (error.error != QJsonParseError::NoError) { qCritical() << "Couldn't parse" << orderFile.fileName() << ":" << error.errorString(); qWarning() << "Ignoring overridden order"; return false; } // and then read it and process it if all above is true. try { auto obj = Json::requireObject(doc); // check order file version. auto version = Json::requireInteger(obj.value("version")); if (version != currentOrderFileVersion) { throw JSONValidationError(QObject::tr("Invalid order file version, expected %1").arg(currentOrderFileVersion)); } auto orderArray = Json::requireArray(obj.value("order")); for (auto item : orderArray) { order.append(Json::requireString(item)); } } catch ([[maybe_unused]] const JSONValidationError& err) { qCritical() << "Couldn't parse" << orderFile.fileName() << ": bad file format"; qWarning() << "Ignoring overridden order"; order.clear(); return false; } return true; } static VersionFilePtr createErrorVersionFile(QString fileId, QString filepath, QString error) { auto outError = std::make_shared(); outError->uid = outError->name = fileId; // outError->filename = filepath; outError->addProblem(ProblemSeverity::Error, error); return outError; } static VersionFilePtr guardedParseJson(const QJsonDocument& doc, const QString& fileId, const QString& filepath, const bool& requireOrder) { try { return OneSixVersionFormat::versionFileFromJson(doc, filepath, requireOrder); } catch (const Exception& e) { return createErrorVersionFile(fileId, filepath, e.cause()); } } VersionFilePtr parseJsonFile(const QFileInfo& fileInfo, const bool requireOrder) { QFile file(fileInfo.absoluteFilePath()); if (!file.open(QFile::ReadOnly)) { auto errorStr = QObject::tr("Unable to open the version file %1: %2.").arg(fileInfo.fileName(), file.errorString()); return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); } QJsonParseError error; auto data = file.readAll(); QJsonDocument doc = QJsonDocument::fromJson(data, &error); file.close(); if (error.error != QJsonParseError::NoError) { int line = 1; int column = 0; for (int i = 0; i < error.offset; i++) { if (data[i] == '\n') { line++; column = 0; continue; } column++; } auto errorStr = QObject::tr("Unable to process the version file %1: %2 at line %3 column %4.") .arg(fileInfo.fileName(), error.errorString()) .arg(line) .arg(column); return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); } return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), requireOrder); } bool saveJsonFile(const QJsonDocument& doc, const QString& filename) { auto data = doc.toJson(); QSaveFile jsonFile(filename); if (!jsonFile.open(QIODevice::WriteOnly)) { qWarning() << "Couldn't open" << filename << "for writing:" << jsonFile.errorString(); jsonFile.cancelWriting(); return false; } jsonFile.write(data); if (!jsonFile.commit()) { qWarning() << "Couldn't save" << filename << "error:" << jsonFile.errorString(); return false; } return true; } void removeLwjglFromPatch(VersionFilePtr patch) { auto filter = [](QList& libs) { QList filteredLibs; for (auto lib : libs) { if (!g_VersionFilterData.lwjglWhitelist.contains(lib->artifactPrefix())) { filteredLibs.append(lib); } } libs = filteredLibs; }; filter(patch->libraries); } } // namespace ProfileUtils PrismLauncher-11.0.3/launcher/minecraft/MinecraftLoadAndCheck.cpp0000644000175100017510000000217715224505336024340 0ustar runnerrunner#include "MinecraftLoadAndCheck.h" #include "MinecraftInstance.h" #include "PackProfile.h" MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode) : m_inst(inst), m_netmode(netmode) {} void MinecraftLoadAndCheck::executeTask() { // add offline metadata load task auto components = m_inst->getPackProfile(); if (auto result = components->reload(m_netmode); !result) { emitFailed(result.error); return; } m_task = components->getCurrentTask(); if (!m_task) { emitSucceeded(); return; } connect(m_task.get(), &Task::succeeded, this, &MinecraftLoadAndCheck::emitSucceeded); connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::emitFailed); connect(m_task.get(), &Task::aborted, this, &MinecraftLoadAndCheck::emitAborted); propagateFromOther(m_task.get()); } bool MinecraftLoadAndCheck::canAbort() const { if (m_task) { return m_task->canAbort(); } return true; } bool MinecraftLoadAndCheck::abort() { if (m_task && m_task->canAbort()) { return m_task->abort(); } return Task::abort(); } PrismLauncher-11.0.3/launcher/minecraft/ParseUtils.cpp0000644000175100017510000000157415224505336022342 0ustar runnerrunner#include "ParseUtils.h" #include #include #include #include QDateTime timeFromS3Time(QString str) { return QDateTime::fromString(str, Qt::ISODate); } QString timeToS3Time(QDateTime time) { // this all because Qt can't format timestamps right. int offsetRaw = time.offsetFromUtc(); bool negative = offsetRaw < 0; int offsetAbs = std::abs(offsetRaw); int offsetSeconds = offsetAbs % 60; offsetAbs -= offsetSeconds; int offsetMinutes = offsetAbs % 3600; offsetAbs -= offsetMinutes; offsetMinutes /= 60; int offsetHours = offsetAbs / 3600; QString raw = time.toString("yyyy-MM-ddTHH:mm:ss"); raw += (negative ? QChar('-') : QChar('+')); raw += QString("%1").arg(offsetHours, 2, 10, QChar('0')); raw += ":"; raw += QString("%1").arg(offsetMinutes, 2, 10, QChar('0')); return raw; } PrismLauncher-11.0.3/launcher/minecraft/ParseUtils.h0000644000175100017510000000036515224505336022004 0ustar runnerrunner#pragma once #include #include /// take the timestamp used by S3 and turn it into QDateTime QDateTime timeFromS3Time(QString str); /// take a timestamp and convert it into an S3 timestamp QString timeToS3Time(QDateTime); PrismLauncher-11.0.3/launcher/minecraft/VersionFile.h0000644000175100017510000001236015224505336022134 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include #include #include "Agent.h" #include "Library.h" #include "ProblemProvider.h" #include "java/JavaMetadata.h" #include "minecraft/Rule.h" class PackProfile; class VersionFile; class LaunchProfile; struct MojangDownloadInfo; struct MojangAssetIndexInfo; using VersionFilePtr = std::shared_ptr; class VersionFile : public ProblemContainer { friend class MojangVersionFormat; friend class OneSixVersionFormat; public: /* methods */ void applyTo(LaunchProfile* profile, const RuntimeContext& runtimeContext); public: /* data */ /// Prism Launcher: order hint for this version file if no explicit order is set int order = 0; /// Prism Launcher: human readable name of this package QString name; /// Prism Launcher: package ID of this package QString uid; /// Prism Launcher: version of this package QString version; /// Prism Launcher: DEPRECATED dependency on a Minecraft version QString dependsOnMinecraftVersion; /// Mojang: DEPRECATED used to version the Mojang version format int minimumLauncherVersion = -1; /// Mojang: DEPRECATED version of Minecraft this is QString minecraftVersion; /// Mojang: class to launch Minecraft with QString mainClass; /// Prism Launcher: class to launch legacy Minecraft with (embed in a custom window) QString appletClass; /// Mojang: Minecraft launch arguments (may contain placeholders for variable substitution) QString minecraftArguments; /// Prism Launcher: Additional JVM launch arguments QStringList addnJvmArguments; /// Mojang: list of compatible java majors QList compatibleJavaMajors; /// Mojang: the name of recommended java version QString compatibleJavaName; /// Mojang: type of the Minecraft version QString type; /// Mojang: the time this version was actually released by Mojang QDateTime releaseTime; /// Mojang: DEPRECATED the time this version was last updated by Mojang QDateTime updateTime; /// Mojang: DEPRECATED asset group to be used with Minecraft QString assets; /// Prism Launcher: list of tweaker mod arguments for launchwrapper QStringList addTweakers; /// Mojang: list of libraries to add to the version QList libraries; /// Prism Launcher: list of maven files to put in the libraries folder, but not in classpath QList mavenFiles; /// Prism Launcher: list of agents to add to JVM arguments QList agents; /// The main jar (Minecraft version library, normally) LibraryPtr mainJar; /// Prism Launcher: list of attached traits of this version file - used to enable features QSet traits; /// Prism Launcher: list of jar mods added to this version QList jarMods; /// Prism Launcher: list of mods added to this version QList mods; /** * Prism Launcher: set of packages this depends on * NOTE: this is shared with the meta format!!! */ Meta::RequireSet m_requires; /** * Prism Launcher: set of packages this conflicts with * NOTE: this is shared with the meta format!!! */ Meta::RequireSet conflicts; /// is volatile -- may be removed as soon as it is no longer needed by something else bool m_volatile = false; QList runtimes; public: // Mojang: DEPRECATED list of 'downloads' - client jar, server jar, windows server exe, maybe more. QMap> mojangDownloads; // Mojang: extended asset index download information std::shared_ptr mojangAssetIndex; }; PrismLauncher-11.0.3/launcher/minecraft/Rule.h0000644000175100017510000000407715224505336020624 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2025 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include "RuntimeContext.h" class Library; class Rule { public: enum Action { Allow, Disallow, Defer }; static Rule fromJson(const QJsonObject& json); QJsonObject toJson(); Action apply(const RuntimeContext& runtimeContext); private: struct OS { QString name; // FIXME: unsupported // retained to avoid information being lost from files QString version; }; Action m_action = Defer; std::optional m_os; }; PrismLauncher-11.0.3/launcher/minecraft/Library.h0000644000175100017510000001644215224505336021320 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include #include #include "GradleSpecifier.h" #include "MojangDownloadInfo.h" #include "Rule.h" #include "RuntimeContext.h" #include "net/NetRequest.h" class Library; class MinecraftInstance; using LibraryPtr = std::shared_ptr; class Library { friend class OneSixVersionFormat; friend class MojangVersionFormat; friend class LibraryTest; public: Library() {} Library(const QString& name) { m_name = name; } /// limited copy without some data. TODO: why? static LibraryPtr limitedCopy(LibraryPtr base) { auto newlib = std::make_shared(); newlib->m_name = base->m_name; newlib->m_repositoryURL = base->m_repositoryURL; newlib->m_hint = base->m_hint; newlib->m_absoluteURL = base->m_absoluteURL; newlib->m_extractExcludes = base->m_extractExcludes; newlib->m_nativeClassifiers = base->m_nativeClassifiers; newlib->m_rules = base->m_rules; newlib->m_storagePrefix = base->m_storagePrefix; newlib->m_mojangDownloads = base->m_mojangDownloads; newlib->m_filename = base->m_filename; return newlib; } public: /* methods */ /// Returns the raw name field const GradleSpecifier& rawName() const { return m_name; } void setRawName(const GradleSpecifier& spec) { m_name = spec; } void setClassifier(const QString& spec) { m_name.setClassifier(spec); } /// returns the full group and artifact prefix QString artifactPrefix() const { return m_name.artifactPrefix(); } /// get the artifact ID QString artifactId() const { return m_name.artifactId(); } /// get the artifact version QString version() const { return m_name.version(); } /// Returns true if the library is native bool isNative() const { return m_nativeClassifiers.size() != 0; } void setStoragePrefix(QString prefix = QString()); /// Set the url base for downloads void setRepositoryURL(const QString& base_url) { m_repositoryURL = base_url; } void getApplicableFiles(const RuntimeContext& runtimeContext, QStringList& jar, QStringList& native, QStringList& native32, QStringList& native64, const QString& overridePath) const; void setAbsoluteUrl(const QString& absolute_url) { m_absoluteURL = absolute_url; } void setFilename(const QString& filename) { m_filename = filename; } /// Get the file name of the library QString filename(const RuntimeContext& runtimeContext) const; // DEPRECATED: set a display name, used by jar mods only void setDisplayName(const QString& displayName) { m_displayname = displayName; } /// Get the file name of the library QString displayName(const RuntimeContext& runtimeContext) const; void setMojangDownloadInfo(MojangLibraryDownloadInfo::Ptr info) { m_mojangDownloads = info; } void setHint(const QString& hint) { m_hint = hint; } /// Set the load rules void setRules(QList rules) { m_rules = rules; } /// Returns true if the library should be loaded (or extracted, in case of natives) bool isActive(const RuntimeContext& runtimeContext) const; /// Returns true if the library is contained in an instance and false if it is shared bool isLocal() const; /// Returns true if the library is to always be checked for updates bool isAlwaysStale() const; /// Return true if the library requires forge XZ hacks bool isForge() const; // Get a list of downloads for this library QList getDownloads(const RuntimeContext& runtimeContext, class HttpMetaCache* cache, QStringList& failedLocalFiles, const QString& overridePath) const; QString getCompatibleNative(const RuntimeContext& runtimeContext) const; private: /* methods */ /// the default storage prefix used by Prism Launcher static QString defaultStoragePrefix(); /// Get the prefix - root of the storage to be used QString storagePrefix() const; /// Get the relative file path where the library should be saved QString storageSuffix(const RuntimeContext& runtimeContext) const; QString hint() const { return m_hint; } protected: /* data */ /// the basic gradle dependency specifier. GradleSpecifier m_name; /// DEPRECATED URL prefix of the maven repo where the file can be downloaded QString m_repositoryURL; /// DEPRECATED: Prism Launcher-specific absolute URL. takes precedence over the implicit maven repo URL, if defined QString m_absoluteURL; /// Prism Launcher extension - filename override QString m_filename; /// DEPRECATED Prism Launcher extension - display name QString m_displayname; /** * Prism Launcher-specific type hint - modifies how the library is treated */ QString m_hint; /** * storage - by default the local libraries folder in Prism Launcher, but could be elsewhere * Prism Launcher specific, because of FTB. */ QString m_storagePrefix; /// true if the library had an extract/excludes section (even empty) bool m_hasExcludes = false; /// a list of files that shouldn't be extracted from the library QStringList m_extractExcludes; /// native suffixes per OS QMap m_nativeClassifiers; /// true if the library had a rules section (even empty) bool applyRules = false; /// rules associated with the library QList m_rules; /// MOJANG: container with Mojang style download info MojangLibraryDownloadInfo::Ptr m_mojangDownloads; }; PrismLauncher-11.0.3/launcher/ResourceDownloadTask.h0000644000175100017510000000447015224505336022044 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022-2023 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "net/NetJob.h" #include "tasks/SequentialTask.h" #include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/ModIndex.h" class ResourceFolderModel; class ResourceDownloadTask : public SequentialTask { Q_OBJECT public: explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion version, ResourceFolderModel* packs, bool isIndexed = true, QString downloadReason = "standalone"); const QString& getFilename() const { return m_pack_version.fileName; } const QVariant& getVersionID() const { return m_pack_version.fileId; } const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; } const ModPlatform::ResourceProvider& getProvider() const { return m_pack->provider; } const QString& getName() const { return m_pack->name; } ModPlatform::IndexedPack::Ptr getPack() { return m_pack; } private: ModPlatform::IndexedPack::Ptr m_pack; ModPlatform::IndexedVersion m_pack_version; ResourceFolderModel* m_pack_model; NetJob::Ptr m_filesNetJob; LocalResourceUpdateTask::Ptr m_update_task; void downloadProgressChanged(qint64 current, qint64 total); void downloadFailed(QString reason); void downloadSucceeded(); std::tuple to_delete{ "", "" }; private slots: void hasOldResource(const QString& name, const QString& filename); }; PrismLauncher-11.0.3/launcher/tools/0000755000175100017510000000000015224505336016724 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/tools/BaseExternalTool.cpp0000644000175100017510000000144115224505336022643 0ustar runnerrunner#include "BaseExternalTool.h" #include #include #ifdef Q_OS_WIN #include #endif #include "BaseInstance.h" BaseExternalTool::BaseExternalTool(SettingsObject* settings, BaseInstance* instance, QObject* parent) : QObject(parent), m_instance(instance), globalSettings(settings) {} BaseExternalTool::~BaseExternalTool() {} BaseDetachedTool::BaseDetachedTool(SettingsObject* settings, BaseInstance* instance, QObject* parent) : BaseExternalTool(settings, instance, parent) {} void BaseDetachedTool::run() { runImpl(); } BaseExternalToolFactory::~BaseExternalToolFactory() {} BaseDetachedTool* BaseDetachedToolFactory::createDetachedTool(BaseInstance* instance, QObject* parent) { return qobject_cast(createTool(instance, parent)); } PrismLauncher-11.0.3/launcher/tools/GenericProfiler.h0000644000175100017510000000237215224505336022160 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "BaseProfiler.h" class GenericProfilerFactory : public BaseProfilerFactory { public: QString name() const override { return "Generic"; } void registerSettings([[maybe_unused]] SettingsObject* settings) override {}; BaseExternalTool* createTool(BaseInstance* instance, QObject* parent = 0) override; bool check([[maybe_unused]] QString* error) override { return true; }; bool check([[maybe_unused]] const QString& path, [[maybe_unused]] QString* error) override { return true; }; }; PrismLauncher-11.0.3/launcher/tools/MCEditTool.h0000644000175100017510000000052115224505336021036 0ustar runnerrunner#pragma once #include #include "settings/SettingsObject.h" class MCEditTool { public: MCEditTool(SettingsObject* settings); void setPath(QString& path); QString path() const; bool check(const QString& toolPath, QString& error); QString getProgramPath(); private: SettingsObject* m_settings; }; PrismLauncher-11.0.3/launcher/tools/JProfiler.cpp0000644000175100017510000000560315224505336021330 0ustar runnerrunner#include "JProfiler.h" #include #include "BaseInstance.h" #include "launch/LaunchTask.h" #include "settings/SettingsObject.h" class JProfiler : public BaseProfiler { Q_OBJECT public: JProfiler(SettingsObject* settings, BaseInstance* instance, QObject* parent = 0); private slots: void profilerStarted(); void profilerFinished(int exit, QProcess::ExitStatus status); protected: void beginProfilingImpl(LaunchTask* process); private: int listeningPort = 0; }; JProfiler::JProfiler(SettingsObject* settings, BaseInstance* instance, QObject* parent) : BaseProfiler(settings, instance, parent) {} void JProfiler::profilerStarted() { emit readyToLaunch(tr("Listening on port: %1").arg(listeningPort)); } void JProfiler::profilerFinished([[maybe_unused]] int exit, QProcess::ExitStatus status) { if (status == QProcess::CrashExit) { emit abortLaunch(tr("Profiler aborted")); } if (m_profilerProcess) { m_profilerProcess->deleteLater(); m_profilerProcess = 0; } } void JProfiler::beginProfilingImpl(LaunchTask* process) { listeningPort = globalSettings->get("JProfilerPort").toInt(); QProcess* profiler = new QProcess(this); QStringList profilerArgs = { "-d", QString::number(process->pid()), "--gui", "-p", QString::number(listeningPort) }; auto basePath = globalSettings->get("JProfilerPath").toString(); #ifdef Q_OS_WIN QString profilerProgram = QDir(basePath).absoluteFilePath("bin/jpenable.exe"); #else QString profilerProgram = QDir(basePath).absoluteFilePath("bin/jpenable"); #endif profiler->setArguments(profilerArgs); profiler->setProgram(profilerProgram); connect(profiler, &QProcess::started, this, &JProfiler::profilerStarted); connect(profiler, &QProcess::finished, this, &JProfiler::profilerFinished); m_profilerProcess = profiler; profiler->start(); } void JProfilerFactory::registerSettings(SettingsObject* settings) { settings->registerSetting("JProfilerPath"); settings->registerSetting("JProfilerPort", 42042); globalSettings = settings; } BaseExternalTool* JProfilerFactory::createTool(BaseInstance* instance, QObject* parent) { return new JProfiler(globalSettings, instance, parent); } bool JProfilerFactory::check(QString* error) { return check(globalSettings->get("JProfilerPath").toString(), error); } bool JProfilerFactory::check(const QString& path, QString* error) { if (path.isEmpty()) { *error = QObject::tr("Empty path"); return false; } QDir dir(path); if (!dir.exists()) { *error = QObject::tr("Path does not exist"); return false; } if (!dir.exists("bin") || !(dir.exists("bin/jprofiler") || dir.exists("bin/jprofiler.exe")) || !dir.exists("bin/agent.jar")) { *error = QObject::tr("Invalid JProfiler install"); return false; } return true; } #include "JProfiler.moc" PrismLauncher-11.0.3/launcher/tools/JVisualVM.cpp0000644000175100017510000000503715224505336021255 0ustar runnerrunner#include "JVisualVM.h" #include #include #include "BaseInstance.h" #include "launch/LaunchTask.h" #include "settings/SettingsObject.h" class JVisualVM : public BaseProfiler { Q_OBJECT public: JVisualVM(SettingsObject* settings, BaseInstance* instance, QObject* parent = 0); private slots: void profilerStarted(); void profilerFinished(int exit, QProcess::ExitStatus status); protected: void beginProfilingImpl(LaunchTask* process); }; JVisualVM::JVisualVM(SettingsObject* settings, BaseInstance* instance, QObject* parent) : BaseProfiler(settings, instance, parent) {} void JVisualVM::profilerStarted() { emit readyToLaunch(tr("VisualVM started")); } void JVisualVM::profilerFinished([[maybe_unused]] int exit, QProcess::ExitStatus status) { if (status == QProcess::CrashExit) { emit abortLaunch(tr("Profiler aborted")); } if (m_profilerProcess) { m_profilerProcess->deleteLater(); m_profilerProcess = 0; } } void JVisualVM::beginProfilingImpl(LaunchTask* process) { QProcess* profiler = new QProcess(this); QStringList profilerArgs = { "--openpid", QString::number(process->pid()) }; auto programPath = globalSettings->get("JVisualVMPath").toString(); profiler->setArguments(profilerArgs); profiler->setProgram(programPath); connect(profiler, &QProcess::started, this, &JVisualVM::profilerStarted); connect(profiler, &QProcess::finished, this, &JVisualVM::profilerFinished); profiler->start(); m_profilerProcess = profiler; } void JVisualVMFactory::registerSettings(SettingsObject* settings) { QString defaultValue = QStandardPaths::findExecutable("jvisualvm"); if (defaultValue.isNull()) { defaultValue = QStandardPaths::findExecutable("visualvm"); } settings->registerSetting("JVisualVMPath", defaultValue); globalSettings = settings; } BaseExternalTool* JVisualVMFactory::createTool(BaseInstance* instance, QObject* parent) { return new JVisualVM(globalSettings, instance, parent); } bool JVisualVMFactory::check(QString* error) { return check(globalSettings->get("JVisualVMPath").toString(), error); } bool JVisualVMFactory::check(const QString& path, QString* error) { if (path.isEmpty()) { *error = QObject::tr("Empty path"); return false; } QFileInfo finfo(path); if (!finfo.isExecutable() || !finfo.fileName().contains("visualvm")) { *error = QObject::tr("Invalid path to VisualVM"); return false; } return true; } #include "JVisualVM.moc" PrismLauncher-11.0.3/launcher/tools/BaseProfiler.cpp0000644000175100017510000000142215224505336022004 0ustar runnerrunner#include "BaseProfiler.h" #include "QObjectPtr.h" #include BaseProfiler::BaseProfiler(SettingsObject* settings, BaseInstance* instance, QObject* parent) : BaseExternalTool(settings, instance, parent) {} void BaseProfiler::beginProfiling(LaunchTask* process) { beginProfilingImpl(process); } void BaseProfiler::abortProfiling() { abortProfilingImpl(); } void BaseProfiler::abortProfilingImpl() { if (!m_profilerProcess) { return; } m_profilerProcess->terminate(); m_profilerProcess->deleteLater(); m_profilerProcess = 0; emit abortLaunch(tr("Profiler aborted")); } BaseProfiler* BaseProfilerFactory::createProfiler(BaseInstance* instance, QObject* parent) { return qobject_cast(createTool(instance, parent)); } PrismLauncher-11.0.3/launcher/tools/BaseExternalTool.h0000644000175100017510000000240615224505336022312 0ustar runnerrunner#pragma once #include #include class BaseInstance; class SettingsObject; class QProcess; class BaseExternalTool : public QObject { Q_OBJECT public: explicit BaseExternalTool(SettingsObject* settings, BaseInstance* instance, QObject* parent = 0); virtual ~BaseExternalTool(); protected: BaseInstance* m_instance; SettingsObject* globalSettings; }; class BaseDetachedTool : public BaseExternalTool { Q_OBJECT public: explicit BaseDetachedTool(SettingsObject* settings, BaseInstance* instance, QObject* parent = 0); public slots: void run(); protected: virtual void runImpl() = 0; }; class BaseExternalToolFactory { public: virtual ~BaseExternalToolFactory(); virtual QString name() const = 0; virtual void registerSettings(SettingsObject* settings) = 0; virtual BaseExternalTool* createTool(BaseInstance* instance, QObject* parent = 0) = 0; virtual bool check(QString* error) = 0; virtual bool check(const QString& path, QString* error) = 0; protected: SettingsObject* globalSettings; }; class BaseDetachedToolFactory : public BaseExternalToolFactory { public: virtual BaseDetachedTool* createDetachedTool(BaseInstance* instance, QObject* parent = 0); }; PrismLauncher-11.0.3/launcher/tools/JVisualVM.h0000644000175100017510000000064315224505336020720 0ustar runnerrunner#pragma once #include "BaseProfiler.h" class JVisualVMFactory : public BaseProfilerFactory { public: QString name() const override { return "VisualVM"; } void registerSettings(SettingsObject* settings) override; BaseExternalTool* createTool(BaseInstance* instance, QObject* parent = 0) override; bool check(QString* error) override; bool check(const QString& path, QString* error) override; }; PrismLauncher-11.0.3/launcher/tools/GenericProfiler.cpp0000644000175100017510000000306115224505336022507 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "GenericProfiler.h" #include "BaseInstance.h" #include "launch/LaunchTask.h" #include "settings/SettingsObject.h" class GenericProfiler : public BaseProfiler { Q_OBJECT public: GenericProfiler(SettingsObject* settings, BaseInstance* instance, QObject* parent = 0); protected: void beginProfilingImpl(LaunchTask* process); }; GenericProfiler::GenericProfiler(SettingsObject* settings, BaseInstance* instance, QObject* parent) : BaseProfiler(settings, instance, parent) {} void GenericProfiler::beginProfilingImpl(LaunchTask* process) { emit readyToLaunch(tr("Started process: %1").arg(process->pid())); } BaseExternalTool* GenericProfilerFactory::createTool(BaseInstance* instance, QObject* parent) { return new GenericProfiler(globalSettings, instance, parent); } #include "GenericProfiler.moc" PrismLauncher-11.0.3/launcher/tools/MCEditTool.cpp0000644000175100017510000000341115224505336021372 0ustar runnerrunner#include "MCEditTool.h" #include #include #include #include "BaseInstance.h" #include "minecraft/MinecraftInstance.h" #include "settings/SettingsObject.h" MCEditTool::MCEditTool(SettingsObject* settings) { settings->registerSetting("MCEditPath"); m_settings = settings; } void MCEditTool::setPath(QString& path) { m_settings->set("MCEditPath", path); } QString MCEditTool::path() const { return m_settings->get("MCEditPath").toString(); } bool MCEditTool::check(const QString& toolPath, QString& error) { if (toolPath.isEmpty()) { error = QObject::tr("Path is empty"); return false; } const QDir dir(toolPath); if (!dir.exists()) { error = QObject::tr("Path does not exist"); return false; } if (!dir.exists("mcedit.sh") && !dir.exists("mcedit.py") && !dir.exists("mcedit.exe") && !dir.exists("Contents") && !dir.exists("mcedit2.exe")) { error = QObject::tr("Path does not seem to be a MCEdit path"); return false; } return true; } QString MCEditTool::getProgramPath() { #ifdef Q_OS_MACOS return path(); #else const QString mceditPath = path(); QDir mceditDir(mceditPath); #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) if (mceditDir.exists("mcedit.sh")) { return mceditDir.absoluteFilePath("mcedit.sh"); } else if (mceditDir.exists("mcedit.py")) { return mceditDir.absoluteFilePath("mcedit.py"); } return QString(); #elif defined(Q_OS_WIN32) if (mceditDir.exists("mcedit.exe")) { return mceditDir.absoluteFilePath("mcedit.exe"); } else if (mceditDir.exists("mcedit2.exe")) { return mceditDir.absoluteFilePath("mcedit2.exe"); } return QString(); #endif #endif } PrismLauncher-11.0.3/launcher/tools/BaseProfiler.h0000644000175100017510000000147515224505336021461 0ustar runnerrunner#pragma once #include "BaseExternalTool.h" #include "QObjectPtr.h" class BaseInstance; class SettingsObject; class LaunchTask; class QProcess; class BaseProfiler : public BaseExternalTool { Q_OBJECT public: explicit BaseProfiler(SettingsObject* settings, BaseInstance* instance, QObject* parent = 0); public slots: void beginProfiling(LaunchTask* process); void abortProfiling(); protected: QProcess* m_profilerProcess; virtual void beginProfilingImpl(LaunchTask* process) = 0; virtual void abortProfilingImpl(); signals: void readyToLaunch(const QString& message); void abortLaunch(const QString& message); }; class BaseProfilerFactory : public BaseExternalToolFactory { public: virtual BaseProfiler* createProfiler(BaseInstance* instance, QObject* parent = 0); }; PrismLauncher-11.0.3/launcher/tools/JProfiler.h0000644000175100017510000000064415224505336020775 0ustar runnerrunner#pragma once #include "BaseProfiler.h" class JProfilerFactory : public BaseProfilerFactory { public: QString name() const override { return "JProfiler"; } void registerSettings(SettingsObject* settings) override; BaseExternalTool* createTool(BaseInstance* instance, QObject* parent = 0) override; bool check(QString* error) override; bool check(const QString& path, QString* error) override; }; PrismLauncher-11.0.3/launcher/logs/0000755000175100017510000000000015224505336016530 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/logs/AnonymizeLog.cpp0000644000175100017510000000601715224505336021653 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "AnonymizeLog.h" #include struct RegReplace { RegReplace(QRegularExpression r, QString w) : reg(r), with(w) { reg.optimize(); } QRegularExpression reg; QString with; }; static const QVector anonymizeRules = { RegReplace(QRegularExpression("C:\\\\Users\\\\([^\\\\]+)\\\\", QRegularExpression::CaseInsensitiveOption), "C:\\Users\\********\\"), // windows RegReplace(QRegularExpression("C:\\/Users\\/([^\\/]+)\\/", QRegularExpression::CaseInsensitiveOption), "C:/Users/********/"), // windows with forward slashes RegReplace(QRegularExpression("(?)"), // SESSION_TOKEN RegReplace(QRegularExpression("new refresh token: \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), "new refresh token: \"\""), // refresh token RegReplace(QRegularExpression("\"device_code\" : \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), "\"device_code\" : \"\""), // device code }; void anonymizeLog(QString& log) { for (auto rule : anonymizeRules) { log.replace(rule.reg, rule.with); } } PrismLauncher-11.0.3/launcher/logs/LogParser.h0000644000175100017510000000402415224505336020577 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include #include #include #include #include #include #include #include "MessageLevel.h" class LogParser { public: struct LogEntry { QString logger; MessageLevel level; QString levelText; QDateTime timestamp; QString thread; QString message; }; struct Partial { QString data; }; struct PlainText { QString message; }; struct Error { QString errMessage; QXmlStreamReader::Error error; }; using ParsedItem = std::variant; public: LogParser() = default; void appendLine(QAnyStringView data); std::optional parseNext(); QList parseAvailable(); std::optional getError(); /// guess log level from a line of game log static MessageLevel guessLevel(const QString& line, MessageLevel previous); protected: std::optional parseAttributes(); void setError(); void clearError(); std::optional parseLog4J(); private: QString m_buffer; QString m_partialData; QXmlStreamReader m_parser; std::optional m_error; }; PrismLauncher-11.0.3/launcher/logs/LogParser.cpp0000644000175100017510000002652615224505336021145 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "LogParser.h" #include #include "MessageLevel.h" using namespace Qt::Literals::StringLiterals; void LogParser::appendLine(QAnyStringView data) { if (!m_partialData.isEmpty()) { m_buffer = QString(m_partialData); m_buffer.append("\n"); m_partialData.clear(); } m_buffer.append(data.toString()); } std::optional LogParser::getError() { return m_error; } std::optional LogParser::parseAttributes() { LogParser::LogEntry entry{ "", MessageLevel::Info, }; auto attributes = m_parser.attributes(); for (const auto& attr : attributes) { auto name = attr.name(); auto value = attr.value(); if (name == "logger"_L1) { entry.logger = value.trimmed().toString(); } else if (name == "timestamp"_L1) { if (value.trimmed().isEmpty()) { m_parser.raiseError("log4j:Event Missing required attribute: timestamp"); return {}; } entry.timestamp = QDateTime::fromSecsSinceEpoch(value.trimmed().toLongLong()); } else if (name == "level"_L1) { entry.levelText = value.trimmed().toString(); entry.level = MessageLevel::fromName(entry.levelText); } else if (name == "thread"_L1) { entry.thread = value.trimmed().toString(); } } if (entry.logger.isEmpty()) { m_parser.raiseError("log4j:Event Missing required attribute: logger"); return {}; } return entry; } void LogParser::setError() { m_error = { m_parser.errorString(), m_parser.error(), }; } void LogParser::clearError() { m_error = {}; // clear previous error } bool isPotentialLog4JStart(QStringView buffer) { static QString target = QStringLiteral(" LogParser::parseNext() { clearError(); if (m_buffer.isEmpty()) { return {}; } if (m_buffer.trimmed().isEmpty()) { auto text = QString(m_buffer); m_buffer.clear(); return LogParser::PlainText{ text }; } // check if we have a full xml log4j event bool isCompleteLog4j = false; m_parser.clear(); m_parser.setNamespaceProcessing(false); m_parser.addData(m_buffer); if (m_parser.readNextStartElement()) { if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { int depth = 1; bool eod = false; while (depth > 0 && !eod) { auto tok = m_parser.readNext(); switch (tok) { case QXmlStreamReader::TokenType::StartElement: { depth += 1; } break; case QXmlStreamReader::TokenType::EndElement: { depth -= 1; } break; case QXmlStreamReader::TokenType::EndDocument: { eod = true; // break outer while loop } break; default: { // no op } } if (m_parser.hasError()) { break; } } isCompleteLog4j = depth == 0; } } if (isCompleteLog4j) { return parseLog4J(); } else { if (isPotentialLog4JStart(m_buffer)) { m_partialData = QString(m_buffer); return LogParser::Partial{ QString(m_buffer) }; } int start = 0; auto bufView = QStringView(m_buffer); while (start < bufView.length()) { if (qsizetype pos = bufView.right(bufView.length() - start).indexOf('<'); pos != -1) { auto slicestart = start + pos; auto slice = bufView.right(bufView.length() - slicestart); if (isPotentialLog4JStart(slice)) { if (slicestart > 0) { auto text = m_buffer.left(slicestart); m_buffer = m_buffer.right(m_buffer.length() - slicestart); if (!text.trimmed().isEmpty()) { return LogParser::PlainText{ text }; } } m_partialData = QString(m_buffer); return LogParser::Partial{ QString(m_buffer) }; } start = slicestart + 1; } else { break; } } // no log4j found, all plain text auto text = QString(m_buffer); m_buffer.clear(); return LogParser::PlainText{ text }; } } QList LogParser::parseAvailable() { QList items; bool doNext = true; while (doNext) { auto item_ = parseNext(); if (m_error.has_value()) { return {}; } if (item_.has_value()) { auto item = item_.value(); if (std::holds_alternative(item)) { break; } else { items.push_back(item); } } else { doNext = false; } } return items; } std::optional LogParser::parseLog4J() { m_parser.clear(); m_parser.setNamespaceProcessing(false); m_parser.addData(m_buffer); m_parser.readNextStartElement(); if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { auto entry_ = parseAttributes(); if (!entry_.has_value()) { setError(); return {}; } auto entry = entry_.value(); bool foundMessage = false; int depth = 1; enum parseOp { noOp, entryReady, parseError }; auto foundStart = [&]() -> parseOp { depth += 1; if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) { QString message; bool messageComplete = false; while (!messageComplete) { auto tok = m_parser.readNext(); switch (tok) { case QXmlStreamReader::TokenType::Characters: { message.append(m_parser.text()); } break; case QXmlStreamReader::TokenType::EndElement: { if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) { messageComplete = true; } } break; case QXmlStreamReader::TokenType::EndDocument: { return parseError; // parse fail } break; default: { // no op } } if (m_parser.hasError()) { return parseError; } } entry.message = message; foundMessage = true; depth -= 1; } return noOp; }; auto foundEnd = [&]() -> parseOp { depth -= 1; if (depth == 0 && m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { if (foundMessage) { auto consumed = m_parser.characterOffset(); if (consumed > 0 && consumed <= m_buffer.length()) { m_buffer = m_buffer.right(m_buffer.length() - consumed); // potential whitespace preserved for next item } clearError(); return entryReady; } m_parser.raiseError("log4j:Event Missing required attribute: message"); setError(); return parseError; } return noOp; }; while (!m_parser.atEnd()) { auto tok = m_parser.readNext(); parseOp op = noOp; switch (tok) { case QXmlStreamReader::TokenType::StartElement: { op = foundStart(); } break; case QXmlStreamReader::TokenType::EndElement: { op = foundEnd(); } break; case QXmlStreamReader::TokenType::EndDocument: { return {}; } break; default: { // no op } } switch (op) { case parseError: return {}; // parse fail or error case entryReady: return entry; case noOp: default: { // no op } } if (m_parser.hasError()) { return {}; } } } throw std::runtime_error("unreachable: already verified this was a complete log4j:Event"); } MessageLevel LogParser::guessLevel(const QString& line, MessageLevel previous) { static const QRegularExpression LINE_WITH_LEVEL("^\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); auto match = LINE_WITH_LEVEL.match(line); if (match.hasMatch()) { // New style logs from log4j QString timestamp = match.captured("timestamp"); QString levelStr = match.captured("level"); return MessageLevel::fromName(levelStr); } else { // Old style forge logs if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") || line.contains("[FINEST]")) return MessageLevel::Info; if (line.contains("[SEVERE]") || line.contains("[STDERR]")) return MessageLevel::Error; if (line.contains("[WARNING]")) return MessageLevel::Warning; if (line.contains("[DEBUG]")) return MessageLevel::Debug; } if (line.contains("Exception: ") || line.contains("Throwable: ")) return MessageLevel::Error; if (line.startsWith("Caused by: ") || line.startsWith("Exception in thread")) return MessageLevel::Error; if (line.contains("overwriting existing")) return MessageLevel::Fatal; if (line.startsWith("\t") || line.startsWith(" ")) return previous; return MessageLevel::Unknown; } PrismLauncher-11.0.3/launcher/logs/AnonymizeLog.h0000644000175100017510000000303315224505336021313 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 void anonymizeLog(QString& log); PrismLauncher-11.0.3/launcher/VersionProxyModel.h0000644000175100017510000000454515224505336021415 0ustar runnerrunner#pragma once #include #include "BaseVersionList.h" #include class VersionFilterModel; class VersionProxyModel : public QAbstractProxyModel { Q_OBJECT public: enum Column { Name, ParentVersion, Branch, Type, CPUArchitecture, Path, Time, JavaName, JavaMajor }; using FilterMap = QHash; public: VersionProxyModel(QObject* parent = 0); virtual ~VersionProxyModel() {}; virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; virtual QModelIndex mapFromSource(const QModelIndex& sourceIndex) const override; virtual QModelIndex mapToSource(const QModelIndex& proxyIndex) const override; virtual QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; virtual QModelIndex parent(const QModelIndex& child) const override; virtual void setSourceModel(QAbstractItemModel* sourceModel) override; const FilterMap& filters() const; const QString& search() const; void setFilter(BaseVersionList::ModelRoles column, Filter filter); void setSearch(const QString& search); void clearFilters(); QModelIndex getRecommended() const; QModelIndex getVersion(const QString& version) const; void setCurrentVersion(const QString& version); private slots: void sourceDataChanged(const QModelIndex& source_top_left, const QModelIndex& source_bottom_right); void sourceAboutToBeReset(); void sourceReset(); void sourceRowsAboutToBeInserted(const QModelIndex& parent, int first, int last); void sourceRowsInserted(const QModelIndex& parent, int first, int last); void sourceRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last); void sourceRowsRemoved(const QModelIndex& parent, int first, int last); private: QList m_columns; FilterMap m_filters; QString m_search; BaseVersionList::RoleList roles; VersionFilterModel* filterModel; bool hasRecommended = false; bool hasLatest = false; QString m_currentVersion; }; PrismLauncher-11.0.3/launcher/LibraryUtils.cpp0000644000175100017510000001212515224505336020716 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * PrismLauncher - Minecraft Launcher * Copyright (C) 2022 Jan Drögehoff * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include "FileSystem.h" #include "Json.h" #include "LibraryUtils.h" #ifdef __GLIBC__ #ifndef _GNU_SOURCE #define _GNU_SOURCE #define UNDEF_GNU_SOURCE #endif #include #include #endif namespace LibraryUtils { QString findMangoHud() { /** * Guess MangoHud install location by searching for vulkan layers in this order: * * $VK_LAYER_PATH * $XDG_DATA_DIRS (/usr/local/share/:/usr/share/) * $XDG_DATA_HOME (~/.local/share) * /etc * $XDG_CONFIG_DIRS (/etc/xdg) * $XDG_CONFIG_HOME (~/.config) * * @returns Absolute path of libMangoHud.so if found and empty QString otherwise. */ QStringList vkLayerList; { QString home = QDir::homePath(); QString vkLayerPath = qEnvironmentVariable("VK_LAYER_PATH"); if (!vkLayerPath.isEmpty()) { vkLayerList << vkLayerPath; } QStringList xdgDataDirs = qEnvironmentVariable("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/").split(QLatin1String(":")); for (QString dir : xdgDataDirs) { vkLayerList << FS::PathCombine(dir, "vulkan", "implicit_layer.d"); } QString xdgDataHome = qEnvironmentVariable("XDG_DATA_HOME"); if (xdgDataHome.isEmpty()) { xdgDataHome = FS::PathCombine(home, ".local", "share"); } vkLayerList << FS::PathCombine(xdgDataHome, "vulkan", "implicit_layer.d"); vkLayerList << "/etc"; QStringList xdgConfigDirs = qEnvironmentVariable("XDG_CONFIG_DIRS", "/etc/xdg").split(QLatin1String(":")); for (QString dir : xdgConfigDirs) { vkLayerList << FS::PathCombine(dir, "vulkan", "implicit_layer.d"); } QString xdgConfigHome = qEnvironmentVariable("XDG_CONFIG_HOME"); if (xdgConfigHome.isEmpty()) { xdgConfigHome = FS::PathCombine(home, ".config"); } vkLayerList << FS::PathCombine(xdgConfigHome, "vulkan", "implicit_layer.d"); } for (const QString& vkLayer : vkLayerList) { // prefer to use architecture specific vulkan layers QString currentArch = QSysInfo::currentCpuArchitecture(); if (currentArch == "arm64") { currentArch = "aarch64"; } QStringList manifestNames = { QString("MangoHud.%1.json").arg(currentArch), "MangoHud.json" }; QString filePath{}; for (const QString& manifestName : manifestNames) { QString tryPath = FS::PathCombine(vkLayer, manifestName); if (QFile::exists(tryPath)) { filePath = tryPath; break; } } if (filePath.isEmpty()) { continue; } try { auto conf = Json::requireDocument(filePath, vkLayer); auto confObject = Json::requireObject(conf, vkLayer); auto layer = confObject["layer"].toObject(); QString libraryName = layer["library_path"].toString(); if (libraryName.isEmpty()) { continue; } if (QFileInfo(libraryName).isAbsolute()) { return libraryName; } #ifdef __GLIBC__ // Check whether mangohud is usable on a glibc based system QString libraryPath = find(libraryName); if (!libraryPath.isEmpty()) { return libraryPath; } #else // Without glibc return recorded shared library as-is. return libraryName; #endif } catch (const Exception& e) { } } return {}; } QString find(QString libName) { #ifdef __GLIBC__ const char* library = libName.toLocal8Bit().constData(); void* handle = dlopen(library, RTLD_NOW); if (!handle) { qCritical() << "dlopen() failed:" << dlerror(); return {}; } char path[PATH_MAX]; if (dlinfo(handle, RTLD_DI_ORIGIN, path) == -1) { qCritical() << "dlinfo() failed:" << dlerror(); dlclose(handle); return {}; } auto fullPath = FS::PathCombine(QString(path), libName); dlclose(handle); return fullPath; #else qWarning() << "LibraryUtils::find is not implemented on this platform"; return {}; #endif } } // namespace LibraryUtils #ifdef UNDEF_GNU_SOURCE #undef _GNU_SOURCE #undef UNDEF_GNU_SOURCE #endif PrismLauncher-11.0.3/launcher/DesktopServices.h0000644000175100017510000000206215224505336021052 0ustar runnerrunner#pragma once #include #include class QFileInfo; /** * This wraps around QDesktopServices and adds workarounds where needed * Use this instead of QDesktopServices! */ namespace DesktopServices { /** * Open a path in whatever application is applicable. * @param ensureFolderPathExists Make sure the path exists */ bool openPath(const QFileInfo& path, bool ensureFolderPathExists = false); /** * Open a path in whatever application is applicable. * @param ensureFolderPathExists Make sure the path exists */ bool openPath(const QString& path, bool ensureFolderPathExists = false); /** * Run an application */ bool run(const QString& application, const QStringList& args, const QString& workingDirectory = QString(), qint64* pid = 0); /** * Open the URL, most likely in a browser. Maybe. */ bool openUrl(const QUrl& url); /** * Determine whether the launcher is running in a Flatpak environment */ bool isFlatpak(); /** * Determine whether the launcher is running in a Snap environment */ bool isSnap(); } // namespace DesktopServices PrismLauncher-11.0.3/launcher/MMCZip.cpp0000644000175100017510000002747515224505336017406 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "MMCZip.h" #include #include "FileSystem.h" #include "archive/ArchiveReader.h" #include "archive/ArchiveWriter.h" #include #include #include #include #include namespace MMCZip { // ours using FilterFunction = std::function; #if defined(LAUNCHER_APPLICATION) bool mergeZipFiles(ArchiveWriter& into, QFileInfo from, QSet& contained, const FilterFunction& filter = nullptr) { ArchiveReader r(from.absoluteFilePath()); return r.parse([&into, &contained, &filter, from](ArchiveReader::File* f) { auto filename = f->filename(); if (filter && !filter(filename)) { qDebug() << "Skipping file" << filename << "from" << from.fileName() << "- filtered"; f->skip(); return true; } if (contained.contains(filename)) { qDebug() << "Skipping already contained file" << filename << "from" << from.fileName(); f->skip(); return true; } contained.insert(filename); if (!into.addFile(f)) { qCritical() << "Failed to copy data of" << filename << "into the jar"; return false; } return true; }); } bool compressDirFiles(ArchiveWriter& zip, QString dir, QFileInfoList files) { QDir directory(dir); if (!directory.exists()) return false; for (auto e : files) { auto filePath = directory.relativeFilePath(e.absoluteFilePath()); auto srcPath = e.absoluteFilePath(); if (!zip.addFile(srcPath, filePath)) return false; } return true; } // ours bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods) { ArchiveWriter zipOut(targetJarPath); if (!zipOut.open()) { FS::deletePath(targetJarPath); qCritical() << "Failed to open the minecraft.jar for modding"; return false; } // Files already added to the jar. // These files will be skipped. QSet addedFiles; // Modify the jar // This needs to be done in reverse-order to ensure we respect the loading order of components for (auto i = mods.crbegin(); i != mods.crend(); i++) { const auto* mod = *i; // do not merge disabled mods. if (!mod->enabled()) continue; if (mod->type() == ResourceType::ZIPFILE) { if (!mergeZipFiles(zipOut, mod->fileinfo(), addedFiles)) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } } else if (mod->type() == ResourceType::SINGLEFILE) { // FIXME: buggy - does not work with addedFiles auto filename = mod->fileinfo(); if (!zipOut.addFile(filename.absoluteFilePath(), filename.fileName())) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } addedFiles.insert(filename.fileName()); } else if (mod->type() == ResourceType::FOLDER) { // untested, but seems to be unused / not possible to reach // FIXME: buggy - does not work with addedFiles auto filename = mod->fileinfo(); QString what_to_zip = filename.absoluteFilePath(); QDir dir(what_to_zip); dir.cdUp(); QString parent_dir = dir.absolutePath(); auto files = QFileInfoList(); collectFileListRecursively(what_to_zip, nullptr, &files, nullptr); for (auto e : files) { if (addedFiles.contains(e.filePath())) files.removeAll(e); } if (!compressDirFiles(zipOut, parent_dir, files)) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } qDebug() << "Adding folder" << filename.fileName() << "from" << filename.absoluteFilePath(); } else { // Make sure we do not continue launching when something is missing or undefined... zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add unknown mod type" << mod->fileinfo().fileName() << "to the jar."; return false; } } if (!mergeZipFiles(zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to insert minecraft.jar contents."; return false; } // Recompress the jar if (!zipOut.close()) { FS::deletePath(targetJarPath); qCritical() << "Failed to finalize minecraft.jar!"; return false; } return true; } #endif // ours std::optional extractSubDir(ArchiveReader* zip, const QString& subdir, const QString& target) { auto target_top_dir = QUrl::fromLocalFile(target); QStringList extracted; qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target; if (!zip->collectFiles()) { qWarning() << "Failed to enumerate files in archive"; return std::nullopt; } if (zip->getFiles().isEmpty()) { qDebug() << "Extracting empty archives seems odd..."; return extracted; } auto extPtr = ArchiveWriter::createDiskWriter(); auto ext = extPtr.get(); if (!zip->parse([&subdir, &target, &target_top_dir, ext, &extracted](ArchiveReader::File* f) { QString file_name = f->filename(); file_name = FS::RemoveInvalidPathChars(file_name); if (!file_name.startsWith(subdir)) { f->skip(); return true; } auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size())); auto original_name = relative_file_name; // Fix subdirs/files ending with a / getting transformed into absolute paths if (relative_file_name.startsWith('/')) relative_file_name = relative_file_name.mid(1); // Fix weird "folders with a single file get squashed" thing QString sub_path; if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { sub_path = relative_file_name.section('/', 0, -2) + '/'; FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); relative_file_name = relative_file_name.split('/').last(); } QString target_file_path; if (relative_file_name.isEmpty()) { target_file_path = target + '/'; } else { target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) target_file_path += '/'; } if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" << target; return false; } if (!f->writeFile(ext, target_file_path, target)) { qWarning() << "Failed to extract file" << original_name << "to" << target_file_path; return false; } extracted.append(target_file_path); qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; return true; })) { qWarning() << "Failed to parse file" << zip->getZipName(); FS::removeFiles(extracted); return std::nullopt; } return extracted; } // ours std::optional extractDir(QString fileCompressed, QString dir) { // check if this is a minimum size empty zip file... QFileInfo fileInfo(fileCompressed); if (fileInfo.size() == 22) { return QStringList(); } ArchiveReader zip(fileCompressed); return extractSubDir(&zip, "", dir); } // ours std::optional extractDir(QString fileCompressed, QString subdir, QString dir) { // check if this is a minimum size empty zip file... QFileInfo fileInfo(fileCompressed); if (fileInfo.size() == 22) { return QStringList(); } ArchiveReader zip(fileCompressed); return extractSubDir(&zip, subdir, dir); } // ours bool extractFile(QString fileCompressed, QString file, QString target) { // check if this is a minimum size empty zip file... QFileInfo fileInfo(fileCompressed); if (fileInfo.size() == 22) { return true; } ArchiveReader zip(fileCompressed); auto f = zip.goToFile(file); if (!f) { return false; } auto extPtr = ArchiveWriter::createDiskWriter(); auto ext = extPtr.get(); return f->writeFile(ext, target); } bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter) { QDir rootDirectory(rootDir); if (!rootDirectory.exists()) return false; QDir directory; if (subDir == nullptr) directory = rootDirectory; else directory = QDir(subDir); if (!directory.exists()) return false; // shouldn't ever happen // recurse directories QFileInfoList entries = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden); for (const auto& e : entries) { if (!collectFileListRecursively(rootDir, e.filePath(), files, excludeFilter)) return false; } // collect files entries = directory.entryInfoList(QDir::Files); for (const auto& e : entries) { if (excludeFilter && excludeFilter(e)) { QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); qDebug() << "Skipping file" << relativeFilePath; continue; } files->append(e); // we want the original paths for compressDirFiles } return true; } } // namespace MMCZip PrismLauncher-11.0.3/launcher/launch/0000755000175100017510000000000015224505336017036 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/launch/LaunchTask.cpp0000644000175100017510000002446015224505336021605 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan * * 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 "launch/LaunchTask.h" #include #include #include #include #include #include #include #include "MessageLevel.h" #include "tasks/Task.h" void LaunchTask::init() { m_instance->setRunning(true); } std::unique_ptr LaunchTask::create(MinecraftInstance* inst) { auto task = std::unique_ptr(new LaunchTask(inst)); task->init(); return task; } LaunchTask::LaunchTask(MinecraftInstance* instance) : m_instance(instance) {} void LaunchTask::appendStep(shared_qobject_ptr step) { m_steps.append(step); } void LaunchTask::prependStep(shared_qobject_ptr step) { m_steps.prepend(step); } void LaunchTask::executeTask() { m_instance->setCrashed(false); if (!m_steps.size()) { state = LaunchTask::Finished; emitSucceeded(); return; } state = LaunchTask::Running; onStepFinished(); } void LaunchTask::onReadyForLaunch() { state = LaunchTask::Waiting; emit readyForLaunch(); } void LaunchTask::onStepFinished() { // initial -> just start the first step if (currentStep == -1) { currentStep++; m_steps[currentStep]->start(); return; } auto step = m_steps[currentStep]; if (step->wasSuccessful()) { // end? if (currentStep == m_steps.size() - 1) { finalizeSteps(true, QString()); } else { currentStep++; step = m_steps[currentStep]; step->start(); } } else { finalizeSteps(false, step->failReason()); } } void LaunchTask::finalizeSteps(bool successful, const QString& error) { for (auto step = currentStep; step >= 0; step--) { m_steps[step]->finalize(); } if (successful) { emitSucceeded(); } else { emitFailed(error); } } void LaunchTask::onProgressReportingRequested() { state = LaunchTask::Waiting; emit requestProgress(m_steps[currentStep].get()); } void LaunchTask::setCensorFilter(QMap filter) { m_censorFilter = filter; } QString LaunchTask::censorPrivateInfo(QString in) { auto iter = m_censorFilter.begin(); while (iter != m_censorFilter.end()) { in.replace(iter.key(), iter.value()); iter++; } return in; } void LaunchTask::proceed() { if (state != LaunchTask::Waiting) { return; } m_steps[currentStep]->proceed(); } bool LaunchTask::canAbort() const { switch (state) { case LaunchTask::Aborted: case LaunchTask::Failed: case LaunchTask::Finished: return false; case LaunchTask::NotStarted: return true; case LaunchTask::Running: case LaunchTask::Waiting: { auto step = m_steps[currentStep]; return step->canAbort(); } } return false; } bool LaunchTask::abort() { switch (state) { case LaunchTask::Aborted: case LaunchTask::Failed: case LaunchTask::Finished: return true; case LaunchTask::NotStarted: { state = LaunchTask::Aborted; emitAborted(); return true; } case LaunchTask::Running: case LaunchTask::Waiting: { auto step = m_steps[currentStep]; if (!step->canAbort()) { return false; } if (step->abort()) { state = LaunchTask::Aborted; return true; } } default: break; } return false; } shared_qobject_ptr LaunchTask::getLogModel() { if (!m_logModel) { m_logModel.reset(new LogModel()); m_logModel->setMaxLines(getConsoleMaxLines(m_instance->settings())); m_logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(m_instance->settings())); // FIXME: should this really be here? m_logModel->setOverflowMessage(tr("Stopped watching the game log because the log length surpassed %1 lines.\n" "You may have to fix your mods because the game is still logging to files and" " likely wasting harddrive space at an alarming rate!") .arg(m_logModel->getMaxLines())); } return m_logModel; } bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel level) { LogParser* parser; switch (static_cast(level)) { case MessageLevel::StdErr: parser = &m_stderrParser; break; case MessageLevel::StdOut: parser = &m_stdoutParser; break; default: return false; } parser->appendLine(line); auto items = parser->parseAvailable(); if (auto err = parser->getError(); err.has_value()) { auto& model = *getLogModel(); model.append(MessageLevel::Error, tr("[Log4j Parse Error] Failed to parse log4j log event: %1").arg(err.value().errMessage)); return false; } if (items.isEmpty()) return true; auto model = getLogModel(); for (auto const& item : items) { if (std::holds_alternative(item)) { auto entry = std::get(item); auto msg = QString("[%1] [%2/%3] [%4]: %5") .arg(entry.timestamp.toString("HH:mm:ss")) .arg(entry.thread) .arg(entry.levelText) .arg(entry.logger) .arg(entry.message); msg = censorPrivateInfo(msg); model->append(entry.level, msg); } else if (std::holds_alternative(item)) { auto msg = std::get(item).message; MessageLevel newLevel = MessageLevel::takeFromLine(msg); if (newLevel == MessageLevel::Unknown) newLevel = LogParser::guessLevel(line, model->previousLevel()); msg = censorPrivateInfo(msg); model->append(newLevel, msg); } } return true; } void LaunchTask::onLogLines(const QStringList& lines, MessageLevel defaultLevel) { for (auto& line : lines) { onLogLine(line, defaultLevel); } } void LaunchTask::onLogLine(QString line, MessageLevel level) { if (parseXmlLogs(line, level)) { return; } // censor private user info line = censorPrivateInfo(line); getLogModel()->append(level, line); } void LaunchTask::emitSucceeded() { m_instance->setRunning(false); Task::emitSucceeded(); } void LaunchTask::emitFailed(QString reason) { m_instance->setRunning(false); m_instance->setCrashed(true); Task::emitFailed(reason); } QString expandVariables(const QString& input, QProcessEnvironment dict) { QString result = input; 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; } QString LaunchTask::substituteVariables(QString& cmd, bool isLaunch) const { return expandVariables(cmd, isLaunch ? m_instance->createLaunchEnvironment() : m_instance->createEnvironment()); } PrismLauncher-11.0.3/launcher/launch/steps/0000755000175100017510000000000015224505336020174 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/launch/steps/PostLaunchCommand.cpp0000644000175100017510000000667315224505336024273 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "PostLaunchCommand.h" #include PostLaunchCommand::PostLaunchCommand(LaunchTask* parent) : LaunchStep(parent) { auto instance = m_parent->instance(); m_command = instance->getPostExitCommand(); m_process.setProcessEnvironment(instance->createEnvironment()); connect(&m_process, &LoggedProcess::log, this, &PostLaunchCommand::logLines); connect(&m_process, &LoggedProcess::stateChanged, this, &PostLaunchCommand::on_state); } void PostLaunchCommand::executeTask() { auto cmd = m_parent->substituteVariables(m_command); emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher); auto args = QProcess::splitCommand(cmd); const QString program = args.takeFirst(); m_process.start(program, args); } void PostLaunchCommand::on_state(LoggedProcess::State state) { auto getError = [this]() { return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; switch (state) { case LoggedProcess::Aborted: case LoggedProcess::Crashed: case LoggedProcess::FailedToStart: { auto error = getError(); emit logLine(error, MessageLevel::Fatal); emitFailed(error); return; } case LoggedProcess::Finished: { if (m_process.exitCode() != 0) { auto error = getError(); emit logLine(error, MessageLevel::Fatal); emitFailed(error); } else { emit logLine(tr("Post-Launch command ran successfully.\n\n"), MessageLevel::Launcher); emitSucceeded(); } } default: break; } } void PostLaunchCommand::setWorkingDirectory(const QString& wd) { m_process.setWorkingDirectory(wd); } bool PostLaunchCommand::abort() { auto state = m_process.state(); if (state == LoggedProcess::Running || state == LoggedProcess::Starting) { m_process.kill(); } return true; } PrismLauncher-11.0.3/launcher/launch/steps/PreLaunchCommand.cpp0000644000175100017510000000665715224505336024076 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "PreLaunchCommand.h" #include PreLaunchCommand::PreLaunchCommand(LaunchTask* parent) : LaunchStep(parent) { auto instance = m_parent->instance(); m_command = instance->getPreLaunchCommand(); m_process.setProcessEnvironment(instance->createEnvironment()); connect(&m_process, &LoggedProcess::log, this, &PreLaunchCommand::logLines); connect(&m_process, &LoggedProcess::stateChanged, this, &PreLaunchCommand::on_state); } void PreLaunchCommand::executeTask() { auto cmd = m_parent->substituteVariables(m_command); emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher); auto args = QProcess::splitCommand(cmd); const QString program = args.takeFirst(); m_process.start(program, args); } void PreLaunchCommand::on_state(LoggedProcess::State state) { auto getError = [this]() { return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; switch (state) { case LoggedProcess::Aborted: case LoggedProcess::Crashed: case LoggedProcess::FailedToStart: { auto error = getError(); emit logLine(error, MessageLevel::Fatal); emitFailed(error); return; } case LoggedProcess::Finished: { if (m_process.exitCode() != 0) { auto error = getError(); emit logLine(error, MessageLevel::Fatal); emitFailed(error); } else { emit logLine(tr("Pre-Launch command ran successfully.\n\n"), MessageLevel::Launcher); emitSucceeded(); } } default: break; } } void PreLaunchCommand::setWorkingDirectory(const QString& wd) { m_process.setWorkingDirectory(wd); } bool PreLaunchCommand::abort() { auto state = m_process.state(); if (state == LoggedProcess::Running || state == LoggedProcess::Starting) { m_process.kill(); } return true; } PrismLauncher-11.0.3/launcher/launch/steps/LookupServerAddress.cpp0000644000175100017510000000605515224505336024654 0ustar runnerrunner/* 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 "LookupServerAddress.h" #include LookupServerAddress::LookupServerAddress(LaunchTask* parent) : LaunchStep(parent), m_dnsLookup(new QDnsLookup(this)) { connect(m_dnsLookup, &QDnsLookup::finished, this, &LookupServerAddress::on_dnsLookupFinished); m_dnsLookup->setType(QDnsLookup::SRV); } void LookupServerAddress::setLookupAddress(const QString& lookupAddress) { m_lookupAddress = lookupAddress; m_dnsLookup->setName(QString("_minecraft._tcp.%1").arg(lookupAddress)); } void LookupServerAddress::setOutputAddressPtr(MinecraftTarget::Ptr output) { m_output = std::move(output); } bool LookupServerAddress::abort() { m_dnsLookup->abort(); emitAborted(); return true; } void LookupServerAddress::executeTask() { m_dnsLookup->lookup(); } void LookupServerAddress::on_dnsLookupFinished() { if (isFinished()) { // Aborted return; } if (m_dnsLookup->error() != QDnsLookup::NoError) { emit logLine(QString("Failed to resolve server address (this is NOT an error!) %1: %2\n") .arg(m_dnsLookup->name(), m_dnsLookup->errorString()), MessageLevel::Launcher); resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch // and leave it up to minecraft to fail (or maybe not) when connecting return; } const auto records = m_dnsLookup->serviceRecords(); if (records.empty()) { emit logLine(QString("Failed to resolve server address %1: the DNS lookup succeeded, but no records were returned.\n") .arg(m_dnsLookup->name()), MessageLevel::Warning); resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch // and leave it up to minecraft to fail (or maybe not) when connecting return; } const auto& firstRecord = records.at(0); quint16 port = firstRecord.port(); emit logLine( QString("Resolved server address %1 to %2 with port %3\n").arg(m_dnsLookup->name(), firstRecord.target(), QString::number(port)), MessageLevel::Launcher); resolve(firstRecord.target(), port); } void LookupServerAddress::resolve(const QString& address, quint16 port) { m_output->address = address; m_output->port = port; m_dnsLookup->deleteLater(); emitSucceeded(); } PrismLauncher-11.0.3/launcher/launch/steps/CheckJava.h0000644000175100017510000000251315224505336022165 0ustar runnerrunner/* 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 #include #include class CheckJava : public LaunchStep { Q_OBJECT public: explicit CheckJava(LaunchTask* parent) : LaunchStep(parent) {}; virtual ~CheckJava() = default; virtual void executeTask(); virtual bool canAbort() const { return false; } private slots: void checkJavaFinished(const JavaChecker::Result& result); private: void printJavaInfo(const QString& version, const QString& architecture, const QString& realArchitecture, const QString& vendor); void printSystemInfo(bool javaIsKnown, bool javaIs64bit); private: QString m_javaPath; QString m_javaSignature; JavaChecker::Ptr m_JavaChecker; }; PrismLauncher-11.0.3/launcher/launch/steps/TextPrint.h0000644000175100017510000000222715224505336022311 0ustar runnerrunner/* 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 #include #include /* * FIXME: maybe do not export */ class TextPrint : public LaunchStep { Q_OBJECT public: explicit TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel level); explicit TextPrint(LaunchTask* parent, const QString& line, MessageLevel level); virtual ~TextPrint() {}; virtual void executeTask(); virtual bool canAbort() const; virtual bool abort(); private: QStringList m_lines; MessageLevel m_level; }; PrismLauncher-11.0.3/launcher/launch/steps/PreLaunchCommand.h0000644000175100017510000000215015224505336023523 0ustar runnerrunner/* 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 "LoggedProcess.h" #include "launch/LaunchStep.h" class PreLaunchCommand : public LaunchStep { Q_OBJECT public: explicit PreLaunchCommand(LaunchTask* parent); virtual ~PreLaunchCommand() {}; virtual void executeTask(); virtual bool abort(); virtual bool canAbort() const { return true; } void setWorkingDirectory(const QString& wd); private slots: void on_state(LoggedProcess::State state); private: LoggedProcess m_process; QString m_command; }; PrismLauncher-11.0.3/launcher/launch/steps/LookupServerAddress.h0000644000175100017510000000252315224505336024315 0ustar runnerrunner/* 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 #include #include #include "minecraft/launch/MinecraftTarget.h" class LookupServerAddress : public LaunchStep { Q_OBJECT public: explicit LookupServerAddress(LaunchTask* parent); virtual ~LookupServerAddress() = default; virtual void executeTask(); virtual bool abort(); virtual bool canAbort() const { return true; } void setLookupAddress(const QString& lookupAddress); void setOutputAddressPtr(MinecraftTarget::Ptr output); private slots: void on_dnsLookupFinished(); private: void resolve(const QString& address, quint16 port); QDnsLookup* m_dnsLookup; QString m_lookupAddress; MinecraftTarget::Ptr m_output; }; PrismLauncher-11.0.3/launcher/launch/steps/PrintServers.h0000644000175100017510000000226115224505336023014 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Leia uwu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include class PrintServers : public LaunchStep { Q_OBJECT public: PrintServers(LaunchTask* parent, const QStringList& servers); virtual void executeTask(); virtual bool canAbort() const; private: void resolveServer(const QHostInfo& host_info); QMap m_server_to_address; QStringList m_servers; }; PrismLauncher-11.0.3/launcher/launch/steps/QuitAfterGameStop.h0000644000175100017510000000205015224505336023706 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 dada513 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include class QuitAfterGameStop : public LaunchStep { Q_OBJECT public: explicit QuitAfterGameStop(LaunchTask* parent) : LaunchStep(parent) {}; virtual ~QuitAfterGameStop() = default; virtual void executeTask(); virtual bool canAbort() const { return false; } }; PrismLauncher-11.0.3/launcher/launch/steps/PostLaunchCommand.h0000644000175100017510000000215315224505336023725 0ustar runnerrunner/* 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 #include class PostLaunchCommand : public LaunchStep { Q_OBJECT public: explicit PostLaunchCommand(LaunchTask* parent); virtual ~PostLaunchCommand() {}; virtual void executeTask(); virtual bool abort(); virtual bool canAbort() const { return true; } void setWorkingDirectory(const QString& wd); private slots: void on_state(LoggedProcess::State state); private: LoggedProcess m_process; QString m_command; }; PrismLauncher-11.0.3/launcher/launch/steps/CheckJava.cpp0000644000175100017510000001552215224505336022524 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "CheckJava.h" #include #include #include #include #include #include "java/JavaUtils.h" void CheckJava::executeTask() { auto instance = m_parent->instance(); auto settings = instance->settings(); QString javaPathSetting = settings->get("JavaPath").toString(); m_javaPath = FS::ResolveExecutable(javaPathSetting); bool perInstance = settings->get("OverrideJava").toBool() || settings->get("OverrideJavaLocation").toBool(); auto realJavaPath = QStandardPaths::findExecutable(m_javaPath); if (realJavaPath.isEmpty()) { if (perInstance) { emit logLine(QString("The Java binary \"%1\" couldn't be found. Please fix the Java path " "override in the instance's settings or disable it.") .arg(javaPathSetting), MessageLevel::Warning); } else { emit logLine(QString("The Java binary \"%1\" couldn't be found. Please set up Java in " "the settings.") .arg(javaPathSetting), MessageLevel::Warning); } emitFailed(QString("Java path is not valid.")); return; } else { emit logLine("Java path is:\n " + m_javaPath, MessageLevel::Launcher); } if (JavaUtils::getJavaCheckPath().isEmpty()) { const char* reason = QT_TR_NOOP("Java checker library could not be found. Please check your installation."); emit logLine(tr(reason), MessageLevel::Fatal); emitFailed(tr(reason)); return; } QFileInfo javaInfo(realJavaPath); qint64 javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch(); auto storedSignature = settings->get("JavaSignature").toString(); auto storedArchitecture = settings->get("JavaArchitecture").toString(); auto storedRealArchitecture = settings->get("JavaRealArchitecture").toString(); auto storedVersion = settings->get("JavaVersion").toString(); auto storedVendor = settings->get("JavaVendor").toString(); QCryptographicHash hash(QCryptographicHash::Sha1); hash.addData(QByteArray::number(javaUnixTime)); hash.addData(m_javaPath.toUtf8()); m_javaSignature = hash.result().toHex(); // if timestamps are not the same, or something is missing, check! if (m_javaSignature != storedSignature || storedVersion.size() == 0 || storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0 || storedVendor.size() == 0) { m_JavaChecker.reset(new JavaChecker(realJavaPath, "", 0, 0, 0, 0)); emit logLine(QString("Checking Java version..."), MessageLevel::Launcher); connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished); m_JavaChecker->start(); return; } else { auto verString = instance->settings()->get("JavaVersion").toString(); auto archString = instance->settings()->get("JavaArchitecture").toString(); auto realArchString = settings->get("JavaRealArchitecture").toString(); auto vendorString = instance->settings()->get("JavaVendor").toString(); printJavaInfo(verString, archString, realArchString, vendorString); } m_parent->instance()->updateRuntimeContext(); emitSucceeded(); } void CheckJava::checkJavaFinished(const JavaChecker::Result& result) { switch (result.validity) { case JavaChecker::Result::Validity::Errored: { // Error message displayed if java can't start emit logLine(QString("Could not start java:"), MessageLevel::Error); emit logLines(result.errorLog.split('\n'), MessageLevel::Error); emit logLine(QString("\nCheck your Java settings."), MessageLevel::Launcher); emitFailed(QString("Could not start java!")); return; } case JavaChecker::Result::Validity::ReturnedInvalidData: { emit logLine(QString("Java checker returned some invalid data we don't understand:"), MessageLevel::Error); emit logLines(result.outLog.split('\n'), MessageLevel::Warning); emit logLine("\nMinecraft might not start properly.", MessageLevel::Launcher); m_parent->instance()->updateRuntimeContext(); emitSucceeded(); return; } case JavaChecker::Result::Validity::Valid: { auto instance = m_parent->instance(); printJavaInfo(result.javaVersion.toString(), result.mojangPlatform, result.realPlatform, result.javaVendor); instance->settings()->set("JavaVersion", result.javaVersion.toString()); instance->settings()->set("JavaArchitecture", result.mojangPlatform); instance->settings()->set("JavaRealArchitecture", result.realPlatform); instance->settings()->set("JavaVendor", result.javaVendor); instance->settings()->set("JavaSignature", m_javaSignature); m_parent->instance()->updateRuntimeContext(); emitSucceeded(); return; } } } void CheckJava::printJavaInfo(const QString& version, const QString& architecture, const QString& realArchitecture, const QString& vendor) { emit logLine( QString("Java is version %1, using %2 (%3) architecture, from %4").arg(version, architecture, realArchitecture, vendor), MessageLevel::Launcher); } PrismLauncher-11.0.3/launcher/launch/steps/TextPrint.cpp0000644000175100017510000000104115224505336022635 0ustar runnerrunner#include "TextPrint.h" TextPrint::TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel level) : LaunchStep(parent) { m_lines = lines; m_level = level; } TextPrint::TextPrint(LaunchTask* parent, const QString& line, MessageLevel level) : LaunchStep(parent) { m_lines.append(line); m_level = level; } void TextPrint::executeTask() { emit logLines(m_lines, m_level); emitSucceeded(); } bool TextPrint::canAbort() const { return true; } bool TextPrint::abort() { emitAborted(); return true; } PrismLauncher-11.0.3/launcher/launch/steps/QuitAfterGameStop.cpp0000644000175100017510000000157715224505336024256 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 dada513 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "QuitAfterGameStop.h" #include #include "Application.h" void QuitAfterGameStop::executeTask() { APPLICATION->quit(); } PrismLauncher-11.0.3/launcher/launch/steps/PrintServers.cpp0000644000175100017510000000366015224505336023353 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Leia uwu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "PrintServers.h" #include "QHostInfo" PrintServers::PrintServers(LaunchTask* parent, const QStringList& servers) : LaunchStep(parent) { m_servers = servers; } void PrintServers::executeTask() { for (QString server : m_servers) { QHostInfo::lookupHost(server, this, &PrintServers::resolveServer); } } void PrintServers::resolveServer(const QHostInfo& host_info) { QString server = host_info.hostName(); QString addresses = server + " resolves to:\n "; if (!host_info.addresses().isEmpty()) { for (QHostAddress address : host_info.addresses()) { addresses += address.toString(); if (!host_info.addresses().endsWith(address)) { addresses += ", "; } } } else { addresses += "N/A"; } addresses += "\n"; m_server_to_address.insert(server, addresses); // print server info in order once all servers are resolved if (m_server_to_address.size() >= m_servers.size()) { for (QString serv : m_servers) { emit logLine(m_server_to_address.value(serv), MessageLevel::Launcher); } emitSucceeded(); } } bool PrintServers::canAbort() const { return true; } PrismLauncher-11.0.3/launcher/launch/LaunchTask.h0000644000175100017510000000770515224505336021255 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan * * 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 #include #include #include "LaunchStep.h" #include "LogModel.h" #include "MessageLevel.h" #include "logs/LogParser.h" class LaunchTask : public Task { Q_OBJECT protected: explicit LaunchTask(MinecraftInstance* instance); void init(); public: enum State { NotStarted, Running, Waiting, Failed, Aborted, Finished }; public: /* methods */ static std::unique_ptr create(MinecraftInstance* inst); virtual ~LaunchTask() = default; void appendStep(shared_qobject_ptr step); void prependStep(shared_qobject_ptr step); void setCensorFilter(QMap filter); MinecraftInstance* instance() { return m_instance; } void setPid(qint64 pid) { m_pid = pid; } qint64 pid() { return m_pid; } /** * @brief prepare the process for launch (for multi-stage launch) */ virtual void executeTask() override; /** * @brief launch the armed instance */ void proceed(); /** * @brief abort launch */ bool abort() override; bool canAbort() const override; shared_qobject_ptr getLogModel(); public: QString substituteVariables(QString& cmd, bool isLaunch = false) const; QString censorPrivateInfo(QString in); protected: /* methods */ virtual void emitFailed(QString reason) override; virtual void emitSucceeded() override; signals: /** * @brief emitted when the launch preparations are done */ void readyForLaunch(); void requestProgress(Task* task); void requestLogging(); public slots: void onLogLines(const QStringList& lines, MessageLevel defaultLevel = MessageLevel::Launcher); void onLogLine(QString line, MessageLevel defaultLevel = MessageLevel::Launcher); void onReadyForLaunch(); void onStepFinished(); void onProgressReportingRequested(); private: /*methods */ void finalizeSteps(bool successful, const QString& error); protected: bool parseXmlLogs(QString const& line, MessageLevel level); protected: /* data */ MinecraftInstance* m_instance; shared_qobject_ptr m_logModel; QList> m_steps; QMap m_censorFilter; int currentStep = -1; State state = NotStarted; qint64 m_pid = -1; LogParser m_stdoutParser; LogParser m_stderrParser; }; PrismLauncher-11.0.3/launcher/launch/LaunchStep.cpp0000644000175100017510000000220115224505336021603 0ustar runnerrunner/* 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 "LaunchStep.h" #include "LaunchTask.h" LaunchStep::LaunchStep(LaunchTask* parent) : Task(), m_parent(parent) { connect(this, &LaunchStep::readyForLaunch, parent, &LaunchTask::onReadyForLaunch); connect(this, &LaunchStep::logLine, parent, &LaunchTask::onLogLine); connect(this, &LaunchStep::logLines, parent, &LaunchTask::onLogLines); connect(this, &LaunchStep::finished, parent, &LaunchTask::onStepFinished); connect(this, &LaunchStep::progressReportingRequest, parent, &LaunchTask::onProgressReportingRequested); } PrismLauncher-11.0.3/launcher/launch/TaskStepWrapper.h0000644000175100017510000000225515224505336022312 0ustar runnerrunner/* 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 #include #include #include #include class TaskStepWrapper : public LaunchStep { Q_OBJECT public: explicit TaskStepWrapper(LaunchTask* parent, Task::Ptr task) : LaunchStep(parent), m_task(task) {}; virtual ~TaskStepWrapper() = default; void executeTask() override; bool canAbort() const override; void proceed() override; public slots: bool abort() override; private slots: void updateFinished(); private: Task::Ptr m_task; }; PrismLauncher-11.0.3/launcher/launch/LaunchStep.h0000644000175100017510000000244215224505336021257 0ustar runnerrunner/* 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 "MessageLevel.h" #include "tasks/Task.h" #include class LaunchTask; class LaunchStep : public Task { Q_OBJECT public: /* methods */ explicit LaunchStep(LaunchTask* parent); virtual ~LaunchStep() = default; signals: void logLines(QStringList lines, MessageLevel level); void logLine(QString line, MessageLevel level); void readyForLaunch(); void progressReportingRequest(); public slots: virtual void proceed() {}; // called in the opposite order than the Task launch(), used to clean up or otherwise undo things after the launch ends virtual void finalize() {}; protected: /* data */ LaunchTask* m_parent; }; PrismLauncher-11.0.3/launcher/launch/LogModel.cpp0000644000175100017510000000775115224505336021256 0ustar runnerrunner#include "LogModel.h" LogModel::LogModel(QObject* parent) : QAbstractListModel(parent) { m_content.resize(m_maxLines); } int LogModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_numLines; } QVariant LogModel::data(const QModelIndex& index, int role) const { if (index.row() < 0 || index.row() >= m_numLines) return QVariant(); auto row = index.row(); auto realRow = (row + m_firstLine) % m_maxLines; if (role == Qt::DisplayRole || role == Qt::EditRole) { return m_content[realRow].line; } if (role == LevelRole) { return static_cast(m_content[realRow].level); } return QVariant(); } void LogModel::append(MessageLevel level, QString line) { if (m_suspended) { return; } int lineNum = (m_firstLine + m_numLines) % m_maxLines; // overflow if (m_numLines == m_maxLines) { if (m_stopOnOverflow) { // nothing more to do, the buffer is full return; } beginRemoveRows(QModelIndex(), 0, 0); m_firstLine = (m_firstLine + 1) % m_maxLines; m_numLines--; endRemoveRows(); } else if (m_numLines == m_maxLines - 1 && m_stopOnOverflow) { level = MessageLevel::Fatal; line = m_overflowMessage; } beginInsertRows(QModelIndex(), m_numLines, m_numLines); m_numLines++; m_content[lineNum].level = level; m_content[lineNum].line = line; endInsertRows(); } void LogModel::suspend(bool suspend) { m_suspended = suspend; } bool LogModel::suspended() { return m_suspended; } void LogModel::clear() { beginResetModel(); m_firstLine = 0; m_numLines = 0; endResetModel(); } QString LogModel::toPlainText() { QString out; out.reserve(m_numLines * 80); for (int i = 0; i < m_numLines; i++) { QString& line = m_content[(m_firstLine + i) % m_maxLines].line; out.append(line + '\n'); } out.squeeze(); return out; } void LogModel::setMaxLines(int maxLines) { // no-op if (maxLines == m_maxLines) { return; } // if it all still fits in the buffer, just resize it if (m_firstLine + m_numLines < m_maxLines) { m_maxLines = maxLines; m_content.resize(maxLines); return; } // otherwise, we need to reorganize the data because it crosses the wrap boundary QList newContent; newContent.resize(maxLines); if (m_numLines <= maxLines) { // if it all fits in the new buffer, just copy it over for (int i = 0; i < m_numLines; i++) { newContent[i] = m_content[(m_firstLine + i) % m_maxLines]; } m_content.swap(newContent); } else { // if it doesn't fit, part of the data needs to be thrown away (the oldest log messages) int lead = m_numLines - maxLines; beginRemoveRows(QModelIndex(), 0, lead - 1); for (int i = 0; i < maxLines; i++) { newContent[i] = m_content[(m_firstLine + lead + i) % m_maxLines]; } m_numLines = m_maxLines; m_content.swap(newContent); endRemoveRows(); } m_firstLine = 0; m_maxLines = maxLines; } int LogModel::getMaxLines() { return m_maxLines; } void LogModel::setStopOnOverflow(bool stop) { m_stopOnOverflow = stop; } void LogModel::setOverflowMessage(const QString& overflowMessage) { m_overflowMessage = overflowMessage; } void LogModel::setLineWrap(bool state) { if (m_lineWrap != state) { m_lineWrap = state; } } bool LogModel::wrapLines() const { return m_lineWrap; } void LogModel::setColorLines(bool state) { if (m_colorLines != state) { m_colorLines = state; } } bool LogModel::colorLines() const { return m_colorLines; } bool LogModel::isOverFlow() { return m_numLines >= m_maxLines && m_stopOnOverflow; } MessageLevel LogModel::previousLevel() { if (m_numLines > 0) { return m_content[m_numLines - 1].level; } return MessageLevel::Unknown; } PrismLauncher-11.0.3/launcher/launch/LogModel.h0000644000175100017510000000263015224505336020712 0ustar runnerrunner#pragma once #include #include #include "MessageLevel.h" class LogModel : public QAbstractListModel { Q_OBJECT public: explicit LogModel(QObject* parent = 0); int rowCount(const QModelIndex& parent = QModelIndex()) const; QVariant data(const QModelIndex& index, int role) const; void append(MessageLevel, QString line); void clear(); void suspend(bool suspend); bool suspended(); QString toPlainText(); int getMaxLines(); void setMaxLines(int maxLines); void setStopOnOverflow(bool stop); void setOverflowMessage(const QString& overflowMessage); bool isOverFlow(); void setLineWrap(bool state); bool wrapLines() const; void setColorLines(bool state); bool colorLines() const; MessageLevel previousLevel(); enum Roles { LevelRole = Qt::UserRole }; private /* types */: struct entry { MessageLevel level = MessageLevel::Unknown; QString line; }; private: /* data */ QList m_content; int m_maxLines = 1000; // first line in the circular buffer int m_firstLine = 0; // number of lines occupied in the circular buffer int m_numLines = 0; bool m_stopOnOverflow = false; QString m_overflowMessage = "OVERFLOW"; bool m_suspended = false; bool m_lineWrap = true; bool m_colorLines = true; private: Q_DISABLE_COPY(LogModel) }; PrismLauncher-11.0.3/launcher/launch/TaskStepWrapper.cpp0000644000175100017510000000314415224505336022643 0ustar runnerrunner/* 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 "TaskStepWrapper.h" #include "tasks/Task.h" void TaskStepWrapper::executeTask() { if (m_state == Task::State::AbortedByUser) { emitFailed(tr("Task aborted.")); return; } connect(m_task.get(), &Task::finished, this, &TaskStepWrapper::updateFinished); propagateFromOther(m_task.get()); emit progressReportingRequest(); } void TaskStepWrapper::proceed() { m_task->start(); } void TaskStepWrapper::updateFinished() { if (m_task->wasSuccessful()) { m_task.reset(); emitSucceeded(); } else { QString reason = tr("Instance update failed because: %1\n\n").arg(m_task->failReason()); m_task.reset(); emit logLine(reason, MessageLevel::Fatal); emitFailed(reason); } } bool TaskStepWrapper::canAbort() const { if (m_task) { return m_task->canAbort(); } return true; } bool TaskStepWrapper::abort() { if (m_task && m_task->canAbort()) { return m_task->abort(); } return Task::abort(); } PrismLauncher-11.0.3/launcher/Filter.h0000644000175100017510000000263615224505336017171 0ustar runnerrunner#pragma once #include #include using Filter = std::function; namespace Filters { inline Filter inverse(Filter filter) { return [filter = std::move(filter)](const QString& src) { return !filter(src); }; } inline Filter any(QList filters) { return [filters = std::move(filters)](const QString& src) { for (auto& filter : filters) if (filter(src)) return true; return false; }; } inline Filter equals(QString pattern) { return [pattern = std::move(pattern)](const QString& src) { return src == pattern; }; } inline Filter equalsAny(QStringList patterns = {}) { return [patterns = std::move(patterns)](const QString& src) { return patterns.isEmpty() || patterns.contains(src); }; } inline Filter equalsOrEmpty(QString pattern) { return [pattern = std::move(pattern)](const QString& src) { return src.isEmpty() || src == pattern; }; } inline Filter contains(QString pattern) { return [pattern = std::move(pattern)](const QString& src) { return src.contains(pattern); }; } inline Filter startsWith(QString pattern) { return [pattern = std::move(pattern)](const QString& src) { return src.startsWith(pattern); }; } inline Filter regexp(QRegularExpression pattern) { return [pattern = std::move(pattern)](const QString& src) { return pattern.match(src).hasMatch(); }; } } // namespace Filters PrismLauncher-11.0.3/launcher/InstanceImportTask.h0000644000175100017510000000514015224505336021517 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include "InstanceTask.h" class InstanceImportTask : public InstanceTask { Q_OBJECT public: explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap&& extra_info = {}); virtual ~InstanceImportTask() = default; bool abort() override; protected: //! Entry point for tasks. virtual void executeTask() override; private: void processMultiMC(); void processTechnic(); void processFlame(); void processModrinth(); private slots: void processZipPack(); void extractFinished(); private: /* data */ QUrl m_sourceUrl; QString m_archivePath; Task::Ptr m_task; enum class ModpackType { Unknown, MultiMC, Technic, Flame, Modrinth, } m_modpackType = ModpackType::Unknown; // Extra info we might need, that's available before, but can't be derived from // the source URL / the resource it points to alone. QMap m_extra_info; // FIXME: nuke QWidget* m_parent; void downloadFromUrl(); }; PrismLauncher-11.0.3/launcher/ProblemProvider.h0000644000175100017510000000171615224505336021055 0ustar runnerrunner#pragma once #include #include enum class ProblemSeverity { None, Warning, Error }; struct PatchProblem { ProblemSeverity m_severity; QString m_description; }; class ProblemProvider { public: virtual ~ProblemProvider() {} virtual const QList getProblems() const = 0; virtual ProblemSeverity getProblemSeverity() const = 0; }; class ProblemContainer : public ProblemProvider { public: const QList getProblems() const override { return m_problems; } ProblemSeverity getProblemSeverity() const override { return m_problemSeverity; } virtual void addProblem(ProblemSeverity severity, const QString& description) { if (severity > m_problemSeverity) { m_problemSeverity = severity; } m_problems.append({ severity, description }); } private: QList m_problems; ProblemSeverity m_problemSeverity = ProblemSeverity::None; }; PrismLauncher-11.0.3/launcher/DesktopServices.cpp0000644000175100017510000000520115224505336021403 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 dada513 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2022 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 "DesktopServices.h" #include #include #include #include #include "FileSystem.h" namespace DesktopServices { bool openPath(const QFileInfo& path, bool ensureFolderPathExists) { qDebug() << "Opening path" << path; if (ensureFolderPathExists) { FS::ensureFolderPathExists(path); } return openUrl(QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath())); } bool openPath(const QString& path, bool ensureFolderPathExists) { return openPath(QFileInfo(path), ensureFolderPathExists); } bool run(const QString& application, const QStringList& args, const QString& workingDirectory, qint64* pid) { qDebug() << "Running" << application << "with args" << args.join(' '); return QProcess::startDetached(application, args, workingDirectory, pid); } bool openUrl(const QUrl& url) { qDebug() << "Opening URL" << url.toString(); return QDesktopServices::openUrl(url); } bool isFlatpak() { #ifdef Q_OS_LINUX return QFile::exists("/.flatpak-info"); #else return false; #endif } bool isSnap() { #ifdef Q_OS_LINUX return getenv("SNAP"); #else return false; #endif } } // namespace DesktopServices PrismLauncher-11.0.3/launcher/InstanceCopyPrefs.h0000644000175100017510000000332215224505336021334 0ustar runnerrunner// // Created by marcelohdez on 10/22/22. // #pragma once #include struct InstanceCopyPrefs { public: bool allTrue() const; QString getSelectedFiltersAsRegex() const; QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const; // Getters bool isCopySavesEnabled() const; bool isKeepPlaytimeEnabled() const; bool isCopyGameOptionsEnabled() const; bool isCopyResourcePacksEnabled() const; bool isCopyShaderPacksEnabled() const; bool isCopyServersEnabled() const; bool isCopyModsEnabled() const; bool isCopyScreenshotsEnabled() const; bool isUseSymLinksEnabled() const; bool isLinkRecursivelyEnabled() const; bool isUseHardLinksEnabled() const; bool isDontLinkSavesEnabled() const; bool isUseCloneEnabled() const; // Setters void enableCopySaves(bool b); void enableKeepPlaytime(bool b); void enableCopyGameOptions(bool b); void enableCopyResourcePacks(bool b); void enableCopyShaderPacks(bool b); void enableCopyServers(bool b); void enableCopyMods(bool b); void enableCopyScreenshots(bool b); void enableUseSymLinks(bool b); void enableLinkRecursively(bool b); void enableUseHardLinks(bool b); void enableDontLinkSaves(bool b); void enableUseClone(bool b); protected: // data bool copySaves = true; bool keepPlaytime = true; bool copyGameOptions = true; bool copyResourcePacks = true; bool copyShaderPacks = true; bool copyServers = true; bool copyMods = true; bool copyScreenshots = true; bool useSymLinks = false; bool linkRecursively = false; bool useHardLinks = false; bool dontLinkSaves = false; bool useClone = false; }; PrismLauncher-11.0.3/launcher/InstanceTask.h0000644000175100017510000000446115224505336020331 0ustar runnerrunner#pragma once #include "settings/SettingsObject.h" #include "tasks/Task.h" /* Helpers */ enum class InstanceNameChange { ShouldChange, ShouldKeep }; [[nodiscard]] InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name); enum class ShouldUpdate { Update, SkipUpdating, Cancel }; [[nodiscard]] ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name); enum class ShouldDeleteSaves { NotAsked, Yes, No }; [[nodiscard]] ShouldDeleteSaves askIfShouldDeleteSaves(QWidget* parent); struct InstanceName { public: InstanceName() = default; InstanceName(QString name, QString version) : m_original_name(std::move(name)), m_original_version(std::move(version)) {} QString modifiedName() const; QString originalName() const; QString name() const; QString version() const; void setName(QString name) { m_modified_name = name; } void setName(InstanceName& other); protected: QString m_original_name; QString m_original_version; QString m_modified_name; }; class InstanceTask : public Task, public InstanceName { Q_OBJECT public: InstanceTask(); ~InstanceTask() override = default; void setParentSettings(SettingsObject* settings) { m_globalSettings = settings; } void setStagingPath(const QString& stagingPath) { m_stagingPath = stagingPath; } void setIcon(const QString& icon) { m_instIcon = icon; } void setGroup(const QString& group) { m_instGroup = group; } QString group() const { return m_instGroup; } bool shouldConfirmUpdate() const { return m_confirm_update; } void setConfirmUpdate(bool confirm) { m_confirm_update = confirm; } bool shouldOverride() const { return m_override_existing; } QString originalInstanceID() const { return m_original_instance_id; }; protected: void setOverride(bool override, QString instance_id_to_override = {}) { m_override_existing = override; if (!instance_id_to_override.isEmpty()) m_original_instance_id = instance_id_to_override; } protected: /* data */ SettingsObject* m_globalSettings; QString m_instIcon; QString m_instGroup; QString m_stagingPath; bool m_override_existing = false; bool m_confirm_update = true; QString m_original_instance_id; }; PrismLauncher-11.0.3/launcher/ExponentialSeries.h0000644000175100017510000000142315224505336021376 0ustar runnerrunner #pragma once template inline void clamp(T& current, T min, T max) { if (current < min) { current = min; } else if (current > max) { current = max; } } // List of numbers from min to max. Next is exponent times bigger than previous. class ExponentialSeries { public: ExponentialSeries(unsigned min, unsigned max, unsigned exponent = 2) { m_current = m_min = min; m_max = max; m_exponent = exponent; } void reset() { m_current = m_min; } unsigned operator()() { unsigned retval = m_current; m_current *= m_exponent; clamp(m_current, m_min, m_max); return retval; } unsigned m_current; unsigned m_min; unsigned m_max; unsigned m_exponent; }; PrismLauncher-11.0.3/launcher/RWStorage.h0000644000175100017510000000240115224505336017607 0ustar runnerrunner#pragma once #include #include #include #include template class RWStorage { public: void add(K key, V value) { QWriteLocker l(&lock); cache[key] = value; stale_entries.remove(key); } V get(K key) { QReadLocker l(&lock); if (cache.contains(key)) { return cache[key]; } else return V(); } bool get(K key, V& value) { QReadLocker l(&lock); if (cache.contains(key)) { value = cache[key]; return true; } else return false; } bool has(K key) { QReadLocker l(&lock); return cache.contains(key); } bool stale(K key) { QReadLocker l(&lock); if (!cache.contains(key)) return true; return stale_entries.contains(key); } void setStale(K key) { QWriteLocker l(&lock); if (cache.contains(key)) { stale_entries.insert(key); } } void clear() { QWriteLocker l(&lock); cache.clear(); stale_entries.clear(); } private: QReadWriteLock lock; QMap cache; QSet stale_entries; }; PrismLauncher-11.0.3/launcher/qtlogging.ini0000644000175100017510000000111615224505336020257 0ustar runnerrunner[Rules] *.debug=true # prevent log spam and strange bugs # qt.qpa.drawing in particular causes theme artifacts on MacOS qt.*.debug=false # supress image format noise kf.imageformats.plugins.hdr=false kf.imageformats.plugins.xcf=false # don't log credentials by default launcher.auth.credentials.debug=false # remove the debug lines, other log levels still get through launcher.task.net.download.debug=false # enable or disable whole catageries launcher.task.net=true launcher.task=false launcher.task.net.upload=true launcher.task.net.metacache=false launcher.task.net.metacache.http=true PrismLauncher-11.0.3/launcher/FileIgnoreProxy.cpp0000644000175100017510000002363515224505336021366 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "FileIgnoreProxy.h" #include #include #include #include #include "FileSystem.h" #include "SeparatorPrefixTree.h" #include "StringUtils.h" FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), m_root(root) {} // NOTE: Sadly, we have to do sorting ourselves. bool FileIgnoreProxy::lessThan(const QModelIndex& left, const QModelIndex& right) const { QFileSystemModel* fsm = qobject_cast(sourceModel()); if (!fsm) { return QSortFilterProxyModel::lessThan(left, right); } bool asc = sortOrder() == Qt::AscendingOrder ? true : false; QFileInfo leftFileInfo = fsm->fileInfo(left); QFileInfo rightFileInfo = fsm->fileInfo(right); if (!leftFileInfo.isDir() && rightFileInfo.isDir()) { return !asc; } if (leftFileInfo.isDir() && !rightFileInfo.isDir()) { return asc; } // sort and proxy model breaks the original model... if (sortColumn() == 0) { return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0; } if (sortColumn() == 1) { auto leftSize = leftFileInfo.size(); auto rightSize = rightFileInfo.size(); if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir())) { return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0 ? asc : !asc; } return leftSize < rightSize; } return QSortFilterProxyModel::lessThan(left, right); } Qt::ItemFlags FileIgnoreProxy::flags(const QModelIndex& index) const { if (!index.isValid()) return Qt::NoItemFlags; auto sourceIndex = mapToSource(index); Qt::ItemFlags flags = sourceIndex.flags(); if (index.column() == 0) { flags |= Qt::ItemIsUserCheckable; if (sourceIndex.model()->hasChildren(sourceIndex)) { flags |= Qt::ItemIsAutoTristate; } } return flags; } QVariant FileIgnoreProxy::data(const QModelIndex& index, int role) const { QModelIndex sourceIndex = mapToSource(index); if (index.column() == 0 && role == Qt::CheckStateRole) { QFileSystemModel* fsm = qobject_cast(sourceModel()); auto blockedPath = relPath(fsm->filePath(sourceIndex)); auto cover = m_blocked.cover(blockedPath); if (!cover.isNull()) { return QVariant(Qt::Unchecked); } else if (m_blocked.exists(blockedPath)) { return QVariant(Qt::PartiallyChecked); } else { return QVariant(Qt::Checked); } } return sourceIndex.data(role); } bool FileIgnoreProxy::setData(const QModelIndex& index, const QVariant& value, int role) { if (index.column() == 0 && role == Qt::CheckStateRole) { Qt::CheckState state = static_cast(value.toInt()); return setFilterState(index, state); } QModelIndex sourceIndex = mapToSource(index); return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role); } QString FileIgnoreProxy::relPath(const QString& path) const { return QDir(m_root).relativeFilePath(path); } bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state) { QFileSystemModel* fsm = qobject_cast(sourceModel()); if (!fsm) { return false; } QModelIndex sourceIndex = mapToSource(index); auto blockedPath = relPath(fsm->filePath(sourceIndex)); bool changed = false; if (state == Qt::Unchecked) { // blocking a path auto& node = m_blocked.insert(blockedPath); // get rid of all blocked nodes below node.clear(); changed = true; } else if (state == Qt::Checked || state == Qt::PartiallyChecked) { if (!m_blocked.remove(blockedPath)) { auto cover = m_blocked.cover(blockedPath); qDebug() << "Blocked by cover" << cover; // uncover m_blocked.remove(cover); // block all contents, except for any cover QModelIndex rootIndex = fsm->index(FS::PathCombine(m_root, cover)); QModelIndex doing = rootIndex; int row = 0; QStack todo; while (1) { auto node = fsm->index(row, 0, doing); if (!node.isValid()) { if (!todo.size()) { break; } else { doing = todo.pop(); row = 0; continue; } } auto relpath = relPath(fsm->filePath(node)); if (blockedPath.startsWith(relpath)) // cover found? { // continue processing cover later todo.push(node); } else { // or just block this one. m_blocked.insert(relpath); } row++; } } changed = true; } if (changed) { // update the thing emit dataChanged(index, index, { Qt::CheckStateRole }); // update everything above index QModelIndex up = index.parent(); while (1) { if (!up.isValid()) break; emit dataChanged(up, up, { Qt::CheckStateRole }); up = up.parent(); } // and everything below the index QModelIndex doing = index; int row = 0; QStack todo; while (1) { auto node = this->index(row, 0, doing); if (!node.isValid()) { if (!todo.size()) { break; } else { doing = todo.pop(); row = 0; continue; } } emit dataChanged(node, node, { Qt::CheckStateRole }); todo.push(node); row++; } // siblings and unrelated nodes are ignored } return true; } bool FileIgnoreProxy::shouldExpand(QModelIndex index) { QModelIndex sourceIndex = mapToSource(index); QFileSystemModel* fsm = qobject_cast(sourceModel()); if (!fsm) { return false; } auto blockedPath = relPath(fsm->filePath(sourceIndex)); auto found = m_blocked.find(blockedPath); if (found) { return !found->leaf(); } return false; } void FileIgnoreProxy::setBlockedPaths(QStringList paths) { beginResetModel(); m_blocked.clear(); m_blocked.insert(paths); endResetModel(); } bool FileIgnoreProxy::filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const { Q_UNUSED(source_parent) // adjust the columns you want to filter out here // return false for those that will be hidden if (source_column == 2 || source_column == 3) return false; return true; } bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); QFileSystemModel* fsm = qobject_cast(sourceModel()); auto fileInfo = fsm->fileInfo(index); return !ignoreFile(fileInfo); } bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const { if (m_ignoreFiles.contains(fileInfo.fileName())) { return true; } for (const auto& suffix : m_ignoreFilesSuffixes) { if (fileInfo.fileName().endsWith(suffix)) { return true; } } if (m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath()))) { return true; } return false; } bool FileIgnoreProxy::filterFile(const QFileInfo& file) const { return m_blocked.covers(relPath(file.absoluteFilePath())) || ignoreFile(file); } void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName) { QFile ignoreFile(fileName); if (!ignoreFile.open(QIODevice::ReadOnly)) { return; } auto ignoreData = ignoreFile.readAll(); auto string = QString::fromUtf8(ignoreData); setBlockedPaths(string.split('\n', Qt::SkipEmptyParts)); } void FileIgnoreProxy::saveBlockedPathsToFile(const QString& fileName) { auto ignoreData = blockedPaths().toStringList().join('\n').toUtf8(); try { FS::write(fileName, ignoreData); } catch (const Exception& e) { qWarning() << e.cause(); } } PrismLauncher-11.0.3/launcher/LibraryUtils.h0000644000175100017510000000162415224505336020365 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * PrismLauncher - Minecraft Launcher * Copyright (C) 2022 Jan Drögehoff * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include namespace LibraryUtils { QString findMangoHud(); QString find(QString libName); } // namespace LibraryUtils PrismLauncher-11.0.3/launcher/MMCTime.cpp0000644000175100017510000000604615224505336017531 0ustar runnerrunner/* * Copyright 2015 Petr Mrazek * Copyright 2021 Jamie Mansfield * * 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 #include #include #include QString Time::prettifyDuration(int64_t duration, bool noDays) { int seconds = (int)(duration % 60); duration /= 60; int minutes = (int)(duration % 60); duration /= 60; int hours = (int)(noDays ? duration : (duration % 24)); int days = (int)(noDays ? 0 : (duration / 24)); if ((hours == 0) && (days == 0)) { return QObject::tr("%1min %2s").arg(minutes).arg(seconds); } if (days == 0) { return QObject::tr("%1h %2min").arg(hours).arg(minutes); } return QObject::tr("%1d %2h %3min").arg(days).arg(hours).arg(minutes); } QString Time::humanReadableDuration(double duration, int precision) { using days = std::chrono::duration>; QString outStr; QTextStream os(&outStr); bool neg = false; if (duration < 0) { neg = true; // flag duration *= -1; // invert } auto std_duration = std::chrono::duration(duration); auto d = std::chrono::duration_cast(std_duration); std_duration -= d; auto h = std::chrono::duration_cast(std_duration); std_duration -= h; auto m = std::chrono::duration_cast(std_duration); std_duration -= m; auto s = std::chrono::duration_cast(std_duration); std_duration -= s; auto ms = std::chrono::duration_cast(std_duration); auto dc = d.count(); auto hc = h.count(); auto mc = m.count(); auto sc = s.count(); auto msc = ms.count(); if (neg) { os << '-'; } if (dc) { os << dc << QObject::tr("days"); } if (hc) { if (dc) os << " "; os << qSetFieldWidth(2) << hc << QObject::tr("h"); // hours } if (mc) { if (dc || hc) os << " "; os << qSetFieldWidth(2) << mc << QObject::tr("m"); // minutes } if (dc || hc || mc || sc) { if (dc || hc || mc) os << " "; os << qSetFieldWidth(2) << sc << QObject::tr("s"); // seconds } if ((msc && (precision > 0)) || !(dc || hc || mc || sc)) { if (dc || hc || mc || sc) os << " "; os << qSetFieldWidth(0) << qSetRealNumberPrecision(precision) << msc << QObject::tr("ms"); // miliseconds } os.flush(); return outStr; } PrismLauncher-11.0.3/launcher/ResourceDownloadTask.cpp0000644000175100017510000001376115224505336022402 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022-2023 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ResourceDownloadTask.h" #include #include "Application.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/ResourceFolderModel.h" #include "minecraft/mod/ShaderPackFolderModel.h" #include "modplatform/ModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "net/ApiDownload.h" #include "net/ChecksumValidator.h" namespace { Net::ModrinthDownloadMeta createModrinthMeta(BaseInstance* instance, QString reason) { auto* mcInstance = dynamic_cast(instance); if (!mcInstance) { return {}; } auto* profile = mcInstance->getPackProfile(); if (!profile) { return {}; } auto loaders = profile->getModLoadersList(); return { .reason = std::move(reason), .gameVersion = profile->getComponentVersion("net.minecraft"), .loader = !loaders.isEmpty() ? ModPlatform::getModLoaderAsString(loaders.first()) : "", }; } } // namespace ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion version, ResourceFolderModel* packs, bool isIndexed, QString downloadReason) : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs) { if (isIndexed) { m_update_task.reset(new LocalResourceUpdateTask(m_pack_model->indexDir(), *m_pack, m_pack_version)); connect(m_update_task.get(), &LocalResourceUpdateTask::hasOldResource, this, &ResourceDownloadTask::hasOldResource); addTask(m_update_task); } m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network())); m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl)); auto action = Net::ApiDownload::makeFile(m_pack_version.downloadUrl, m_pack_model->dir().absoluteFilePath(getFilename()), Net::Download::Option::NoOptions, createModrinthMeta(m_pack_model->instance(), std::move(downloadReason))); if (!m_pack_version.hash_type.isEmpty() && !m_pack_version.hash.isEmpty()) { switch (Hashing::algorithmFromString(m_pack_version.hash_type)) { case Hashing::Algorithm::Md4: action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Md4, m_pack_version.hash)); break; case Hashing::Algorithm::Md5: action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Md5, m_pack_version.hash)); break; case Hashing::Algorithm::Sha1: action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha1, m_pack_version.hash)); break; case Hashing::Algorithm::Sha256: action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha256, m_pack_version.hash)); break; case Hashing::Algorithm::Sha512: action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha512, m_pack_version.hash)); break; default: break; } } m_filesNetJob->addNetAction(action); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ResourceDownloadTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &ResourceDownloadTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &ResourceDownloadTask::propagateStepProgress); connect(m_filesNetJob.get(), &NetJob::failed, this, &ResourceDownloadTask::downloadFailed); addTask(m_filesNetJob); } void ResourceDownloadTask::downloadSucceeded() { m_filesNetJob.reset(); auto oldName = std::get<0>(to_delete); auto oldFilename = std::get<1>(to_delete); if (oldName.isEmpty() || oldFilename == m_pack_version.fileName) { return; } m_pack_model->uninstallResource(oldFilename, true); // also rename the shader config file if (dynamic_cast(m_pack_model) != nullptr) { QFileInfo oldConfig(m_pack_model->dir(), oldFilename + ".txt"); QFileInfo newConfig(m_pack_model->dir(), getFilename() + ".txt"); if (oldConfig.exists() && !newConfig.exists()) { bool success = FS::move(oldConfig.filePath(), newConfig.filePath()); if (!success) { emit logWarning(tr("Failed to rename shader config from '%1' to '%2'").arg(oldConfig.fileName(), newConfig.fileName())); } } } } void ResourceDownloadTask::downloadFailed(QString reason) { m_filesNetJob.reset(); emitFailed(std::move(reason)); } void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total) { emit progress(current, total); } // This indirection is done so that we don't delete a mod before being sure it was // downloaded successfully! void ResourceDownloadTask::hasOldResource(const QString& name, const QString& filename) { to_delete = { name, filename }; } PrismLauncher-11.0.3/launcher/StringUtils.h0000644000175100017510000000571115224505336020230 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include namespace StringUtils { #if defined Q_OS_WIN32 using string = std::wstring; inline string toStdString(QString s) { return s.toStdWString(); } inline QString fromStdString(string s) { return QString::fromStdWString(s); } #else using string = std::string; inline string toStdString(QString s) { return s.toStdString(); } inline QString fromStdString(string s) { return QString::fromStdString(s); } #endif int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs); /** * @brief Truncate a url while keeping its readability py placing the `...` in the middle of the path * @param url Url to truncate * @param max_len max length of url in characters * @param hard_limit if truncating the path can't get the url short enough, truncate it normally. */ QString truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_limit = false); QString humanReadableFileSize(double bytes, bool use_si = false, int decimal_points = 1); QString getRandomAlphaNumeric(); QPair splitFirst(const QString& s, const QString& sep, Qt::CaseSensitivity cs = Qt::CaseSensitive); QPair splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive); QPair splitFirst(const QString& s, const QRegularExpression& re); QString htmlListPatch(QString htmlStr); } // namespace StringUtils PrismLauncher-11.0.3/launcher/FastFileIconProvider.cpp0000644000175100017510000000271015224505336022311 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "FastFileIconProvider.h" #include #include QIcon FastFileIconProvider::icon(const QFileInfo& info) const { #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) bool link = info.isSymbolicLink() || info.isAlias() || info.isShortcut(); #else // in versions prior to 6.4 we don't have access to isAlias bool link = info.isSymLink(); #endif QStyle::StandardPixmap icon; if (info.isDir()) { if (link) icon = QStyle::SP_DirLinkIcon; else icon = QStyle::SP_DirIcon; } else { if (link) icon = QStyle::SP_FileLinkIcon; else icon = QStyle::SP_FileIcon; } return QApplication::style()->standardIcon(icon); } PrismLauncher-11.0.3/launcher/InstanceCreationTask.cpp0000644000175100017510000000736115224505336022353 0ustar runnerrunner#include "InstanceCreationTask.h" #include #include #include "InstanceTask.h" #include "minecraft/MinecraftLoadAndCheck.h" #include "tasks/SequentialTask.h" bool InstanceCreationTask::abort() { if (!canAbort()) { return false; } m_abort = true; if (m_gameFilesTask) { return m_gameFilesTask->abort(); } return InstanceTask::abort(); } void InstanceCreationTask::executeTask() { setAbortable(true); if (updateInstance()) { emitSucceeded(); return; } // When the user aborted in the update stage. if (m_abort) { emitAborted(); return; } m_instance = createInstance(); if (!m_instance) { if (m_abort) return; qWarning() << "Instance creation failed!"; if (!m_error_message.isEmpty()) { qWarning() << "Reason:" << m_error_message; emitFailed(tr("Error while creating new instance:\n%1").arg(m_error_message)); } else { emitFailed(tr("Error while creating new instance.")); } return; } // If this is set, it means we're updating an instance. So, we now need to remove the // files scheduled to, and we'd better not let the user abort in the middle of it, since it'd // put the instance in an invalid state. if (shouldOverride()) { bool deleteFailed = false; setAbortable(false); setStatus(tr("Removing old conflicting files...")); qDebug() << "Removing old files"; for (const QString& path : m_filesToRemove) { if (!QFile::exists(path)) continue; qDebug() << "Removing" << path; if (!QFile::remove(path)) { qCritical() << "Could not remove" << path; deleteFailed = true; } } if (deleteFailed) { emitFailed(tr("Failed to remove old conflicting files.")); return; } } if (!m_abort) { setAbortable(true); setAbortButtonText(tr("Skip")); qDebug() << "Downloading game files"; auto updateTasks = m_instance->createUpdateTask(); if (updateTasks.isEmpty()) { emitSucceeded(); return; } auto task = makeShared(); task->addTask(makeShared(m_instance.get(), Net::Mode::Online)); for (const auto& t : updateTasks) { task->addTask(t); } connect(task.get(), &Task::finished, this, [this, task] { if (task->wasSuccessful() || m_abort) { emitSucceeded(); } else { emitFailed(tr("Could not download game files: %1").arg(task->failReason())); } }); propagateFromOther(task.get()); setDetails(tr("Downloading game files")); m_gameFilesTask = task; m_gameFilesTask->start(); } } void InstanceCreationTask::scheduleToDelete(QWidget* parent, QDir dir, QString path, bool checkDisabled) { if (path.isEmpty()) { return; } if (path.startsWith("saves/")) { if (m_shouldDeleteSaves == ShouldDeleteSaves::NotAsked) { m_shouldDeleteSaves = askIfShouldDeleteSaves(parent); } if (m_shouldDeleteSaves == ShouldDeleteSaves::No) { return; } } qDebug() << "Scheduling" << path << "for removal"; m_filesToRemove.append(dir.absoluteFilePath(path)); if (checkDisabled) { if (path.endsWith(".disabled")) { // remove it if it was enabled/disabled by user m_filesToRemove.append(dir.absoluteFilePath(path.chopped(9))); } else { m_filesToRemove.append(dir.absoluteFilePath(path + ".disabled")); } } } PrismLauncher-11.0.3/launcher/BaseInstance.cpp0000644000175100017510000003537115224505336020640 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "BaseInstance.h" #include #include #include #include #include #include "Application.h" #include "Json.h" #include "launch/LaunchTask.h" #include "settings/INISettingsObject.h" #include "settings/OverrideSetting.h" #include "settings/Setting.h" #include "BuildConfig.h" #include "Commandline.h" #include "FileSystem.h" int getConsoleMaxLines(SettingsObject* settings) { auto lineSetting = settings->getSetting("ConsoleMaxLines"); bool conversionOk = false; int maxLines = lineSetting->get().toInt(&conversionOk); if (!conversionOk) { maxLines = lineSetting->defValue().toInt(); qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; } return maxLines; } bool shouldStopOnConsoleOverflow(SettingsObject* settings) { return settings->get("ConsoleOverflowStop").toBool(); } BaseInstance::BaseInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir) : QObject() { m_settings = std::move(settings); m_global_settings = globalSettings; m_rootDir = rootDir; m_settings->registerSetting("name", "Unnamed Instance"); m_settings->registerSetting("iconKey", "default"); m_settings->registerSetting("notes", ""); m_settings->registerSetting("lastLaunchTime", 0); m_settings->registerSetting("totalTimePlayed", 0); if (m_settings->get("totalTimePlayed").toLongLong() < 0) m_settings->reset("totalTimePlayed"); m_settings->registerSetting("lastTimePlayed", 0); m_settings->registerSetting("linkedInstances", "[]"); m_settings->registerSetting("shortcuts", QString()); // Game time override auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride); m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride); // NOTE: Sometimees InstanceType is already registered, as it was used to identify the type of // a locally stored instance if (!m_settings->getSetting("InstanceType")) m_settings->registerSetting("InstanceType", ""); // Custom Commands auto commandSetting = m_settings->registerSetting({ "OverrideCommands", "OverrideLaunchCmd" }, false); m_settings->registerOverride(globalSettings->getSetting("PreLaunchCommand"), commandSetting); m_settings->registerOverride(globalSettings->getSetting("WrapperCommand"), commandSetting); m_settings->registerOverride(globalSettings->getSetting("PostExitCommand"), commandSetting); // Console auto consoleSetting = m_settings->registerSetting("OverrideConsole", false); m_settings->registerOverride(globalSettings->getSetting("ShowConsole"), consoleSetting); m_settings->registerOverride(globalSettings->getSetting("AutoCloseConsole"), consoleSetting); m_settings->registerOverride(globalSettings->getSetting("ShowConsoleOnError"), consoleSetting); m_settings->registerOverride(globalSettings->getSetting("LogPrePostOutput"), consoleSetting); m_settings->registerPassthrough(globalSettings->getSetting("ConsoleMaxLines"), nullptr); m_settings->registerPassthrough(globalSettings->getSetting("ConsoleOverflowStop"), nullptr); // Managed Packs m_settings->registerSetting("ManagedPack", false); m_settings->registerSetting("ManagedPackType", ""); m_settings->registerSetting("ManagedPackID", ""); m_settings->registerSetting("ManagedPackName", ""); m_settings->registerSetting("ManagedPackVersionID", ""); m_settings->registerSetting("ManagedPackVersionName", ""); m_settings->registerSetting("ManagedPackURL", ""); m_settings->registerSetting("Profiler", ""); } BaseInstance::~BaseInstance() {} QString BaseInstance::getPreLaunchCommand() { return settings()->get("PreLaunchCommand").toString(); } QString BaseInstance::getWrapperCommand() { return settings()->get("WrapperCommand").toString(); } QString BaseInstance::getPostExitCommand() { return settings()->get("PostExitCommand").toString(); } bool BaseInstance::isManagedPack() const { return m_settings->get("ManagedPack").toBool(); } QString BaseInstance::getManagedPackType() const { return m_settings->get("ManagedPackType").toString(); } QString BaseInstance::getManagedPackID() const { return m_settings->get("ManagedPackID").toString(); } QString BaseInstance::getManagedPackName() const { return m_settings->get("ManagedPackName").toString(); } QString BaseInstance::getManagedPackVersionID() const { return m_settings->get("ManagedPackVersionID").toString(); } QString BaseInstance::getManagedPackVersionName() const { return m_settings->get("ManagedPackVersionName").toString(); } void BaseInstance::setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version) { m_settings->set("ManagedPack", true); m_settings->set("ManagedPackType", type); m_settings->set("ManagedPackID", id); m_settings->set("ManagedPackName", name); m_settings->set("ManagedPackVersionID", versionId); m_settings->set("ManagedPackVersionName", version); } void BaseInstance::copyManagedPack(BaseInstance& other) { m_settings->set("ManagedPack", other.isManagedPack()); m_settings->set("ManagedPackType", other.getManagedPackType()); m_settings->set("ManagedPackID", other.getManagedPackID()); m_settings->set("ManagedPackName", other.getManagedPackName()); m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID()); m_settings->set("ManagedPackVersionName", other.getManagedPackVersionName()); if (APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() && m_settings->get("AutomaticJava").toBool() && m_settings->get("OverrideJavaLocation").toBool()) { m_settings->set("OverrideJavaLocation", false); m_settings->set("JavaPath", ""); } } QStringList BaseInstance::getLinkedInstances() const { auto setting = m_settings->get("linkedInstances").toString(); return Json::toStringList(setting); } void BaseInstance::setLinkedInstances(const QStringList& list) { m_settings->set("linkedInstances", Json::fromStringList(list)); } void BaseInstance::addLinkedInstanceId(const QString& id) { auto linkedInstances = getLinkedInstances(); linkedInstances.append(id); setLinkedInstances(linkedInstances); } bool BaseInstance::removeLinkedInstanceId(const QString& id) { auto linkedInstances = getLinkedInstances(); int numRemoved = linkedInstances.removeAll(id); setLinkedInstances(linkedInstances); return numRemoved > 0; } bool BaseInstance::isLinkedToInstanceId(const QString& id) const { auto linkedInstances = getLinkedInstances(); return linkedInstances.contains(id); } void BaseInstance::iconUpdated(QString key) { if (iconKey() == key) { emit propertiesChanged(this); } } void BaseInstance::invalidate() { changeStatus(Status::Gone); qDebug() << "Instance" << id() << "has been invalidated."; } void BaseInstance::changeStatus(BaseInstance::Status newStatus) { Status status = currentStatus(); if (status != newStatus) { m_status = newStatus; emit statusChanged(status, newStatus); } } BaseInstance::Status BaseInstance::currentStatus() const { return m_status; } QString BaseInstance::id() const { return QFileInfo(instanceRoot()).fileName(); } bool BaseInstance::isRunning() const { return m_isRunning; } void BaseInstance::setRunning(bool running) { if (running == m_isRunning) return; m_isRunning = running; emit runningStatusChanged(running); } void BaseInstance::setMinecraftRunning(bool running) { if (!settings()->get("RecordGameTime").toBool()) { return; } if (running) { m_timeStarted = QDateTime::currentDateTime(); setLastLaunch(m_timeStarted.toMSecsSinceEpoch()); } else { QDateTime timeEnded = QDateTime::currentDateTime(); qint64 current = settings()->get("totalTimePlayed").toLongLong(); settings()->set("totalTimePlayed", current + m_timeStarted.secsTo(timeEnded)); settings()->set("lastTimePlayed", m_timeStarted.secsTo(timeEnded)); emit propertiesChanged(this); } } int64_t BaseInstance::totalTimePlayed() const { qint64 current = m_settings->get("totalTimePlayed").toLongLong(); if (m_isRunning) { QDateTime timeNow = QDateTime::currentDateTime(); return current + m_timeStarted.secsTo(timeNow); } return current; } int64_t BaseInstance::lastTimePlayed() const { if (m_isRunning) { QDateTime timeNow = QDateTime::currentDateTime(); return m_timeStarted.secsTo(timeNow); } return m_settings->get("lastTimePlayed").toLongLong(); } void BaseInstance::resetTimePlayed() { settings()->reset("totalTimePlayed"); settings()->reset("lastTimePlayed"); } QString BaseInstance::instanceType() const { return m_settings->get("InstanceType").toString(); } QString BaseInstance::instanceRoot() const { return m_rootDir; } SettingsObject* BaseInstance::settings() { loadSpecificSettings(); return m_settings.get(); } bool BaseInstance::canLaunch() const { return (!hasVersionBroken() && !isRunning()); } bool BaseInstance::reloadSettings() { return m_settings->reload(); } qint64 BaseInstance::lastLaunch() const { return m_settings->get("lastLaunchTime").value(); } void BaseInstance::setLastLaunch(qint64 val) { // FIXME: if no change, do not set. setting involves saving a file. m_settings->set("lastLaunchTime", val); emit propertiesChanged(this); } void BaseInstance::setNotes(QString val) { // FIXME: if no change, do not set. setting involves saving a file. m_settings->set("notes", val); } QString BaseInstance::notes() const { return m_settings->get("notes").toString(); } void BaseInstance::setIconKey(QString val) { // FIXME: if no change, do not set. setting involves saving a file. m_settings->set("iconKey", val); emit propertiesChanged(this); } QString BaseInstance::iconKey() const { return m_settings->get("iconKey").toString(); } void BaseInstance::setName(QString val) { // FIXME: if no change, do not set. setting involves saving a file. m_settings->set("name", val); emit propertiesChanged(this); } bool BaseInstance::syncInstanceDirName(const QString& newRoot) const { auto oldRoot = instanceRoot(); return oldRoot == newRoot || QFile::rename(oldRoot, newRoot); } void BaseInstance::registerShortcut(const ShortcutData& data) { auto currentShortcuts = shortcuts(); currentShortcuts.append(data); qDebug() << "Registering shortcut for instance" << id() << "with name" << data.name << "and path" << data.filePath; setShortcuts(currentShortcuts); } void BaseInstance::setShortcuts(const QList& shortcuts) { // FIXME: if no change, do not set. setting involves saving a file. QJsonArray array; for (const auto& elem : shortcuts) { array.append(QJsonObject{ { "name", elem.name }, { "filePath", elem.filePath }, { "target", static_cast(elem.target) } }); } QJsonDocument document; document.setArray(array); m_settings->set("shortcuts", QString::fromUtf8(document.toJson(QJsonDocument::Compact))); } QList BaseInstance::shortcuts() const { auto data = m_settings->get("shortcuts").toString().toUtf8(); QJsonParseError parseError; auto document = QJsonDocument::fromJson(data, &parseError); if (parseError.error != QJsonParseError::NoError || !document.isArray()) return {}; QList results; for (const auto& elem : document.array()) { if (!elem.isObject()) continue; auto dict = elem.toObject(); if (!dict.contains("name") || !dict.contains("filePath") || !dict.contains("target")) continue; int value = dict["target"].toInt(-1); if (!dict["name"].isString() || !dict["filePath"].isString() || value < 0 || value >= 3) continue; QString shortcutName = dict["name"].toString(); QString filePath = dict["filePath"].toString(); if (!QDir(filePath).exists()) { qWarning() << "Shortcut" << shortcutName << "for instance" << name() << "have non-existent path" << filePath; continue; } results.append({ shortcutName, filePath, static_cast(value) }); } return results; } QString BaseInstance::name() const { return m_settings->get("name").toString(); } QString BaseInstance::windowTitle() const { return BuildConfig.LAUNCHER_DISPLAYNAME + ": " + name(); } // FIXME: why is this here? move it to MinecraftInstance!!! QStringList BaseInstance::extraArguments() { return Commandline::splitArgs(settings()->get("JvmArgs").toString()); } LaunchTask* BaseInstance::getLaunchTask() { return m_launchProcess.get(); } void BaseInstance::updateRuntimeContext() { // NOOP } bool BaseInstance::isLegacy() { return traits().contains("legacyLaunch") || traits().contains("alphaLaunch"); } PrismLauncher-11.0.3/launcher/WatchLock.h0000644000175100017510000000056315224505336017620 0ustar runnerrunner #pragma once #include #include struct WatchLock { WatchLock(QFileSystemWatcher* watcher, const QString& directory) : m_watcher(watcher), m_directory(directory) { m_watcher->removePath(m_directory); } ~WatchLock() { m_watcher->addPath(m_directory); } QFileSystemWatcher* m_watcher; QString m_directory; }; PrismLauncher-11.0.3/launcher/DataMigrationTask.h0000644000175100017510000000165115224505336021306 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include "FileSystem.h" #include "Filter.h" #include "tasks/Task.h" #include #include /* * Migrate existing data from other MMC-like launchers. */ class DataMigrationTask : public Task { Q_OBJECT public: explicit DataMigrationTask(const QString& sourcePath, const QString& targetPath, Filter pathmatcher); ~DataMigrationTask() override = default; protected: virtual void executeTask() override; protected slots: void dryRunFinished(); void dryRunAborted(); void copyFinished(); void copyAborted(); private: const QString& m_sourcePath; const QString& m_targetPath; const Filter m_pathMatcher; FS::copy m_copy; int m_toCopy = 0; QFuture m_copyFuture; QFutureWatcher m_copyFutureWatcher; }; PrismLauncher-11.0.3/launcher/NullInstance.h0000644000175100017510000000650415224505336020341 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "BaseInstance.h" #include "launch/LaunchTask.h" class NullInstance : public BaseInstance { Q_OBJECT public: NullInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir) : BaseInstance(globalSettings, std::move(settings), rootDir) { setVersionBroken(true); } virtual ~NullInstance() = default; void saveNow() override {} void loadSpecificSettings() override { setSpecificSettingsLoaded(true); } QString getStatusbarDescription() override { return tr("Unknown instance type"); }; QSet traits() const override { return {}; }; QString instanceConfigFolder() const override { return instanceRoot(); }; LaunchTask* createLaunchTask(AuthSessionPtr, MinecraftTarget::Ptr) override { return nullptr; } QList createUpdateTask() override { return {}; } QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); } QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); } QMap getVariables() override { return QMap(); } QStringList getLogFileSearchPaths() override { return {}; } QString typeName() const override { return "Null"; } bool canExport() const override { return false; } bool canEdit() const override { return false; } bool canLaunch() const override { return false; } void populateLaunchMenu(QMenu* menu) override {} QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override { QStringList out; out << "Null instance - placeholder."; return out; } QString modsRoot() const override { return QString(); } void updateRuntimeContext() override { // NOOP } }; PrismLauncher-11.0.3/launcher/main.cpp0000644000175100017510000000525515224505336017223 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include "Application.h" #if defined Q_OS_WIN32 #include "console/WindowsConsole.h" #endif int main(int argc, char* argv[]) { #if defined Q_OS_WIN32 // used on Windows to attach the standard IO streams console::WindowsConsoleGuard _consoleGuard; #endif // initialize Qt Application app(argc, argv); switch (app.status()) { case Application::StartingUp: case Application::Initialized: { Q_INIT_RESOURCE(multimc); Q_INIT_RESOURCE(backgrounds); Q_INIT_RESOURCE(documents); Q_INIT_RESOURCE(prismlauncher); Q_INIT_RESOURCE(pe_dark); Q_INIT_RESOURCE(pe_light); Q_INIT_RESOURCE(pe_blue); Q_INIT_RESOURCE(pe_colored); Q_INIT_RESOURCE(breeze_dark); Q_INIT_RESOURCE(breeze_light); Q_INIT_RESOURCE(OSX); Q_INIT_RESOURCE(iOS); Q_INIT_RESOURCE(flat); Q_INIT_RESOURCE(flat_white); Q_INIT_RESOURCE(shaders); return app.exec(); } case Application::Failed: return 1; case Application::Succeeded: return 0; default: return -1; } } PrismLauncher-11.0.3/launcher/GZip.cpp0000644000175100017510000001466715224505336017157 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "GZip.h" #include #include #include #include bool GZip::unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes) { if (compressedBytes.size() == 0) { uncompressedBytes = compressedBytes; return true; } unsigned uncompLength = compressedBytes.size(); uncompressedBytes.clear(); uncompressedBytes.resize(uncompLength); z_stream strm; memset(&strm, 0, sizeof(strm)); strm.next_in = (Bytef*)compressedBytes.data(); strm.avail_in = compressedBytes.size(); bool done = false; if (inflateInit2(&strm, (16 + MAX_WBITS)) != Z_OK) { return false; } int err = Z_OK; while (!done) { // If our output buffer is too small if (strm.total_out >= uncompLength) { uncompressedBytes.resize(uncompLength * 2); uncompLength *= 2; } strm.next_out = reinterpret_cast((uncompressedBytes.data() + strm.total_out)); strm.avail_out = uncompLength - strm.total_out; // Inflate another chunk. err = inflate(&strm, Z_SYNC_FLUSH); if (err == Z_STREAM_END) done = true; else if (err != Z_OK) { break; } } if (inflateEnd(&strm) != Z_OK || !done) { return false; } uncompressedBytes.resize(strm.total_out); return true; } bool GZip::zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes) { if (uncompressedBytes.size() == 0) { compressedBytes = uncompressedBytes; return true; } unsigned compLength = qMin(uncompressedBytes.size(), 16); compressedBytes.clear(); compressedBytes.resize(compLength); z_stream zs; memset(&zs, 0, sizeof(zs)); if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (16 + MAX_WBITS), 8, Z_DEFAULT_STRATEGY) != Z_OK) { return false; } zs.next_in = (Bytef*)uncompressedBytes.data(); zs.avail_in = uncompressedBytes.size(); int ret; compressedBytes.resize(uncompressedBytes.size()); unsigned offset = 0; unsigned temp = 0; do { auto remaining = compressedBytes.size() - offset; if (remaining < 1) { compressedBytes.resize(compressedBytes.size() * 2); } zs.next_out = reinterpret_cast((compressedBytes.data() + offset)); temp = zs.avail_out = compressedBytes.size() - offset; ret = deflate(&zs, Z_FINISH); offset += temp - zs.avail_out; } while (ret == Z_OK); compressedBytes.resize(offset); if (deflateEnd(&zs) != Z_OK) { return false; } if (ret != Z_STREAM_END) { return false; } return true; } int inf(QFile* source, std::function handleBlock) { constexpr auto CHUNK = 16384; int ret; unsigned have; z_stream strm; memset(&strm, 0, sizeof(strm)); char in[CHUNK]; unsigned char out[CHUNK]; ret = inflateInit2(&strm, (16 + MAX_WBITS)); if (ret != Z_OK) return ret; /* decompress until deflate stream ends or end of file */ do { strm.avail_in = source->read(in, CHUNK); if (source->error()) { (void)inflateEnd(&strm); return Z_ERRNO; } if (strm.avail_in == 0) break; strm.next_in = reinterpret_cast(in); /* run inflate() on input until output buffer not full */ do { strm.avail_out = CHUNK; strm.next_out = out; ret = inflate(&strm, Z_NO_FLUSH); assert(ret != Z_STREAM_ERROR); /* state not clobbered */ switch (ret) { case Z_NEED_DICT: ret = Z_DATA_ERROR; [[fallthrough]]; case Z_DATA_ERROR: case Z_MEM_ERROR: (void)inflateEnd(&strm); return ret; } have = CHUNK - strm.avail_out; if (!handleBlock(QByteArray(reinterpret_cast(out), have))) { (void)inflateEnd(&strm); return Z_OK; } } while (strm.avail_out == 0); /* done when inflate() says it's done */ } while (ret != Z_STREAM_END); /* clean up and return */ (void)inflateEnd(&strm); return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR; } QString zerr(int ret) { switch (ret) { case Z_ERRNO: return QObject::tr("error handling file"); case Z_STREAM_ERROR: return QObject::tr("invalid compression level"); case Z_DATA_ERROR: return QObject::tr("invalid or incomplete deflate data"); case Z_MEM_ERROR: return QObject::tr("out of memory"); case Z_VERSION_ERROR: return QObject::tr("zlib version mismatch!"); } return {}; } QString GZip::readGzFileByBlocks(QFile* source, std::function handleBlock) { auto ret = inf(source, handleBlock); return zerr(ret); } PrismLauncher-11.0.3/launcher/CMakeLists.txt0000644000175100017510000014512515224505336020334 0ustar runnerrunnerproject(application) ################################ FILES ################################ ######## Sources and headers ######## set(CORE_SOURCES # LOGIC - Base classes and infrastructure BaseInstaller.h BaseInstaller.cpp BaseVersionList.h BaseVersionList.cpp InstanceList.h InstanceList.cpp InstanceTask.h InstanceTask.cpp LoggedProcess.h LoggedProcess.cpp MessageLevel.cpp MessageLevel.h BaseVersion.h BaseInstance.h BaseInstance.cpp InstanceDirUpdate.h InstanceDirUpdate.cpp NullInstance.h MMCZip.h MMCZip.cpp archive/ArchiveReader.cpp archive/ArchiveReader.h archive/ArchiveWriter.cpp archive/ArchiveWriter.h archive/ExportToZipTask.cpp archive/ExportToZipTask.h archive/ExtractZipTask.cpp archive/ExtractZipTask.h StringUtils.h StringUtils.cpp QVariantUtils.h RuntimeContext.h PSaveFile.h # Basic instance manipulation tasks (derived from InstanceTask) InstanceCreationTask.h InstanceCreationTask.cpp InstanceCopyPrefs.h InstanceCopyPrefs.cpp InstanceCopyTask.h InstanceCopyTask.cpp InstanceImportTask.h InstanceImportTask.cpp # Resource downloading task ResourceDownloadTask.h ResourceDownloadTask.cpp # Use tracking separate from memory management Usable.h # Prefix tree where node names are strings between separators SeparatorPrefixTree.h # String filters Filter.h # JSON parsing helpers Json.h Json.cpp FileSystem.h FileSystem.cpp Exception.h # RW lock protected map RWStorage.h # a smart pointer wrapper intended for safer use with Qt signal/slot mechanisms QObjectPtr.h # Compression support GZip.h GZip.cpp # Command line parameter parsing Commandline.h Commandline.cpp # Version number string support Version.h Version.cpp # A Recursive file system watcher RecursiveFileSystemWatcher.h RecursiveFileSystemWatcher.cpp # Time MMCTime.h MMCTime.cpp MTPixmapCache.h # Assertion helper AssertHelpers.h ) if (UNIX AND NOT CYGWIN AND NOT APPLE) set(CORE_SOURCES ${CORE_SOURCES} # LibraryUtils LibraryUtils.h LibraryUtils.cpp ) endif() set(NET_SOURCES # network stuffs net/ByteArraySink.h net/ChecksumValidator.h net/Download.cpp net/Download.h net/DummySink.h net/FileSink.cpp net/FileSink.h net/HttpMetaCache.cpp net/HttpMetaCache.h net/MetaCacheSink.cpp net/MetaCacheSink.h net/Logging.h net/Logging.cpp net/NetJob.cpp net/NetJob.h net/NetUtils.h net/PasteUpload.cpp net/PasteUpload.h net/Sink.h net/Validator.h net/Upload.cpp net/Upload.h net/HeaderProxy.h net/RawHeaderProxy.h net/ApiHeaderProxy.h net/ApiDownload.h net/ApiDownload.cpp net/ApiUpload.cpp net/ApiUpload.h net/NetRequest.cpp net/NetRequest.h ) # Game launch logic set(LAUNCH_SOURCES launch/steps/CheckJava.cpp launch/steps/CheckJava.h launch/steps/LookupServerAddress.cpp launch/steps/LookupServerAddress.h launch/steps/PostLaunchCommand.cpp launch/steps/PostLaunchCommand.h launch/steps/PreLaunchCommand.cpp launch/steps/PreLaunchCommand.h launch/steps/TextPrint.cpp launch/steps/TextPrint.h launch/steps/QuitAfterGameStop.cpp launch/steps/QuitAfterGameStop.h launch/steps/PrintServers.cpp launch/steps/PrintServers.h launch/LaunchStep.cpp launch/LaunchStep.h launch/LaunchTask.cpp launch/LaunchTask.h launch/LogModel.cpp launch/LogModel.h launch/TaskStepWrapper.cpp launch/TaskStepWrapper.h logs/LogParser.cpp logs/LogParser.h ) # Old update system set(UPDATE_SOURCES updater/ExternalUpdater.h ) set(MAC_UPDATE_SOURCES updater/MacSparkleUpdater.h updater/MacSparkleUpdater.mm ) set(PRISM_UPDATE_SOURCES updater/PrismExternalUpdater.h updater/PrismExternalUpdater.cpp ) # Backend for the news bar... there's usually no news. set(NEWS_SOURCES # News System news/NewsChecker.h news/NewsChecker.cpp news/NewsEntry.h news/NewsEntry.cpp ) # Icon interface set(ICONS_SOURCES # Icons System and related code icons/IconUtils.h icons/IconUtils.cpp ) # Support for Minecraft instances and launch set(MINECRAFT_SOURCES # Logging minecraft/Logging.h minecraft/Logging.cpp # Minecraft support minecraft/auth/AccountData.cpp minecraft/auth/AccountData.h minecraft/auth/AccountList.cpp minecraft/auth/AccountList.h minecraft/auth/AuthSession.cpp minecraft/auth/AuthSession.h minecraft/auth/AuthStep.h minecraft/auth/MinecraftAccount.cpp minecraft/auth/MinecraftAccount.h minecraft/auth/Parsers.cpp minecraft/auth/Parsers.h minecraft/auth/AuthFlow.cpp minecraft/auth/AuthFlow.h minecraft/auth/steps/EntitlementsStep.cpp minecraft/auth/steps/EntitlementsStep.h minecraft/auth/steps/GetSkinStep.cpp minecraft/auth/steps/GetSkinStep.h minecraft/auth/steps/LauncherLoginStep.cpp minecraft/auth/steps/LauncherLoginStep.h minecraft/auth/steps/MinecraftProfileStep.cpp minecraft/auth/steps/MinecraftProfileStep.h minecraft/auth/steps/MSADeviceCodeStep.cpp minecraft/auth/steps/MSADeviceCodeStep.h minecraft/auth/steps/MSAStep.cpp minecraft/auth/steps/MSAStep.h minecraft/auth/steps/XboxAuthorizationStep.cpp minecraft/auth/steps/XboxAuthorizationStep.h minecraft/auth/steps/XboxUserStep.cpp minecraft/auth/steps/XboxUserStep.h minecraft/update/AssetUpdateTask.h minecraft/update/AssetUpdateTask.cpp minecraft/update/LegacyFMLLibrariesTask.cpp minecraft/update/LegacyFMLLibrariesTask.h minecraft/update/FoldersTask.cpp minecraft/update/FoldersTask.h minecraft/update/LibrariesTask.cpp minecraft/update/LibrariesTask.h minecraft/launch/ClaimAccount.cpp minecraft/launch/ClaimAccount.h minecraft/launch/CreateGameFolders.cpp minecraft/launch/CreateGameFolders.h minecraft/launch/EnsureAvailableMemory.cpp minecraft/launch/EnsureAvailableMemory.h minecraft/launch/EnsureOfflineLibraries.cpp minecraft/launch/EnsureOfflineLibraries.h minecraft/launch/ModMinecraftJar.cpp minecraft/launch/ModMinecraftJar.h minecraft/launch/ExtractNatives.cpp minecraft/launch/ExtractNatives.h minecraft/launch/LauncherPartLaunch.cpp minecraft/launch/LauncherPartLaunch.h minecraft/launch/MinecraftTarget.cpp minecraft/launch/MinecraftTarget.h minecraft/launch/PrintInstanceInfo.cpp minecraft/launch/PrintInstanceInfo.h minecraft/launch/ReconstructAssets.cpp minecraft/launch/ReconstructAssets.h minecraft/launch/ScanModFolders.cpp minecraft/launch/ScanModFolders.h minecraft/launch/VerifyJavaInstall.cpp minecraft/launch/VerifyJavaInstall.h minecraft/launch/AutoInstallJava.cpp minecraft/launch/AutoInstallJava.h minecraft/GradleSpecifier.h minecraft/MinecraftInstance.cpp minecraft/MinecraftInstance.h minecraft/LaunchProfile.cpp minecraft/LaunchProfile.h minecraft/Component.cpp minecraft/Component.h minecraft/PackProfile.cpp minecraft/PackProfile.h minecraft/ComponentUpdateTask.cpp minecraft/ComponentUpdateTask.h minecraft/MinecraftLoadAndCheck.h minecraft/MinecraftLoadAndCheck.cpp minecraft/MojangVersionFormat.cpp minecraft/MojangVersionFormat.h minecraft/Rule.cpp minecraft/Rule.h minecraft/OneSixVersionFormat.cpp minecraft/OneSixVersionFormat.h minecraft/ParseUtils.cpp minecraft/ParseUtils.h minecraft/ProfileUtils.cpp minecraft/ProfileUtils.h minecraft/ShortcutUtils.cpp minecraft/ShortcutUtils.h minecraft/Library.cpp minecraft/Library.h minecraft/MojangDownloadInfo.h minecraft/VanillaInstanceCreationTask.cpp minecraft/VanillaInstanceCreationTask.h minecraft/VersionFile.cpp minecraft/VersionFile.h minecraft/VersionFilterData.h minecraft/VersionFilterData.cpp minecraft/World.h minecraft/World.cpp minecraft/WorldList.h minecraft/WorldList.cpp minecraft/mod/MetadataHandler.h minecraft/mod/Mod.h minecraft/mod/Mod.cpp minecraft/mod/ModDetails.h minecraft/mod/ModFolderModel.h minecraft/mod/ModFolderModel.cpp minecraft/mod/Resource.h minecraft/mod/Resource.cpp minecraft/mod/ResourceFolderModel.h minecraft/mod/ResourceFolderModel.cpp minecraft/mod/DataPack.h minecraft/mod/DataPack.cpp minecraft/mod/DataPackFolderModel.h minecraft/mod/DataPackFolderModel.cpp minecraft/mod/ResourcePack.h minecraft/mod/ResourcePack.cpp minecraft/mod/ResourcePackFolderModel.h minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/TexturePack.h minecraft/mod/TexturePack.cpp minecraft/mod/ShaderPack.h minecraft/mod/ShaderPack.cpp minecraft/mod/WorldSave.h minecraft/mod/WorldSave.cpp minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.cpp minecraft/mod/ShaderPackFolderModel.h minecraft/mod/ShaderPackFolderModel.cpp minecraft/mod/tasks/ResourceFolderLoadTask.h minecraft/mod/tasks/ResourceFolderLoadTask.cpp minecraft/mod/tasks/LocalModParseTask.h minecraft/mod/tasks/LocalModParseTask.cpp minecraft/mod/tasks/LocalResourceUpdateTask.h minecraft/mod/tasks/LocalResourceUpdateTask.cpp minecraft/mod/tasks/LocalDataPackParseTask.h minecraft/mod/tasks/LocalDataPackParseTask.cpp minecraft/mod/tasks/LocalTexturePackParseTask.h minecraft/mod/tasks/LocalTexturePackParseTask.cpp minecraft/mod/tasks/LocalShaderPackParseTask.h minecraft/mod/tasks/LocalShaderPackParseTask.cpp minecraft/mod/tasks/LocalWorldSaveParseTask.h minecraft/mod/tasks/LocalWorldSaveParseTask.cpp minecraft/mod/tasks/LocalResourceParse.h minecraft/mod/tasks/LocalResourceParse.cpp minecraft/mod/tasks/GetModDependenciesTask.h minecraft/mod/tasks/GetModDependenciesTask.cpp # Assets minecraft/AssetsUtils.h minecraft/AssetsUtils.cpp # Minecraft skins minecraft/skins/CapeChange.cpp minecraft/skins/CapeChange.h minecraft/skins/SkinUpload.cpp minecraft/skins/SkinUpload.h minecraft/skins/SkinDelete.cpp minecraft/skins/SkinDelete.h minecraft/skins/SkinModel.cpp minecraft/skins/SkinModel.h minecraft/skins/SkinList.cpp minecraft/skins/SkinList.h minecraft/Agent.h) # the screenshots feature set(SCREENSHOTS_SOURCES screenshots/Screenshot.h screenshots/ImgurUpload.h screenshots/ImgurUpload.cpp screenshots/ImgurAlbumCreation.h screenshots/ImgurAlbumCreation.cpp ) set(TASKS_SOURCES # Tasks tasks/Task.h tasks/Task.cpp tasks/ConcurrentTask.h tasks/ConcurrentTask.cpp tasks/SequentialTask.h tasks/SequentialTask.cpp tasks/MultipleOptionsTask.h tasks/MultipleOptionsTask.cpp ) set(SETTINGS_SOURCES # Settings settings/INIFile.cpp settings/INIFile.h settings/INISettingsObject.cpp settings/INISettingsObject.h settings/OverrideSetting.cpp settings/OverrideSetting.h settings/PassthroughSetting.cpp settings/PassthroughSetting.h settings/Setting.cpp settings/Setting.h settings/SettingsObject.cpp settings/SettingsObject.h ) set(JAVA_SOURCES java/JavaChecker.h java/JavaChecker.cpp java/JavaInstall.h java/JavaInstall.cpp java/JavaInstallList.h java/JavaInstallList.cpp java/JavaUtils.h java/JavaUtils.cpp java/JavaVersion.h java/JavaVersion.cpp java/JavaMetadata.h java/JavaMetadata.cpp java/download/ArchiveDownloadTask.cpp java/download/ArchiveDownloadTask.h java/download/ManifestDownloadTask.cpp java/download/ManifestDownloadTask.h java/download/SymlinkTask.cpp java/download/SymlinkTask.h ui/java/InstallJavaDialog.h ui/java/InstallJavaDialog.cpp ui/java/VersionList.h ui/java/VersionList.cpp ) set(TRANSLATIONS_SOURCES translations/TranslationsModel.h translations/TranslationsModel.cpp translations/POTranslator.h translations/POTranslator.cpp ) set(TOOLS_SOURCES # Tools tools/BaseExternalTool.cpp tools/BaseExternalTool.h tools/BaseProfiler.cpp tools/BaseProfiler.h tools/JProfiler.cpp tools/JProfiler.h tools/JVisualVM.cpp tools/JVisualVM.h tools/MCEditTool.cpp tools/MCEditTool.h tools/GenericProfiler.cpp tools/GenericProfiler.h ) set(META_SOURCES # Metadata sources meta/JsonFormat.cpp meta/JsonFormat.h meta/BaseEntity.cpp meta/BaseEntity.h meta/VersionList.cpp meta/VersionList.h meta/Version.cpp meta/Version.h meta/Index.cpp meta/Index.h ) set(API_SOURCES modplatform/ModIndex.h modplatform/ModIndex.cpp modplatform/ResourceType.h modplatform/ResourceType.cpp modplatform/ResourceAPI.h modplatform/ResourceAPI.cpp modplatform/EnsureMetadataTask.h modplatform/EnsureMetadataTask.cpp modplatform/CheckUpdateTask.h modplatform/flame/FlameAPI.h modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h modplatform/modrinth/ModrinthAPI.cpp modplatform/helpers/HashUtils.h modplatform/helpers/HashUtils.cpp modplatform/helpers/OverrideUtils.h modplatform/helpers/OverrideUtils.cpp modplatform/helpers/ExportToModList.h modplatform/helpers/ExportToModList.cpp ) set(FTB_SOURCES modplatform/legacy_ftb/PackFetchTask.h modplatform/legacy_ftb/PackFetchTask.cpp modplatform/legacy_ftb/PackInstallTask.h modplatform/legacy_ftb/PackInstallTask.cpp modplatform/legacy_ftb/PrivatePackManager.h modplatform/legacy_ftb/PrivatePackManager.cpp modplatform/legacy_ftb/PackHelpers.h modplatform/import_ftb/PackInstallTask.h modplatform/import_ftb/PackInstallTask.cpp modplatform/import_ftb/PackHelpers.h modplatform/import_ftb/PackHelpers.cpp modplatform/ftb/FTBPackInstallTask.h modplatform/ftb/FTBPackInstallTask.cpp modplatform/ftb/FTBPackManifest.h modplatform/ftb/FTBPackManifest.cpp ) set(FLAME_SOURCES # Flame modplatform/flame/FlameModIndex.cpp modplatform/flame/FlameModIndex.h modplatform/flame/PackManifest.h modplatform/flame/PackManifest.cpp modplatform/flame/FileResolvingTask.h modplatform/flame/FileResolvingTask.cpp modplatform/flame/FlameCheckUpdate.cpp modplatform/flame/FlameCheckUpdate.h modplatform/flame/FlameInstanceCreationTask.h modplatform/flame/FlameInstanceCreationTask.cpp modplatform/flame/FlamePackExportTask.h modplatform/flame/FlamePackExportTask.cpp ) set(MODRINTH_SOURCES modplatform/modrinth/ModrinthPackIndex.cpp modplatform/modrinth/ModrinthPackIndex.h modplatform/modrinth/ModrinthCheckUpdate.cpp modplatform/modrinth/ModrinthCheckUpdate.h modplatform/modrinth/ModrinthInstanceCreationTask.cpp modplatform/modrinth/ModrinthInstanceCreationTask.h modplatform/modrinth/ModrinthPackExportTask.cpp modplatform/modrinth/ModrinthPackExportTask.h ) set(PACKWIZ_SOURCES modplatform/packwiz/Packwiz.h modplatform/packwiz/Packwiz.cpp ) set(TECHNIC_SOURCES modplatform/technic/SingleZipPackInstallTask.h modplatform/technic/SingleZipPackInstallTask.cpp modplatform/technic/SolderPackInstallTask.h modplatform/technic/SolderPackInstallTask.cpp modplatform/technic/SolderPackManifest.h modplatform/technic/SolderPackManifest.cpp modplatform/technic/TechnicPackProcessor.h modplatform/technic/TechnicPackProcessor.cpp ) set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLPackIndex.cpp modplatform/atlauncher/ATLPackIndex.h modplatform/atlauncher/ATLPackInstallTask.cpp modplatform/atlauncher/ATLPackInstallTask.h modplatform/atlauncher/ATLPackManifest.cpp modplatform/atlauncher/ATLPackManifest.h modplatform/atlauncher/ATLShareCode.cpp modplatform/atlauncher/ATLShareCode.h ) set(LINKEXE_SOURCES console/WindowsConsole.h console/WindowsConsole.cpp filelink/FileLink.h filelink/FileLink.cpp FileSystem.h FileSystem.cpp Exception.h StringUtils.h StringUtils.cpp DesktopServices.h DesktopServices.cpp ) set(PRISMUPDATER_SOURCES updater/prismupdater/PrismUpdater.h updater/prismupdater/PrismUpdater.cpp updater/prismupdater/UpdaterDialogs.h updater/prismupdater/UpdaterDialogs.cpp updater/prismupdater/GitHubRelease.h updater/prismupdater/GitHubRelease.cpp Json.h Json.cpp FileSystem.h FileSystem.cpp StringUtils.h StringUtils.cpp DesktopServices.h DesktopServices.cpp Version.h Version.cpp Markdown.h Markdown.cpp # Zip MMCZip.h MMCZip.cpp archive/ArchiveReader.cpp archive/ArchiveReader.h archive/ArchiveWriter.cpp archive/ArchiveWriter.h # Time MMCTime.h MMCTime.cpp net/ByteArraySink.h net/ChecksumValidator.h net/Download.cpp net/Download.h net/FileSink.cpp net/FileSink.h net/HttpMetaCache.cpp net/HttpMetaCache.h net/Logging.h net/Logging.cpp net/NetRequest.cpp net/NetRequest.h net/NetJob.cpp net/NetJob.h net/NetUtils.h net/Sink.h net/Validator.h net/HeaderProxy.h net/RawHeaderProxy.h ui/dialogs/ProgressDialog.cpp ui/dialogs/ProgressDialog.h ui/widgets/SubTaskProgressBar.h ui/widgets/SubTaskProgressBar.cpp ) if(WIN32) set(PRISMUPDATER_SOURCES console/WindowsConsole.h console/WindowsConsole.cpp ${PRISMUPDATER_SOURCES} ) endif() ######## Logging categories ######## ecm_qt_declare_logging_category(CORE_SOURCES HEADER Logging.h IDENTIFIER authCredentials CATEGORY_NAME "launcher.auth.credentials" DEFAULT_SEVERITY Warning DESCRIPTION "Secrets and credentials for debugging purposes" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER instanceProfileC CATEGORY_NAME "launcher.instance.profile" DEFAULT_SEVERITY Debug DESCRIPTION "Profile actions" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER instanceProfileResolveC CATEGORY_NAME "launcher.instance.profile.resolve" DEFAULT_SEVERITY Debug DESCRIPTION "Profile component resolusion actions" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER taskLogC CATEGORY_NAME "launcher.task" DEFAULT_SEVERITY Debug DESCRIPTION "Task actions" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER taskNetLogC CATEGORY_NAME "launcher.task.net" DEFAULT_SEVERITY Debug DESCRIPTION "Task network action" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER taskDownloadLogC CATEGORY_NAME "launcher.task.net.download" DEFAULT_SEVERITY Debug DESCRIPTION "Task network download actions" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER taskUploadLogC CATEGORY_NAME "launcher.task.net.upload" DEFAULT_SEVERITY Debug DESCRIPTION "Task network upload actions" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER taskMetaCacheLogC CATEGORY_NAME "launcher.task.net.metacache" DEFAULT_SEVERITY Debug DESCRIPTION "task network meta-cache actions" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER taskHttpMetaCacheLogC CATEGORY_NAME "launcher.task.net.metacache.http" DEFAULT_SEVERITY Debug DESCRIPTION "task network http meta-cache actions" EXPORT "${Launcher_Name}" ) if(KDE_INSTALL_LOGGINGCATEGORIESDIR) # only install if there is a standard path for this ecm_qt_install_logging_categories( EXPORT "${Launcher_Name}" DESTINATION "${KDE_INSTALL_LOGGINGCATEGORIESDIR}" ) endif() ################################ COMPILE ################################ set(LOGIC_SOURCES ${CORE_SOURCES} ${NET_SOURCES} ${LAUNCH_SOURCES} ${UPDATE_SOURCES} ${NEWS_SOURCES} ${MINECRAFT_SOURCES} ${SCREENSHOTS_SOURCES} ${TASKS_SOURCES} ${SETTINGS_SOURCES} ${JAVA_SOURCES} ${TRANSLATIONS_SOURCES} ${TOOLS_SOURCES} ${META_SOURCES} ${ICONS_SOURCES} ${API_SOURCES} ${FTB_SOURCES} ${FLAME_SOURCES} ${MODRINTH_SOURCES} ${PACKWIZ_SOURCES} ${TECHNIC_SOURCES} ${ATLAUNCHER_SOURCES} ) if(APPLE AND Launcher_ENABLE_UPDATER) set (LOGIC_SOURCES ${LOGIC_SOURCES} ${MAC_UPDATE_SOURCES}) else() set (LOGIC_SOURCES ${LOGIC_SOURCES} ${PRISM_UPDATE_SOURCES}) endif() SET(LAUNCHER_SOURCES # Application base Application.h Application.cpp DataMigrationTask.h DataMigrationTask.cpp ApplicationMessage.h ApplicationMessage.cpp SysInfo.h SysInfo.cpp HardwareInfo.cpp HardwareInfo.h # console utils console/Console.h # GUI - general utilities DesktopServices.h DesktopServices.cpp VersionProxyModel.h VersionProxyModel.cpp Markdown.h Markdown.cpp # Super secret! KonamiCode.h KonamiCode.cpp # Icons icons/MMCIcon.h icons/MMCIcon.cpp icons/IconList.h icons/IconList.cpp # log utils logs/AnonymizeLog.cpp logs/AnonymizeLog.h # GUI - windows ui/GuiUtil.h ui/GuiUtil.cpp ui/MainWindow.h ui/MainWindow.cpp ui/InstanceWindow.h ui/InstanceWindow.cpp ui/ViewLogWindow.h ui/ViewLogWindow.cpp ui/ToolTipFilter.h ui/ToolTipFilter.cpp # FIXME: maybe find a better home for this. FileIgnoreProxy.cpp FileIgnoreProxy.h FastFileIconProvider.cpp FastFileIconProvider.h # GUI - setup wizard ui/setupwizard/SetupWizard.h ui/setupwizard/SetupWizard.cpp ui/setupwizard/BaseWizardPage.h ui/setupwizard/JavaWizardPage.cpp ui/setupwizard/JavaWizardPage.h ui/setupwizard/LanguageWizardPage.cpp ui/setupwizard/LanguageWizardPage.h ui/setupwizard/PasteWizardPage.cpp ui/setupwizard/PasteWizardPage.h ui/setupwizard/ThemeWizardPage.h ui/setupwizard/AutoJavaWizardPage.cpp ui/setupwizard/AutoJavaWizardPage.h ui/setupwizard/LoginWizardPage.cpp ui/setupwizard/LoginWizardPage.h # GUI - themes ui/themes/FusionTheme.cpp ui/themes/FusionTheme.h ui/themes/BrightTheme.cpp ui/themes/BrightTheme.h ui/themes/CustomTheme.cpp ui/themes/CustomTheme.h ui/themes/DarkTheme.cpp ui/themes/DarkTheme.h ui/themes/ITheme.cpp ui/themes/ITheme.h ui/themes/HintOverrideProxyStyle.cpp ui/themes/HintOverrideProxyStyle.h ui/themes/SystemTheme.cpp ui/themes/SystemTheme.h ui/themes/IconTheme.cpp ui/themes/IconTheme.h ui/themes/ThemeManager.cpp ui/themes/ThemeManager.h ui/themes/CatPack.cpp ui/themes/CatPack.h ui/themes/CatPainter.cpp ui/themes/CatPainter.h # Processes LaunchMode.h LaunchController.h LaunchController.cpp # page provider for instances InstancePageProvider.h # Common java checking UI JavaCommon.h JavaCommon.cpp # GUI - paged dialog base ui/pages/BasePage.h ui/pages/BasePageContainer.h ui/pages/BasePageProvider.h # GUI - instance pages ui/pages/instance/ExternalResourcesPage.cpp ui/pages/instance/ExternalResourcesPage.h ui/pages/instance/VersionPage.cpp ui/pages/instance/VersionPage.h ui/pages/instance/ManagedPackPage.cpp ui/pages/instance/ManagedPackPage.h ui/pages/instance/DataPackPage.h ui/pages/instance/DataPackPage.cpp ui/pages/instance/TexturePackPage.h ui/pages/instance/TexturePackPage.cpp ui/pages/instance/ResourcePackPage.h ui/pages/instance/ResourcePackPage.cpp ui/pages/instance/ShaderPackPage.h ui/pages/instance/ShaderPackPage.cpp ui/pages/instance/ModFolderPage.cpp ui/pages/instance/ModFolderPage.h ui/pages/instance/NotesPage.cpp ui/pages/instance/NotesPage.h ui/pages/instance/LogPage.cpp ui/pages/instance/LogPage.h ui/pages/instance/InstanceSettingsPage.h ui/pages/instance/ScreenshotsPage.cpp ui/pages/instance/ScreenshotsPage.h ui/pages/instance/OtherLogsPage.cpp ui/pages/instance/OtherLogsPage.h ui/pages/instance/ServersPage.cpp ui/pages/instance/ServersPage.h ui/pages/instance/WorldListPage.cpp ui/pages/instance/WorldListPage.h ui/pages/instance/McClient.cpp ui/pages/instance/McClient.h ui/pages/instance/McResolver.cpp ui/pages/instance/McResolver.h ui/pages/instance/ServerPingTask.cpp ui/pages/instance/ServerPingTask.h # GUI - global settings pages ui/pages/global/AccountListPage.cpp ui/pages/global/AccountListPage.h ui/pages/global/ExternalToolsPage.cpp ui/pages/global/ExternalToolsPage.h ui/pages/global/JavaPage.cpp ui/pages/global/JavaPage.h ui/pages/global/LanguagePage.cpp ui/pages/global/LanguagePage.h ui/pages/global/MinecraftPage.h ui/pages/global/LauncherPage.cpp ui/pages/global/LauncherPage.h ui/pages/global/AppearancePage.h ui/pages/global/ProxyPage.cpp ui/pages/global/ProxyPage.h ui/pages/global/APIPage.cpp ui/pages/global/APIPage.h # GUI - platform pages ui/pages/modplatform/CustomPage.cpp ui/pages/modplatform/CustomPage.h ui/pages/modplatform/ResourcePage.cpp ui/pages/modplatform/ResourcePage.h ui/pages/modplatform/ResourceModel.cpp ui/pages/modplatform/ResourceModel.h ui/pages/modplatform/ModPage.cpp ui/pages/modplatform/ModPage.h ui/pages/modplatform/ModModel.cpp ui/pages/modplatform/ModModel.h ui/pages/modplatform/ResourcePackPage.cpp ui/pages/modplatform/ResourcePackModel.cpp # Needed for MOC to find them without a corresponding .cpp ui/pages/modplatform/TexturePackPage.h ui/pages/modplatform/TexturePackModel.cpp ui/pages/modplatform/ShaderPackPage.cpp ui/pages/modplatform/ShaderPackModel.cpp ui/pages/modplatform/DataPackPage.cpp ui/pages/modplatform/DataPackModel.cpp ui/pages/modplatform/ModpackProviderBasePage.h ui/pages/modplatform/atlauncher/AtlFilterModel.cpp ui/pages/modplatform/atlauncher/AtlFilterModel.h ui/pages/modplatform/atlauncher/AtlListModel.cpp ui/pages/modplatform/atlauncher/AtlListModel.h ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h ui/pages/modplatform/atlauncher/AtlPage.cpp ui/pages/modplatform/atlauncher/AtlPage.h ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h ui/pages/modplatform/ftb/FtbFilterModel.cpp ui/pages/modplatform/ftb/FtbFilterModel.h ui/pages/modplatform/ftb/FtbListModel.cpp ui/pages/modplatform/ftb/FtbListModel.h ui/pages/modplatform/ftb/FtbPage.cpp ui/pages/modplatform/ftb/FtbPage.h ui/pages/modplatform/legacy_ftb/Page.cpp ui/pages/modplatform/legacy_ftb/Page.h ui/pages/modplatform/legacy_ftb/ListModel.h ui/pages/modplatform/legacy_ftb/ListModel.cpp ui/pages/modplatform/import_ftb/ImportFTBPage.cpp ui/pages/modplatform/import_ftb/ImportFTBPage.h ui/pages/modplatform/import_ftb/ListModel.h ui/pages/modplatform/import_ftb/ListModel.cpp ui/pages/modplatform/flame/FlameModel.cpp ui/pages/modplatform/flame/FlameModel.h ui/pages/modplatform/flame/FlamePage.cpp ui/pages/modplatform/flame/FlamePage.h ui/pages/modplatform/flame/FlameResourceModels.cpp ui/pages/modplatform/flame/FlameResourceModels.h ui/pages/modplatform/flame/FlameResourcePages.cpp ui/pages/modplatform/flame/FlameResourcePages.h ui/pages/modplatform/modrinth/ModrinthPage.cpp ui/pages/modplatform/modrinth/ModrinthPage.h ui/pages/modplatform/modrinth/ModrinthModel.cpp ui/pages/modplatform/modrinth/ModrinthModel.h ui/pages/modplatform/technic/TechnicModel.cpp ui/pages/modplatform/technic/TechnicModel.h ui/pages/modplatform/technic/TechnicPage.cpp ui/pages/modplatform/technic/TechnicPage.h ui/pages/modplatform/ImportPage.cpp ui/pages/modplatform/ImportPage.h ui/pages/modplatform/OptionalModDialog.cpp ui/pages/modplatform/OptionalModDialog.h ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp ui/pages/modplatform/modrinth/ModrinthResourcePages.h # GUI - dialogs ui/dialogs/AboutDialog.cpp ui/dialogs/AboutDialog.h ui/dialogs/ProfileSelectDialog.cpp ui/dialogs/ProfileSelectDialog.h ui/dialogs/ProfileSetupDialog.cpp ui/dialogs/ProfileSetupDialog.h ui/dialogs/CopyInstanceDialog.cpp ui/dialogs/CopyInstanceDialog.h ui/dialogs/CreateShortcutDialog.cpp ui/dialogs/CreateShortcutDialog.h ui/dialogs/CustomMessageBox.cpp ui/dialogs/CustomMessageBox.h ui/dialogs/ExportInstanceDialog.cpp ui/dialogs/ExportInstanceDialog.h ui/dialogs/ExportPackDialog.cpp ui/dialogs/ExportPackDialog.h ui/dialogs/ExportToModListDialog.cpp ui/dialogs/ExportToModListDialog.h ui/dialogs/IconPickerDialog.cpp ui/dialogs/IconPickerDialog.h ui/dialogs/ImportResourceDialog.cpp ui/dialogs/ImportResourceDialog.h ui/dialogs/MSALoginDialog.cpp ui/dialogs/MSALoginDialog.h ui/dialogs/NetworkJobFailedDialog.cpp ui/dialogs/NetworkJobFailedDialog.h ui/dialogs/NewComponentDialog.cpp ui/dialogs/NewComponentDialog.h ui/dialogs/NewInstanceDialog.cpp ui/dialogs/NewInstanceDialog.h ui/dialogs/NewsDialog.cpp ui/dialogs/NewsDialog.h ui/pagedialog/PageDialog.cpp ui/pagedialog/PageDialog.h ui/dialogs/ProgressDialog.cpp ui/dialogs/ProgressDialog.h ui/dialogs/ReviewMessageBox.cpp ui/dialogs/ReviewMessageBox.h ui/dialogs/VersionSelectDialog.cpp ui/dialogs/VersionSelectDialog.h ui/dialogs/ResourceDownloadDialog.cpp ui/dialogs/ResourceDownloadDialog.h ui/dialogs/ScrollMessageBox.cpp ui/dialogs/ScrollMessageBox.h ui/dialogs/BlockedModsDialog.cpp ui/dialogs/BlockedModsDialog.h ui/dialogs/ChooseProviderDialog.h ui/dialogs/ChooseProviderDialog.cpp ui/dialogs/ResourceUpdateDialog.cpp ui/dialogs/ResourceUpdateDialog.h ui/dialogs/InstallLoaderDialog.cpp ui/dialogs/InstallLoaderDialog.h ui/dialogs/ChooseOfflineNameDialog.cpp ui/dialogs/ChooseOfflineNameDialog.h ui/dialogs/skins/SkinManageDialog.cpp ui/dialogs/skins/SkinManageDialog.h ui/dialogs/skins/draw/SkinOpenGLWindow.h ui/dialogs/skins/draw/SkinOpenGLWindow.cpp ui/dialogs/skins/draw/Scene.h ui/dialogs/skins/draw/Scene.cpp ui/dialogs/skins/draw/BoxGeometry.h ui/dialogs/skins/draw/BoxGeometry.cpp # GUI - widgets ui/widgets/CheckComboBox.cpp ui/widgets/CheckComboBox.h ui/widgets/Common.cpp ui/widgets/Common.h ui/widgets/CustomCommands.cpp ui/widgets/CustomCommands.h ui/widgets/EnvironmentVariables.cpp ui/widgets/EnvironmentVariables.h ui/widgets/IconLabel.cpp ui/widgets/IconLabel.h ui/widgets/JavaWizardWidget.cpp ui/widgets/JavaWizardWidget.h ui/widgets/LabeledToolButton.cpp ui/widgets/LabeledToolButton.h ui/widgets/LanguageSelectionWidget.cpp ui/widgets/LanguageSelectionWidget.h ui/widgets/LogView.cpp ui/widgets/LogView.h ui/widgets/InfoFrame.cpp ui/widgets/InfoFrame.h ui/widgets/ModFilterWidget.cpp ui/widgets/ModFilterWidget.h ui/widgets/ModListView.cpp ui/widgets/ModListView.h ui/widgets/PageContainer.cpp ui/widgets/PageContainer.h ui/widgets/PageContainer_p.h ui/widgets/ProjectDescriptionPage.h ui/widgets/ProjectDescriptionPage.cpp ui/widgets/VariableSizedImageObject.h ui/widgets/VariableSizedImageObject.cpp ui/widgets/ProjectItem.h ui/widgets/ProjectItem.cpp ui/widgets/SubTaskProgressBar.h ui/widgets/SubTaskProgressBar.cpp ui/widgets/VersionListView.cpp ui/widgets/VersionListView.h ui/widgets/VersionSelectWidget.cpp ui/widgets/VersionSelectWidget.h ui/widgets/ProgressWidget.h ui/widgets/ProgressWidget.cpp ui/widgets/WideBar.h ui/widgets/WideBar.cpp ui/widgets/AppearanceWidget.h ui/widgets/AppearanceWidget.cpp ui/widgets/MinecraftSettingsWidget.h ui/widgets/MinecraftSettingsWidget.cpp ui/widgets/JavaSettingsWidget.h ui/widgets/JavaSettingsWidget.cpp # GUI - instance group view ui/instanceview/InstanceProxyModel.cpp ui/instanceview/InstanceProxyModel.h ui/instanceview/AccessibleInstanceView.cpp ui/instanceview/AccessibleInstanceView.h ui/instanceview/AccessibleInstanceView_p.h ui/instanceview/InstanceView.cpp ui/instanceview/InstanceView.h ui/instanceview/InstanceDelegate.cpp ui/instanceview/InstanceDelegate.h ui/instanceview/VisualGroup.cpp ui/instanceview/VisualGroup.h ) if (APPLE) set(LAUNCHER_SOURCES ${LAUNCHER_SOURCES} ui/themes/ThemeManager.mm ) endif() if (NOT Apple) set(LAUNCHER_SOURCES ${LAUNCHER_SOURCES} ui/dialogs/UpdateAvailableDialog.h ui/dialogs/UpdateAvailableDialog.cpp ) endif() if (APPLE) set(LAUNCHER_SOURCES ${LAUNCHER_SOURCES} macsandbox/SecurityBookmarkFileAccess.h macsandbox/SecurityBookmarkFileAccess.mm ) endif() if(WIN32) set(LAUNCHER_SOURCES console/WindowsConsole.h console/WindowsConsole.cpp ${LAUNCHER_SOURCES} ) endif() qt_wrap_ui(LAUNCHER_UI ui/MainWindow.ui ui/setupwizard/PasteWizardPage.ui ui/setupwizard/AutoJavaWizardPage.ui ui/setupwizard/LoginWizardPage.ui ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui ui/pages/global/APIPage.ui ui/pages/global/ProxyPage.ui ui/pages/global/ExternalToolsPage.ui ui/pages/instance/ExternalResourcesPage.ui ui/pages/instance/NotesPage.ui ui/pages/instance/LogPage.ui ui/pages/instance/ServersPage.ui ui/pages/instance/OtherLogsPage.ui ui/pages/instance/VersionPage.ui ui/pages/instance/ManagedPackPage.ui ui/pages/instance/WorldListPage.ui ui/pages/instance/ScreenshotsPage.ui ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui ui/pages/modplatform/atlauncher/AtlPage.ui ui/pages/modplatform/CustomPage.ui ui/pages/modplatform/ResourcePage.ui ui/pages/modplatform/flame/FlamePage.ui ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/import_ftb/ImportFTBPage.ui ui/pages/modplatform/ImportPage.ui ui/pages/modplatform/OptionalModDialog.ui ui/pages/modplatform/ftb/FtbPage.ui ui/pages/modplatform/modrinth/ModrinthPage.ui ui/pages/modplatform/technic/TechnicPage.ui ui/widgets/CustomCommands.ui ui/widgets/EnvironmentVariables.ui ui/widgets/InfoFrame.ui ui/widgets/ModFilterWidget.ui ui/widgets/SubTaskProgressBar.ui ui/widgets/AppearanceWidget.ui ui/widgets/MinecraftSettingsWidget.ui ui/widgets/JavaSettingsWidget.ui ui/dialogs/CopyInstanceDialog.ui ui/dialogs/CreateShortcutDialog.ui ui/dialogs/ProfileSetupDialog.ui ui/dialogs/ProgressDialog.ui ui/dialogs/NewInstanceDialog.ui ui/dialogs/NetworkJobFailedDialog.ui ui/dialogs/NewComponentDialog.ui ui/dialogs/NewsDialog.ui ui/dialogs/ProfileSelectDialog.ui ui/dialogs/ExportInstanceDialog.ui ui/dialogs/ExportPackDialog.ui ui/dialogs/ExportToModListDialog.ui ui/dialogs/IconPickerDialog.ui ui/dialogs/ImportResourceDialog.ui ui/dialogs/MSALoginDialog.ui ui/dialogs/AboutDialog.ui ui/dialogs/ReviewMessageBox.ui ui/dialogs/ScrollMessageBox.ui ui/dialogs/BlockedModsDialog.ui ui/dialogs/ChooseProviderDialog.ui ui/dialogs/skins/SkinManageDialog.ui ui/dialogs/ChooseOfflineNameDialog.ui ) qt_wrap_ui(PRISM_UPDATE_UI ui/dialogs/UpdateAvailableDialog.ui ) if (NOT Apple) set (LAUNCHER_UI ${LAUNCHER_UI} ${PRISM_UPDATE_UI}) endif() qt_add_resources(LAUNCHER_RESOURCES resources/backgrounds/backgrounds.qrc resources/multimc/multimc.qrc resources/pe_dark/pe_dark.qrc resources/pe_light/pe_light.qrc resources/pe_colored/pe_colored.qrc resources/pe_blue/pe_blue.qrc resources/breeze_dark/breeze_dark.qrc resources/breeze_light/breeze_light.qrc resources/OSX/OSX.qrc resources/iOS/iOS.qrc resources/flat/flat.qrc resources/flat_white/flat_white.qrc resources/documents/documents.qrc resources/shaders/shaders.qrc "${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_LogoQRC}" ) qt_wrap_ui(PRISMUPDATER_UI updater/prismupdater/SelectReleaseDialog.ui ui/widgets/SubTaskProgressBar.ui ui/dialogs/ProgressDialog.ui ) ######## Windows resource files ######## if(WIN32) set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC}) endif() ######## Precompiled Headers ########### if(${Launcher_USE_PCH}) message(STATUS "Using precompiled headers for applicable launcher targets") set(PRECOMPILED_HEADERS include/base.pch.hpp include/qtcore.pch.hpp include/qtgui.pch.hpp ) endif() ####### Targets ######## # Add executable add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) if(${Launcher_USE_PCH}) target_precompile_headers(Launcher_logic PRIVATE ${PRECOMPILED_HEADERS}) endif() target_link_libraries(Launcher_logic Launcher_murmur2 nbt++ ${ZLIB_LIBRARIES} qdcss BuildConfig Qt${QT_VERSION_MAJOR}::Widgets ) if(TARGET PkgConfig::libqrencode) target_link_libraries(Launcher_logic PkgConfig::libqrencode) else() target_include_directories(Launcher_logic PRIVATE ${LIBQRENCODE_INCLUDE_DIR}) target_link_libraries(Launcher_logic ${LIBQRENCODE_LIBRARIES}) endif() if(TARGET PkgConfig::tomlplusplus) target_link_libraries(Launcher_logic PkgConfig::tomlplusplus) else() target_link_libraries(Launcher_logic tomlplusplus::tomlplusplus) endif() if(TARGET PkgConfig::libarchive) target_link_libraries(Launcher_logic PkgConfig::libarchive) else() target_link_libraries(Launcher_logic LibArchive::LibArchive) endif() if (CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_libraries(Launcher_logic gamemode ) endif() target_link_libraries(Launcher_logic Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Xml Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Concurrent Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::NetworkAuth Qt${QT_VERSION_MAJOR}::OpenGL ${Launcher_QT_DBUS} ${Launcher_QT_LIBS} ) target_link_libraries(Launcher_logic cmark::cmark LocalPeer Launcher_rainbow ) if (TARGET ${Launcher_QT_DBUS}) add_compile_definitions(WITH_QTDBUS) endif() if(APPLE) set(CMAKE_MACOSX_RPATH 1) set(CMAKE_INSTALL_RPATH "@loader_path/../Frameworks/") if(Launcher_ENABLE_UPDATER) file(DOWNLOAD ${MACOSX_SPARKLE_DOWNLOAD_URL} ${CMAKE_BINARY_DIR}/Sparkle.tar.xz EXPECTED_HASH SHA256=${MACOSX_SPARKLE_SHA256}) file(ARCHIVE_EXTRACT INPUT ${CMAKE_BINARY_DIR}/Sparkle.tar.xz DESTINATION ${CMAKE_BINARY_DIR}/frameworks/Sparkle) find_library(SPARKLE_FRAMEWORK Sparkle "${CMAKE_BINARY_DIR}/frameworks/Sparkle") add_compile_definitions(SPARKLE_ENABLED) endif() target_link_libraries(Launcher_logic "-framework AppKit" "-framework Carbon" "-framework Foundation" "-framework ApplicationServices" ) if(Launcher_ENABLE_UPDATER) target_link_libraries(Launcher_logic ${SPARKLE_FRAMEWORK}) endif() endif() add_executable(${Launcher_Name} MACOSX_BUNDLE WIN32 main.cpp ${LAUNCHER_RCS}) if(${Launcher_USE_PCH}) target_precompile_headers(${Launcher_Name} REUSE_FROM Launcher_logic) endif() target_link_libraries(${Launcher_Name} Launcher_logic) if(DEFINED Launcher_APP_BINARY_NAME) set_target_properties(${Launcher_Name} PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}") endif() if(DEFINED Launcher_BINARY_RPATH) SET_TARGET_PROPERTIES(${Launcher_Name} PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}") endif() if(DEFINED Launcher_APP_BINARY_DEFS) target_compile_definitions(${Launcher_Name} PRIVATE ${Launcher_APP_BINARY_DEFS}) target_compile_definitions(Launcher_logic PRIVATE ${Launcher_APP_BINARY_DEFS}) endif() install(TARGETS ${Launcher_Name} RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET BUNDLE DESTINATION "." COMPONENT Runtime LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) # Deploy PDBs if(CMAKE_CXX_LINKER_SUPPORTS_PDB) install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) endif() if(Launcher_BUILD_UPDATER) # Updater add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI}) target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) if(${Launcher_USE_PCH}) target_precompile_headers(prism_updater_logic PRIVATE ${PRECOMPILED_HEADERS}) endif() target_link_libraries(prism_updater_logic ${ZLIB_LIBRARIES} BuildConfig Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network ${Launcher_QT_LIBS} cmark::cmark ) if(TARGET PkgConfig::libarchive) target_link_libraries(prism_updater_logic PkgConfig::libarchive) else() target_link_libraries(prism_updater_logic LibArchive::LibArchive) endif() add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp) target_sources("${Launcher_Name}_updater" PRIVATE updater/prismupdater/updater.exe.manifest) target_link_libraries("${Launcher_Name}_updater" prism_updater_logic) if(${Launcher_USE_PCH}) target_precompile_headers("${Launcher_Name}_updater" REUSE_FROM prism_updater_logic) endif() if(DEFINED Launcher_APP_BINARY_NAME) set_target_properties("${Launcher_Name}_updater" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_updater") endif() if(DEFINED Launcher_BINARY_RPATH) SET_TARGET_PROPERTIES("${Launcher_Name}_updater" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}") endif() install(TARGETS "${Launcher_Name}_updater" BUNDLE DESTINATION "." COMPONENT Runtime LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) # Deploy PDBs if(CMAKE_CXX_LINKER_SUPPORTS_PDB) install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) endif() endif() if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) # File link add_library(filelink_logic STATIC ${LINKEXE_SOURCES}) target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) if(${Launcher_USE_PCH}) target_precompile_headers(filelink_logic PRIVATE ${PRECOMPILED_HEADERS}) endif() target_link_libraries(filelink_logic BuildConfig Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network # Qt${QT_VERSION_MAJOR}::Concurrent ${Launcher_QT_LIBS} ) add_executable("${Launcher_Name}_filelink" WIN32 filelink/filelink_main.cpp) target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest) if(${Launcher_USE_PCH}) target_precompile_headers("${Launcher_Name}_filelink" REUSE_FROM filelink_logic) endif() # HACK: Fix manifest issues with Ninja in release mode (and only release mode) and MSVC # I have no idea why this works or why it's needed. UPDATE THIS IF YOU EDIT THE MANIFEST!!! -@getchoo # Thank you 2018 CMake mailing list thread https://cmake.cmake.narkive.com/LnotZXus/conflicting-msvc-manifests if(MSVC) set_property(TARGET "${Launcher_Name}_filelink" PROPERTY LINK_FLAGS "/MANIFESTUAC:level='requireAdministrator'") endif() target_link_libraries("${Launcher_Name}_filelink" filelink_logic) if(DEFINED Launcher_APP_BINARY_NAME) set_target_properties("${Launcher_Name}_filelink" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_filelink") endif() if(DEFINED Launcher_BINARY_RPATH) SET_TARGET_PROPERTIES("${Launcher_Name}_filelink" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}") endif() install(TARGETS "${Launcher_Name}_filelink" BUNDLE DESTINATION "." COMPONENT Runtime LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) # Deploy PDBs if(CMAKE_CXX_LINKER_SUPPORTS_PDB) install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) endif() endif() if (UNIX AND APPLE AND Launcher_ENABLE_UPDATER) # Add Sparkle updater # It has to be copied here instead of just allowing fixup_bundle to install it, otherwise essential parts of # the framework aren't installed install(DIRECTORY ${MACOSX_SPARKLE_DIR}/Sparkle.framework DESTINATION ${FRAMEWORK_DEST_DIR} USE_SOURCE_PERMISSIONS) endif() # Set basic compiler warning/error flags for all targets get_property(Launcher_TARGETS DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY BUILDSYSTEM_TARGETS) foreach(target ${Launcher_TARGETS}) message(STATUS "Enabling all warnings as errors for target '${target}'") if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") target_compile_options(${target} PRIVATE /W4 /WX /permissive-) else() target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic -Werror) endif() endforeach() # Disable some warnings in main launcher target due to being present in a lot of places. TODO: Fix them. if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") target_compile_options(Launcher_logic PRIVATE /wd4100) # C4100 - unused parameter target_compile_options(${Launcher_Name} PRIVATE /wd4100) # C4100 - unused parameter else() # sfinae-incomplete is a new GCC warning and triggers in Qt headers # no-unknown-warning-option so that compilers that don't have sfinae-incomplete don't error target_compile_options(Launcher_logic PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers -Wno-unknown-warning-option -Wno-sfinae-incomplete) target_compile_options(${Launcher_Name} PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers -Wno-unknown-warning-option -Wno-sfinae-incomplete) endif() #### The bundle mess! #### # Bundle utilities are used to complete packages for different platforms - they add all the libraries that would otherwise be missing on the target system. # NOTE: it seems that this absolutely has to be here, and nowhere else. if(WIN32 OR (UNIX AND APPLE)) if(WIN32) set(QT_DEPLOY_TOOL_OPTIONS "--no-opengl-sw --no-quick-import --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types generic,networkinformation") endif() qt_generate_deploy_script( TARGET ${Launcher_Name} OUTPUT_SCRIPT QT_DEPLOY_SCRIPT CONTENT " qt_deploy_runtime_dependencies( EXECUTABLE ${BINARY_DEST_DIR}/$ BIN_DIR ${BINARY_DEST_DIR} LIBEXEC_DIR ${LIBRARY_DEST_DIR} LIB_DIR ${LIBRARY_DEST_DIR} PLUGINS_DIR ${PLUGIN_DEST_DIR} NO_OVERWRITE NO_TRANSLATIONS NO_COMPILER_RUNTIME DEPLOY_TOOL_OPTIONS ${QT_DEPLOY_TOOL_OPTIONS} )" ) # Bundle our linked dependencies install( RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET COMPONENT bundle DIRECTORIES ${CMAKE_SYSTEM_LIBRARY_PATH} ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} PRE_EXCLUDE_REGEXES "^(api-ms-win|ext-ms)-.*\\.dll$" # FIXME: Why aren't these caught by the below regex??? "^azure.*\\.dll$" "^vcruntime.*\\.dll$" POST_EXCLUDE_REGEXES "system32" LIBRARY DESTINATION ${LIBRARY_DEST_DIR} RUNTIME DESTINATION ${BINARY_DEST_DIR} FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} ) # Deploy Qt plugins install( SCRIPT ${QT_DEPLOY_SCRIPT} COMPONENT bundle ) # FIXME: remove this crap once we stop using msys2 if(MINGW) # i've not found a solution better than injecting the config vars like this... # with install(CODE" for everything everything just breaks install(CODE " set(QT_PLUGINS_DIR \"${QT_PLUGINS_DIR}\") set(QT_LIBS_DIR \"${QT_LIBS_DIR}\") set(QT_LIBEXECS_DIR \"${QT_LIBEXECS_DIR}\") set(CMAKE_SYSTEM_LIBRARY_PATH \"${CMAKE_SYSTEM_LIBRARY_PATH}\") set(CMAKE_INSTALL_PREFIX \"${CMAKE_INSTALL_PREFIX}\") " COMPONENT bundle) install(CODE [[ file(GLOB QT_IMAGEFORMAT_DLLS "${QT_PLUGINS_DIR}/imageformats/*.dll") set(CMAKE_GET_RUNTIME_DEPENDENCIES_TOOL objdump) file(GET_RUNTIME_DEPENDENCIES RESOLVED_DEPENDENCIES_VAR imageformatdeps LIBRARIES ${QT_IMAGEFORMAT_DLLS} DIRECTORIES ${CMAKE_SYSTEM_LIBRARY_PATH} ${QT_PLUGINS_DIR} ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} PRE_EXCLUDE_REGEXES "^(api-ms-win|ext-ms)-.*\\.dll$" # FIXME: Why aren't these caught by the below regex??? "^azure.*\\.dll$" "^vcruntime.*\\.dll$" POST_EXCLUDE_REGEXES "system32" ) foreach(_lib ${imageformatdeps}) file(INSTALL DESTINATION ${CMAKE_INSTALL_PREFIX} TYPE SHARED_LIBRARY FOLLOW_SYMLINK_CHAIN FILES ${_lib} ) endforeach() ]] COMPONENT bundle) endif() # Add qt.conf - this makes Qt stop looking for things outside the bundle install( CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")" COMPONENT bundle ) # Add qtlogging.ini as a config file install( FILES "qtlogging.ini" DESTINATION ${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR} COMPONENT bundle ) endif() find_program(CLANG_FORMAT clang-format OPTIONAL) if(CLANG_FORMAT) message(STATUS "Creating clang-format target") add_custom_target( clang-format COMMAND ${CLANG_FORMAT} -i --style=file:${CMAKE_SOURCE_DIR}/.clang-format ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${PRISMUPDATER_SOURCES} ${LINKEXE_SOURCES} ${PRECOMPILED_HEADERS} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} ) else() message(WARNING "Unable to find `clang-format`. Not creating custom target") endif() PrismLauncher-11.0.3/launcher/RuntimeContext.h0000644000175100017510000000450315224505336020727 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "SysInfo.h" #include "settings/SettingsObject.h" struct RuntimeContext { QString javaArchitecture; QString javaRealArchitecture; QString system = SysInfo::currentSystem(); QString mappedJavaRealArchitecture() const { if (javaRealArchitecture == "amd64") return "x86_64"; if (javaRealArchitecture == "i386" || javaRealArchitecture == "i686") return "x86"; if (javaRealArchitecture == "aarch64") return "arm64"; if (javaRealArchitecture == "arm" || javaRealArchitecture == "armhf") return "arm32"; return javaRealArchitecture; } void updateFromInstanceSettings(SettingsObject* instanceSettings) { javaArchitecture = instanceSettings->get("JavaArchitecture").toString(); javaRealArchitecture = instanceSettings->get("JavaRealArchitecture").toString(); } QString getClassifier() const { return system + "-" + mappedJavaRealArchitecture(); } // "Legacy" refers to the fact that Mojang assumed that these are the only two architectures bool isLegacyArch() const { const QString mapped = mappedJavaRealArchitecture(); return mapped == "x86_64" || mapped == "x86"; } bool classifierMatches(QString target) const { // try to match precise classifier "[os]-[arch]" bool x = target == getClassifier(); // try to match imprecise classifier on legacy architectures "[os]" if (!x && isLegacyArch()) x = target == system; return x; } }; PrismLauncher-11.0.3/launcher/StringUtils.cpp0000644000175100017510000002106015224505336020556 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "StringUtils.h" #include #include #include /// If you're wondering where these came from exactly, then know you're not the only one =D /// TAKEN FROM Qt, because it doesn't expose it intelligently static inline QChar getNextChar(const QString& s, int location) { return (location < s.length()) ? s.at(location) : QChar(); } /// TAKEN FROM Qt, because it doesn't expose it intelligently int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs) { int l1 = 0, l2 = 0; while (l1 <= s1.size() && l2 <= s2.size()) { // skip spaces, tabs and 0's QChar c1 = getNextChar(s1, l1); while (c1.isSpace()) c1 = getNextChar(s1, ++l1); QChar c2 = getNextChar(s2, l2); while (c2.isSpace()) c2 = getNextChar(s2, ++l2); if (c1.isDigit() && c2.isDigit()) { while (c1.digitValue() == 0) c1 = getNextChar(s1, ++l1); while (c2.digitValue() == 0) c2 = getNextChar(s2, ++l2); int lookAheadLocation1 = l1; int lookAheadLocation2 = l2; int currentReturnValue = 0; // find the last digit, setting currentReturnValue as we go if it isn't equal for (QChar lookAhead1 = c1, lookAhead2 = c2; (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length()); lookAhead1 = getNextChar(s1, ++lookAheadLocation1), lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) { bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit(); bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit(); if (!is1ADigit && !is2ADigit) break; if (!is1ADigit) return -1; if (!is2ADigit) return 1; if (currentReturnValue == 0) { if (lookAhead1 < lookAhead2) { currentReturnValue = -1; } else if (lookAhead1 > lookAhead2) { currentReturnValue = 1; } } } if (currentReturnValue != 0) return currentReturnValue; } if (cs == Qt::CaseInsensitive) { if (!c1.isLower()) c1 = c1.toLower(); if (!c2.isLower()) c2 = c2.toLower(); } int r = QString::localeAwareCompare(c1, c2); if (r < 0) return -1; if (r > 0) return 1; l1 += 1; l2 += 1; } // The two strings are the same (02 == 2) so fall back to the normal sort return QString::compare(s1, s2, cs); } QString StringUtils::truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_limit) { auto display_options = QUrl::RemoveUserInfo | QUrl::RemoveFragment | QUrl::NormalizePathSegments; auto str_url = url.toDisplayString(display_options); if (str_url.length() <= max_len) return str_url; auto url_path_parts = url.path().split('/'); QString last_path_segment = url_path_parts.takeLast(); if (url_path_parts.size() >= 1 && url_path_parts.first().isEmpty()) url_path_parts.removeFirst(); // drop empty first segment (from leading / ) if (url_path_parts.size() >= 1) url_path_parts.removeLast(); // drop the next to last path segment auto url_template = QStringLiteral("%1://%2/%3%4"); auto url_compact = url_path_parts.isEmpty() ? url_template.arg(url.scheme(), url.host(), QStringList({ "...", last_path_segment }).join('/'), url.query()) : url_template.arg(url.scheme(), url.host(), QStringList({ url_path_parts.join('/'), "...", last_path_segment }).join('/'), url.query()); // remove url parts one by one if it's still too long while (url_compact.length() > max_len && url_path_parts.size() >= 1) { url_path_parts.removeLast(); // drop the next to last path segment url_compact = url_path_parts.isEmpty() ? url_template.arg(url.scheme(), url.host(), QStringList({ "...", last_path_segment }).join('/'), url.query()) : url_template.arg(url.scheme(), url.host(), QStringList({ url_path_parts.join('/'), "...", last_path_segment }).join('/'), url.query()); } if ((url_compact.length() >= max_len) && hard_limit) { // still too long, truncate normally url_compact = QString(str_url); auto to_remove = url_compact.length() - max_len + 3; url_compact.remove(url_compact.length() - to_remove - 1, to_remove); url_compact.append("..."); } return url_compact; } static const QStringList s_units_si{ "KB", "MB", "GB", "TB" }; static const QStringList s_units_kibi{ "KiB", "MiB", "GiB", "TiB" }; QString StringUtils::humanReadableFileSize(double bytes, bool use_si, int decimal_points) { const QStringList units = use_si ? s_units_si : s_units_kibi; const int scale = use_si ? 1000 : 1024; int u = -1; double r = pow(10, decimal_points); do { bytes /= scale; u++; } while (round(abs(bytes) * r) / r >= scale && u < units.length() - 1); return QString::number(bytes, 'f', 2) + " " + units[u]; } QString StringUtils::getRandomAlphaNumeric() { return QUuid::createUuid().toString(QUuid::Id128); } QPair StringUtils::splitFirst(const QString& s, const QString& sep, Qt::CaseSensitivity cs) { QString left, right; auto index = s.indexOf(sep, 0, cs); left = s.mid(0, index); right = s.mid(index + sep.length()); return qMakePair(left, right); } QPair StringUtils::splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs) { QString left, right; auto index = s.indexOf(sep, 0, cs); left = s.mid(0, index); right = s.mid(left.length() + 1); return qMakePair(left, right); } QPair StringUtils::splitFirst(const QString& s, const QRegularExpression& re) { QString left, right; QRegularExpressionMatch match; auto index = s.indexOf(re, 0, &match); left = s.mid(0, index); auto end = match.hasMatch() ? left.length() + match.capturedLength() : left.length() + 1; right = s.mid(end); return qMakePair(left, right); } QString StringUtils::htmlListPatch(QString htmlStr) { static const QRegularExpression s_ulMatcher("<\\s*/\\s*ul\\s*>"); int pos = htmlStr.indexOf(s_ulMatcher); int imgPos; while (pos != -1) { pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the tag. Add one for zeroeth index imgPos = htmlStr.indexOf(""); pos = htmlStr.indexOf(s_ulMatcher, pos); } return htmlStr; } PrismLauncher-11.0.3/launcher/BaseInstaller.cpp0000644000175100017510000000271115224505336021021 0ustar runnerrunner/* 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 #include "BaseInstaller.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" BaseInstaller::BaseInstaller() {} bool BaseInstaller::isApplied(MinecraftInstance* on) { return QFile::exists(filename(on->instanceRoot())); } bool BaseInstaller::add(MinecraftInstance* to) { if (!patchesDir(to->instanceRoot()).exists()) { QDir(to->instanceRoot()).mkdir("patches"); } if (isApplied(to)) { if (!remove(to)) { return false; } } return true; } bool BaseInstaller::remove(MinecraftInstance* from) { return FS::deletePath(filename(from->instanceRoot())); } QString BaseInstaller::filename(const QString& root) const { return patchesDir(root).absoluteFilePath(id() + ".json"); } QDir BaseInstaller::patchesDir(const QString& root) const { return QDir(root + "/patches/"); } PrismLauncher-11.0.3/launcher/FastFileIconProvider.h0000644000175100017510000000161715224505336021763 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include class FastFileIconProvider : public QFileIconProvider { public: QIcon icon(const QFileInfo& info) const override; }; PrismLauncher-11.0.3/launcher/MMCZip.h0000644000175100017510000000770415224505336017044 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include #include #include #include "archive/ArchiveReader.h" #if defined(LAUNCHER_APPLICATION) #include "minecraft/mod/Mod.h" #endif namespace MMCZip { using FilterFileFunction = std::function; #if defined(LAUNCHER_APPLICATION) /** * take a source jar, add mods to it, resulting in target jar */ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); #endif /** * Extract a subdirectory from an archive */ std::optional extractSubDir(ArchiveReader* zip, const QString& subdir, const QString& target); /** * Extract a whole archive. * * \param fileCompressed The name of the archive. * \param dir The directory to extract to, the current directory if left empty. * \return The list of the full paths of the files extracted, empty on failure. */ std::optional extractDir(QString fileCompressed, QString dir); /** * Extract a subdirectory from an archive * * \param fileCompressed The name of the archive. * \param subdir The directory within the archive to extract * \param dir The directory to extract to, the current directory if left empty. * \return The list of the full paths of the files extracted, empty on failure. */ std::optional extractDir(QString fileCompressed, QString subdir, QString dir); /** * Extract a single file from an archive into a directory * * \param fileCompressed The name of the archive. * \param file The file within the archive to extract * \param dir The directory to extract to, the current directory if left empty. * \return true for success or false for failure */ bool extractFile(QString fileCompressed, QString file, QString dir); /** * Populate a QFileInfoList with a directory tree recursively, while allowing to excludeFilter what shouldn't be included. * \param rootDir directory to start off * \param subDir subdirectory, should be nullptr for first invocation * \param files resulting list of QFileInfo * \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude) * \return true for success or false for failure */ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter); } // namespace MMCZip PrismLauncher-11.0.3/launcher/Markdown.h0000644000175100017510000000151315224505336017517 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Joshua Goins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include QString markdownToHTML(const QString& markdown); PrismLauncher-11.0.3/launcher/modplatform/0000755000175100017510000000000015224505336020110 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/modplatform/ResourceAPI.h0000644000175100017510000001376315224505336022414 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023-2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include "../Version.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceType.h" #include "tasks/Task.h" /* Simple class with a common interface for interacting with APIs */ class ResourceAPI { public: virtual ~ResourceAPI() = default; struct SortingMethod { // The index of the sorting method. Used to allow for arbitrary ordering in the list of methods. // Used by Flame in the API request. unsigned int index; // The real name of the sorting, as used in the respective API specification. // Used by Modrinth in the API request. QString name; // The human-readable name of the sorting, used for display in the UI. QString readable_name; }; template struct Callback { std::function on_succeed; std::function on_fail; std::function on_abort; }; struct SearchArgs { ModPlatform::ResourceType type{}; int offset = 0; std::optional search; std::optional sorting; std::optional loaders; std::optional> versions; std::optional side; std::optional categoryIds; bool openSource{}; }; struct VersionSearchArgs { ModPlatform::IndexedPack::Ptr pack; std::optional> mcVersions; std::optional loaders; ModPlatform::ResourceType resourceType; bool includeChangelog{}; }; struct ProjectInfoArgs { ModPlatform::IndexedPack::Ptr pack; }; struct DependencySearchArgs { ModPlatform::Dependency dependency; Version mcVersion; ModPlatform::ModLoaderTypes loader; bool includeChangelog{}; }; public: /** Gets a list of available sorting methods for this API. */ virtual auto getSortingMethods() const -> QList = 0; public slots: virtual Task::Ptr searchProjects(SearchArgs&&, Callback>&&) const; virtual std::pair getProject(QString addonId, bool askRetry = true) const; virtual std::pair getProjects(QStringList addonIds) const = 0; virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback&&, bool askRetry = true) const; Task::Ptr getProjectVersions(VersionSearchArgs&& args, Callback>&& callbacks) const; virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback&&) const; protected: inline QString debugName() const { return "External resource API"; } QString mapMCVersionToModrinth(Version v) const; QString getGameVersionsString(std::vector mcVersions) const; public: virtual auto getSearchURL(const SearchArgs& args) const -> std::optional = 0; virtual auto getInfoURL(const QString& id) const -> std::optional = 0; virtual auto getVersionsURL(const VersionSearchArgs& args) const -> std::optional = 0; virtual auto getDependencyURL(const DependencySearchArgs& args) const -> std::optional = 0; /** Functions to load data into a pack. * * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way. */ virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) const = 0; virtual ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const = 0; /** Converts a JSON document to a common array format. * * This is needed so that different providers, with different JSON structures, can be parsed * uniformally. You NEED to re-implement this if you intend on using the default callbacks. */ virtual QJsonArray documentToArray(QJsonDocument& obj) const = 0; /** Functions to load data into a pack. * * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way. */ virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) const = 0; }; PrismLauncher-11.0.3/launcher/modplatform/EnsureMetadataTask.cpp0000644000175100017510000004172015224505336024345 0ustar runnerrunner#include "EnsureMetadataTask.h" #include #include #include "Application.h" #include "Json.h" #include "QObjectPtr.h" #include "minecraft/mod/Mod.h" #include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" static ModrinthAPI modrinth_api; static FlameAPI flame_api; EnsureMetadataTask::EnsureMetadataTask(Resource* resource, QDir dir, ModPlatform::ResourceProvider prov) : Task(), m_indexDir(dir), m_provider(prov), m_hashingTask(nullptr), m_currentTask(nullptr) { auto hashTask = createNewHash(resource); if (!hashTask) return; connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); }); connect(hashTask.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); }); m_hashingTask = hashTask; } EnsureMetadataTask::EnsureMetadataTask(QList& resources, QDir dir, ModPlatform::ResourceProvider prov) : Task(), m_indexDir(dir), m_provider(prov), m_currentTask(nullptr) { auto hashTask = makeShared("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); m_hashingTask = hashTask; for (auto* resource : resources) { auto hash_task = createNewHash(resource); if (!hash_task) continue; connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); }); connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); }); hashTask->addTask(hash_task); } } EnsureMetadataTask::EnsureMetadataTask(QHash& resources, QDir dir, ModPlatform::ResourceProvider prov) : Task(), m_resources(resources), m_indexDir(dir), m_provider(prov), m_currentTask(nullptr) {} Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Resource* resource) { if (!resource || !resource->valid() || resource->type() == ResourceType::FOLDER) return nullptr; return Hashing::createHasher(resource->fileinfo().absoluteFilePath(), m_provider); } QString EnsureMetadataTask::getExistingHash(Resource* resource) { // Check for already computed hashes // (linear on the number of mods vs. linear on the size of the mod's JAR) auto it = m_resources.keyValueBegin(); while (it != m_resources.keyValueEnd()) { if ((*it).second == resource) break; it++; } // We already have the hash computed if (it != m_resources.keyValueEnd()) { return (*it).first; } // No existing hash return {}; } bool EnsureMetadataTask::abort() { // Prevent sending signals to a dead object disconnect(this, 0, 0, 0); if (m_currentTask) return m_currentTask->abort(); return true; } void EnsureMetadataTask::executeTask() { setStatus(tr("Checking if resources have metadata...")); for (auto* resource : m_resources) { if (!resource->valid()) { qDebug() << "Resource" << resource->name() << "is invalid!"; emitFail(resource); continue; } // They already have the right metadata :o if (resource->status() != ResourceStatus::NO_METADATA && resource->metadata() && resource->metadata()->provider == m_provider) { qDebug() << "Resource" << resource->name() << "already has metadata!"; emitReady(resource); continue; } // Folders don't have metadata if (resource->type() == ResourceType::FOLDER) { emitReady(resource); } } Task::Ptr version_task; switch (m_provider) { case (ModPlatform::ResourceProvider::MODRINTH): version_task = modrinthVersionsTask(); break; case (ModPlatform::ResourceProvider::FLAME): version_task = flameVersionsTask(); break; } auto invalidade_leftover = [this] { for (auto resource = m_resources.constBegin(); resource != m_resources.constEnd(); resource++) emitFail(resource.value(), resource.key(), RemoveFromList::No); m_resources.clear(); emitSucceeded(); }; connect(version_task.get(), &Task::finished, this, [this, invalidade_leftover] { Task::Ptr project_task; switch (m_provider) { case (ModPlatform::ResourceProvider::MODRINTH): project_task = modrinthProjectsTask(); break; case (ModPlatform::ResourceProvider::FLAME): project_task = flameProjectsTask(); break; } if (!project_task) { invalidade_leftover(); return; } connect(project_task.get(), &Task::finished, this, [this, invalidade_leftover, project_task] { invalidade_leftover(); project_task->deleteLater(); if (m_currentTask) m_currentTask.reset(); }); connect(project_task.get(), &Task::failed, this, &EnsureMetadataTask::emitFailed); m_currentTask = project_task; project_task->start(); }); if (m_resources.size() > 1) setStatus(tr("Requesting metadata information from %1...").arg(ModPlatform::ProviderCapabilities::readableName(m_provider))); else if (!m_resources.empty()) setStatus(tr("Requesting metadata information from %1 for '%2'...") .arg(ModPlatform::ProviderCapabilities::readableName(m_provider), m_resources.begin().value()->name())); m_currentTask = version_task; version_task->start(); } void EnsureMetadataTask::emitReady(Resource* resource, QString key, RemoveFromList remove) { if (!resource) { qCritical() << "Tried to mark a null resource as ready."; if (!key.isEmpty()) m_resources.remove(key); return; } qDebug() << QString("Generated metadata for %1").arg(resource->name()); emit metadataReady(resource); if (remove == RemoveFromList::Yes) { if (key.isEmpty()) key = getExistingHash(resource); m_resources.remove(key); } } void EnsureMetadataTask::emitFail(Resource* resource, QString key, RemoveFromList remove) { if (!resource) { qCritical() << "Tried to mark a null resource as failed."; if (!key.isEmpty()) m_resources.remove(key); return; } qDebug() << QString("Failed to generate metadata for %1").arg(resource->name()); emit metadataFailed(resource); if (remove == RemoveFromList::Yes) { if (key.isEmpty()) key = getExistingHash(resource); m_resources.remove(key); } } // Modrinth Task::Ptr EnsureMetadataTask::modrinthVersionsTask() { auto hash_type = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first(); auto [ver_task, response] = modrinth_api.currentVersions(m_resources.keys(), hash_type); // Prevents unfortunate timings when aborting the task if (!ver_task) return Task::Ptr{ nullptr }; connect(ver_task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; failed(parse_error.errorString()); return; } try { auto entries = Json::requireObject(doc); for (auto& hash : m_resources.keys()) { auto resource = m_resources.find(hash).value(); try { auto entry = Json::requireObject(entries, hash); setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name())); qDebug() << "Getting version for" << resource->name() << "from Modrinth"; m_tempVersions.insert(hash, Modrinth::loadIndexedPackVersion(entry)); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; emitFail(resource); } } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } }); return ver_task; } Task::Ptr EnsureMetadataTask::modrinthProjectsTask() { QHash addonIds; for (auto const& data : m_tempVersions) addonIds.insert(data.addonId.toString(), data.hash); Task::Ptr proj_task; QByteArray* response; if (addonIds.isEmpty()) { qWarning() << "No addonId found!"; } else if (addonIds.size() == 1) { std::tie(proj_task, response) = modrinth_api.getProject(*addonIds.keyBegin()); } else { std::tie(proj_task, response) = modrinth_api.getProjects(addonIds.keys()); } // Prevents unfortunate timings when aborting the task if (!proj_task) return Task::Ptr{ nullptr }; connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Modrinth projects task at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } QJsonArray entries; try { if (addonIds.size() == 1) entries = { doc.object() }; else entries = Json::requireArray(doc); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } for (auto entry : entries) { ModPlatform::IndexedPack pack; try { auto entry_obj = Json::requireObject(entry); Modrinth::loadIndexedPack(pack, entry_obj); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; // Skip this entry, since it has problems continue; } auto hash = addonIds.find(pack.addonId.toString()).value(); auto resource_iter = m_resources.find(hash); if (resource_iter == m_resources.end()) { qWarning() << "Invalid project id from the API response."; continue; } auto* resource = resource_iter.value(); setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name())); updateMetadata(pack, m_tempVersions.find(hash).value(), resource); } }); return proj_task; } // Flame Task::Ptr EnsureMetadataTask::flameVersionsTask() { QList fingerprints; for (auto& murmur : m_resources.keys()) { fingerprints.push_back(murmur.toUInt()); } auto [ver_task, response] = flame_api.matchFingerprints(fingerprints); connect(ver_task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Flame::CurrentVersions at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; failed(parse_error.errorString()); return; } try { auto doc_obj = Json::requireObject(doc); auto data_obj = Json::requireObject(doc_obj, "data"); auto data_arr = Json::requireArray(data_obj, "exactMatches"); if (data_arr.isEmpty()) { qWarning() << "No matches found for fingerprint search!"; return; } for (auto match : data_arr) { auto match_obj = match.toObject(); auto file_obj = match_obj["file"].toObject(); if (match_obj.isEmpty() || file_obj.isEmpty()) { qWarning() << "Fingerprint match is empty!"; return; } auto fingerprint = QString::number(file_obj["fileFingerprint"].toInteger()); auto resource = m_resources.find(fingerprint); if (resource == m_resources.end()) { qWarning() << "Invalid fingerprint from the API response."; continue; } setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*resource)->name())); m_tempVersions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj)); } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } }); return ver_task; } Task::Ptr EnsureMetadataTask::flameProjectsTask() { QHash addonIds; for (auto const& hash : m_resources.keys()) { if (m_tempVersions.contains(hash)) { auto data = m_tempVersions.find(hash).value(); auto id_str = data.addonId.toString(); if (!id_str.isEmpty()) addonIds.insert(data.addonId.toString(), hash); } } Task::Ptr proj_task; QByteArray* response; if (addonIds.isEmpty()) { qWarning() << "No addonId found!"; } else if (addonIds.size() == 1) { std::tie(proj_task, response) = flame_api.getProject(*addonIds.keyBegin()); } else { std::tie(proj_task, response) = flame_api.getProjects(addonIds.keys()); } // Prevents unfortunate timings when aborting the task if (!proj_task) return Task::Ptr{ nullptr }; connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Flame projects task at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } try { QJsonArray entries; if (addonIds.size() == 1) entries = { Json::requireObject(Json::requireObject(doc), "data") }; else entries = Json::requireArray(Json::requireObject(doc), "data"); for (auto entry : entries) { auto entry_obj = Json::requireObject(entry); auto id = QString::number(Json::requireInteger(entry_obj, "id")); auto hash = addonIds.find(id).value(); auto resource = m_resources.find(hash).value(); ModPlatform::IndexedPack pack; try { setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name())); FlameMod::loadIndexedPack(pack, entry_obj); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; emitFail(resource); } updateMetadata(pack, m_tempVersions.find(hash).value(), resource); } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } }); return proj_task; } void EnsureMetadataTask::updateMetadata(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource* resource) { try { // Prevent file name mismatch ver.fileName = resource->fileinfo().fileName(); if (ver.fileName.endsWith(".disabled")) ver.fileName.chop(9); auto task = makeShared(m_indexDir, pack, ver); connect(task.get(), &Task::finished, this, [this, &pack, resource] { updateMetadataCallback(pack, resource); }); m_updateMetadataTasks[ModPlatform::ProviderCapabilities::name(pack.provider) + pack.addonId.toString()] = task; task->start(); } catch (Json::JsonException& e) { qDebug() << e.cause(); emitFail(resource); } } void EnsureMetadataTask::updateMetadataCallback(ModPlatform::IndexedPack& pack, Resource* resource) { QDir tmpIndexDir(m_indexDir); auto metadata = Metadata::get(tmpIndexDir, pack.slug); if (!metadata.isValid()) { qCritical() << "Failed to generate metadata at last step!"; emitFail(resource); return; } resource->setMetadata(metadata); emitReady(resource); } PrismLauncher-11.0.3/launcher/modplatform/ResourceType.h0000644000175100017510000000271015224505336022712 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include namespace ModPlatform { enum class ResourceType { Mod, ResourcePack, ShaderPack, Modpack, DataPack, World, Screenshots, TexturePack, Unknown }; namespace ResourceTypeUtils { static const std::set VALID_RESOURCES = { ResourceType::DataPack, ResourceType::ResourcePack, ResourceType::TexturePack, ResourceType::ShaderPack, ResourceType::World, ResourceType::Mod }; QString getName(ResourceType type); } // namespace ResourceTypeUtils } // namespace ModPlatform PrismLauncher-11.0.3/launcher/modplatform/ResourceType.cpp0000644000175100017510000000356715224505336023260 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ResourceType.h" namespace ModPlatform { static const QMap s_packedTypeNames = { { ResourceType::ResourcePack, QObject::tr("resource pack") }, { ResourceType::TexturePack, QObject::tr("texture pack") }, { ResourceType::DataPack, QObject::tr("data pack") }, { ResourceType::ShaderPack, QObject::tr("shader pack") }, { ResourceType::World, QObject::tr("world save") }, { ResourceType::Mod, QObject::tr("mod") }, { ResourceType::Unknown, QObject::tr("unknown") } }; namespace ResourceTypeUtils { QString getName(ResourceType type) { return s_packedTypeNames.constFind(type).value(); } } // namespace ResourceTypeUtils } // namespace ModPlatform PrismLauncher-11.0.3/launcher/modplatform/technic/0000755000175100017510000000000015224505336021525 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/modplatform/technic/SolderPackManifest.h0000644000175100017510000000233115224505336025413 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include namespace TechnicSolder { struct Pack { QString recommended; QString latest; QList builds; }; void loadPack(Pack& v, QJsonObject& obj); struct PackBuildMod { QString name; QString version; QString md5; QString url; }; struct PackBuild { QString minecraft; QList mods; }; void loadPackBuild(PackBuild& v, QJsonObject& obj); } // namespace TechnicSolder PrismLauncher-11.0.3/launcher/modplatform/technic/SolderPackInstallTask.cpp0000644000175100017510000002007315224505336026434 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2021-2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "SolderPackInstallTask.h" #include #include #include #include #include "SolderPackManifest.h" #include "TechnicPackProcessor.h" #include "net/ApiDownload.h" #include "net/ChecksumValidator.h" Technic::SolderPackInstallTask::SolderPackInstallTask(QNetworkAccessManager* network, const QUrl& solderUrl, const QString& pack, const QString& version, const QString& minecraftVersion) { m_solderUrl = solderUrl; m_pack = pack; m_version = version; m_network = network; m_minecraftVersion = minecraftVersion; } bool Technic::SolderPackInstallTask::abort() { if (m_abortable) { return m_filesNetJob->abort(); } return false; } void Technic::SolderPackInstallTask::executeTask() { setStatus(tr("Resolving modpack files")); m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"), m_network)); auto sourceUrl = QString("%1/modpack/%2/%3").arg(m_solderUrl.toString(), m_pack, m_version); auto [action, response] = Net::ApiDownload::makeByteArray(sourceUrl); m_filesNetJob->addNetAction(action); auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, [this, response] { fileListSucceeded(response); }); connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); connect(job, &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted); m_filesNetJob->start(); } void Technic::SolderPackInstallTask::fileListSucceeded(QByteArray* response) { setStatus(tr("Downloading modpack")); QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Solder at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } auto obj = doc.object(); TechnicSolder::PackBuild build; try { TechnicSolder::loadPackBuild(build, obj); } catch (const JSONValidationError& e) { m_filesNetJob.reset(); emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); return; } if (!build.minecraft.isEmpty()) m_minecraftVersion = build.minecraft; m_filesNetJob.reset(new NetJob(tr("Downloading modpack"), m_network)); int i = 0; for (const auto& mod : build.mods) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); auto dl = Net::ApiDownload::makeFile(mod.url, path); if (!mod.md5.isEmpty()) { dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } m_filesNetJob->addNetAction(dl); i++; } m_modCount = build.mods.size(); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &Technic::SolderPackInstallTask::propagateStepProgress); connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); connect(m_filesNetJob.get(), &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted); m_filesNetJob->start(); } void Technic::SolderPackInstallTask::downloadSucceeded() { m_abortable = false; setStatus(tr("Extracting modpack")); m_filesNetJob.reset(); m_extractFuture = QtConcurrent::run([this]() { int i = 0; QString extractDir = FS::PathCombine(m_stagingPath, "minecraft"); FS::ensureFolderPathExists(extractDir); while (m_modCount > i) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); if (!MMCZip::extractDir(path, extractDir)) { return false; } i++; } return true; }); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &Technic::SolderPackInstallTask::extractFinished); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &Technic::SolderPackInstallTask::extractAborted); m_extractFutureWatcher.setFuture(m_extractFuture); } void Technic::SolderPackInstallTask::downloadFailed(QString reason) { m_abortable = false; m_filesNetJob.reset(); emitFailed(reason); } void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) { m_abortable = true; setProgress(current / 2, total); } void Technic::SolderPackInstallTask::downloadAborted() { m_filesNetJob.reset(); emitAborted(); } void Technic::SolderPackInstallTask::extractFinished() { if (!m_extractFuture.result()) { emitFailed(tr("Failed to extract modpack")); return; } QDir extractDir(m_stagingPath); qDebug() << "Fixing permissions for extracted pack files..."; QDirIterator it(extractDir, QDirIterator::Subdirectories); while (it.hasNext()) { auto filepath = it.next(); QFileInfo file(filepath); auto permissions = QFile::permissions(filepath); auto origPermissions = permissions; if (file.isDir()) { // Folder +rwx for current user permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser; } else { // File +rw for current user permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; } if (origPermissions != permissions) { if (!QFile::setPermissions(filepath, permissions)) { logWarning(tr("Could not fix permissions for %1").arg(filepath)); } else { qDebug() << "Fixed" << filepath; } } } auto packProcessor = makeShared(); connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion, true); } void Technic::SolderPackInstallTask::extractAborted() { emitFailed(tr("Instance import has been aborted.")); } PrismLauncher-11.0.3/launcher/modplatform/technic/TechnicPackProcessor.h0000644000175100017510000000231215224505336025750 0ustar runnerrunner/* Copyright 2020-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 #include "settings/SettingsObject.h" namespace Technic { // not exporting it, only used in SingleZipPackInstallTask, InstanceImportTask and SolderPackInstallTask class TechnicPackProcessor : public QObject { Q_OBJECT signals: void succeeded(); void failed(QString reason); public: void run(SettingsObject* globalSettings, const QString& instName, const QString& instIcon, const QString& stagingPath, const QString& minecraftVersion = QString(), bool isSolder = false); }; } // namespace Technic PrismLauncher-11.0.3/launcher/modplatform/technic/SolderPackInstallTask.h0000644000175100017510000000543215224505336026103 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2021-2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include namespace Technic { class SolderPackInstallTask : public InstanceTask { Q_OBJECT public: explicit SolderPackInstallTask(QNetworkAccessManager* network, const QUrl& solderUrl, const QString& pack, const QString& version, const QString& minecraftVersion); bool canAbort() const override { return true; } bool abort() override; protected: //! Entry point for tasks. virtual void executeTask() override; private slots: void fileListSucceeded(QByteArray* response); void downloadSucceeded(); void downloadFailed(QString reason); void downloadProgressChanged(qint64 current, qint64 total); void downloadAborted(); void extractFinished(); void extractAborted(); private: bool m_abortable = false; QNetworkAccessManager* m_network; NetJob::Ptr m_filesNetJob; QUrl m_solderUrl; QString m_pack; QString m_version; QString m_minecraftVersion; QTemporaryDir m_outputDir; int m_modCount; QFuture m_extractFuture; QFutureWatcher m_extractFutureWatcher; }; } // namespace Technic PrismLauncher-11.0.3/launcher/modplatform/technic/SingleZipPackInstallTask.h0000644000175100017510000000322415224505336026554 0ustar runnerrunner/* 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 "InstanceTask.h" #include "archive/ArchiveReader.h" #include "net/NetJob.h" #include #include #include #include namespace Technic { class SingleZipPackInstallTask : public InstanceTask { Q_OBJECT public: SingleZipPackInstallTask(const QUrl& sourceUrl, const QString& minecraftVersion); bool canAbort() const override { return true; } bool abort() override; protected: void executeTask() override; private slots: void downloadSucceeded(); void downloadFailed(QString reason); void downloadProgressChanged(qint64 current, qint64 total); void extractFinished(); void extractAborted(); private: bool m_abortable = false; QUrl m_sourceUrl; QString m_minecraftVersion; QString m_archivePath; NetJob::Ptr m_filesNetJob; std::unique_ptr m_packZip; QFuture> m_extractFuture; QFutureWatcher> m_extractFutureWatcher; }; } // namespace Technic PrismLauncher-11.0.3/launcher/modplatform/technic/TechnicPackProcessor.cpp0000644000175100017510000002147415224505336026315 0ustar runnerrunner/* Copyright 2020-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 "TechnicPackProcessor.h" #include #include #include #include #include #include #include "archive/ArchiveReader.h" void Technic::TechnicPackProcessor::run(SettingsObject* globalSettings, const QString& instName, const QString& instIcon, const QString& stagingPath, const QString& minecraftVersion, [[maybe_unused]] const bool isSolder) { QString minecraftPath = FS::PathCombine(stagingPath, "minecraft"); QString configPath = FS::PathCombine(stagingPath, "instance.cfg"); auto instanceSettings = std::make_unique(configPath); MinecraftInstance instance(globalSettings, std::move(instanceSettings), stagingPath); instance.setName(instName); if (instIcon != "default") { instance.setIconKey(instIcon); } auto components = instance.getPackProfile(); components->buildingFromScratch(); QByteArray data; QString modpackJar = FS::PathCombine(minecraftPath, "bin", "modpack.jar"); QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json"); QString fmlMinecraftVersion; if (QFile::exists(modpackJar)) { MMCZip::ArchiveReader zipFile(modpackJar); if (!zipFile.collectFiles()) { emit failed(tr("Unable to open \"bin/modpack.jar\" file!")); return; } if (zipFile.exists("/version.json")) { if (zipFile.exists("/fmlversion.properties")) { auto file = zipFile.goToFile("fmlversion.properties"); if (!file) { emit failed(tr("Unable to open \"fmlversion.properties\"!")); return; } QByteArray fmlVersionData = file->readAll(); INIFile iniFile; iniFile.loadFile(fmlVersionData); // If not present, this evaluates to a null string fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString(); } auto file = zipFile.goToFile("version.json"); if (!file) { emit failed(tr("Unable to open \"version.json\"!")); return; } data = file->readAll(); } else { if (minecraftVersion.isEmpty()) { emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown")); return; } components->setComponentVersion("net.minecraft", minecraftVersion, true); components->installJarMods({ modpackJar }); // Forge for 1.4.7 and for 1.5.2 require extra libraries. // Figure out the forge version and add it as a component // (the code still comes from the jar mod installed above) if (zipFile.exists("/forgeversion.properties")) { auto file = zipFile.goToFile("forgeversion.properties"); if (!file) { // Really shouldn't happen, but error handling shall not be forgotten emit failed(tr("Unable to open \"forgeversion.properties\"")); return; } auto forgeVersionData = file->readAll(); INIFile iniFile; iniFile.loadFile(forgeVersionData); QString major, minor, revision, build; major = iniFile["forge.major.number"].toString(); minor = iniFile["forge.minor.number"].toString(); revision = iniFile["forge.revision.number"].toString(); build = iniFile["forge.build.number"].toString(); if (major.isEmpty() || minor.isEmpty() || revision.isEmpty() || build.isEmpty()) { emit failed(tr("Invalid \"forgeversion.properties\"!")); return; } components->setComponentVersion("net.minecraftforge", major + '.' + minor + '.' + revision + '.' + build); } components->saveNow(); emit succeeded(); return; } } else if (QFile::exists(versionJson)) { QFile file(versionJson); if (!file.open(QIODevice::ReadOnly)) { emit failed(tr("Unable to open \"version.json\": %1").arg(file.errorString())); return; } data = file.readAll(); file.close(); } else { // This is the "Vanilla" modpack, excluded by the search code components->setComponentVersion("net.minecraft", minecraftVersion, true); components->saveNow(); emit succeeded(); return; } try { QJsonDocument doc = Json::requireDocument(data); QJsonObject root = Json::requireObject(doc, "version.json"); QString packMinecraftVersion = root["inheritsFrom"].toString(); if (packMinecraftVersion.isEmpty()) { if (fmlMinecraftVersion.isEmpty()) { emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing")); return; } packMinecraftVersion = fmlMinecraftVersion; } components->setComponentVersion("net.minecraft", packMinecraftVersion, true); for (auto library : root["libraries"].toArray()) { if (!library.isObject()) { continue; } auto libraryObject = library.toObject(); auto libraryName = libraryObject["name"].toString(); if (libraryName.startsWith("net.neoforged.fancymodloader:")) { // it is neoforge // no easy way to get the version from the libs so use the arguments auto arguments = root["arguments"].toObject(); bool isVersionArg = false; QString neoforgeVersion; for (auto arg : arguments["game"].toArray()) { auto argument = arg.toString(""); if (isVersionArg) { neoforgeVersion = argument; break; } else { isVersionArg = "--fml.neoForgeVersion" == argument || "--fml.forgeVersion" == argument; } } if (!neoforgeVersion.isEmpty()) { components->setComponentVersion("net.neoforged", neoforgeVersion); } break; } else if ((libraryName.startsWith("net.minecraftforge:forge:") || libraryName.startsWith("net.minecraftforge:fmlloader:")) && libraryName.contains('-')) { QString libraryVersion = libraryName.section(':', 2); if (!libraryVersion.startsWith("1.7.10-")) { components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1)); } else { // 1.7.10 versions sometimes look like 1.7.10-10.13.4.1614-1.7.10, this filters out the 10.13.4.1614 part components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1)); } break; } else { // -> static QMap loaderMap{ { "net.minecraftforge:minecraftforge:", "net.minecraftforge" }, { "net.fabricmc:fabric-loader:", "net.fabricmc.fabric-loader" }, { "org.quiltmc:quilt-loader:", "org.quiltmc.quilt-loader" } }; for (const auto& loader : loaderMap.keys()) { if (libraryName.startsWith(loader)) { components->setComponentVersion(loaderMap.value(loader), libraryName.section(':', 2)); break; } } } } } catch (const JSONValidationError& e) { emit failed(tr("Could not understand \"version.json\":\n") + e.cause()); return; } components->saveNow(); emit succeeded(); } PrismLauncher-11.0.3/launcher/modplatform/technic/SingleZipPackInstallTask.cpp0000644000175100017510000001176315224505336027116 0ustar runnerrunner/* 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 "SingleZipPackInstallTask.h" #include #include "FileSystem.h" #include "MMCZip.h" #include "TechnicPackProcessor.h" #include "Application.h" #include "net/ApiDownload.h" Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl& sourceUrl, const QString& minecraftVersion) { m_sourceUrl = sourceUrl; m_minecraftVersion = minecraftVersion; } bool Technic::SingleZipPackInstallTask::abort() { if (m_abortable) { return m_filesNetJob->abort(); } return false; } void Technic::SingleZipPackInstallTask::executeTask() { setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); auto entry = APPLICATION->metacache()->resolveEntry("general", path); entry->setStale(true); m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network())); m_filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry)); m_archivePath = entry->getFullPath(); auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded); connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged); connect(job, &NetJob::stepProgress, this, &Technic::SingleZipPackInstallTask::propagateStepProgress); connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed); m_filesNetJob->start(); } void Technic::SingleZipPackInstallTask::downloadSucceeded() { m_abortable = false; setStatus(tr("Extracting modpack")); QDir extractDir(FS::PathCombine(m_stagingPath, "minecraft")); qDebug() << "Attempting to create instance from" << m_archivePath; // open the zip and find relevant files in it m_packZip.reset(new MMCZip::ArchiveReader(m_archivePath)); m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath()); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &Technic::SingleZipPackInstallTask::extractFinished); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &Technic::SingleZipPackInstallTask::extractAborted); m_extractFutureWatcher.setFuture(m_extractFuture); m_filesNetJob.reset(); } void Technic::SingleZipPackInstallTask::downloadFailed(QString reason) { m_abortable = false; m_filesNetJob.reset(); emitFailed(reason); } void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) { m_abortable = true; setProgress(current / 2, total); } void Technic::SingleZipPackInstallTask::extractFinished() { m_packZip.reset(); if (!m_extractFuture.result()) { emitFailed(tr("Failed to extract modpack")); return; } QDir extractDir(m_stagingPath); qDebug() << "Fixing permissions for extracted pack files..."; QDirIterator it(extractDir, QDirIterator::Subdirectories); while (it.hasNext()) { auto filepath = it.next(); QFileInfo file(filepath); auto permissions = QFile::permissions(filepath); auto origPermissions = permissions; if (file.isDir()) { // Folder +rwx for current user permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser; } else { // File +rw for current user permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; } if (origPermissions != permissions) { if (!QFile::setPermissions(filepath, permissions)) { logWarning(tr("Could not fix permissions for %1").arg(filepath)); } else { qDebug() << "Fixed" << filepath; } } } auto packProcessor = makeShared(); connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion); } void Technic::SingleZipPackInstallTask::extractAborted() { emitFailed(tr("Instance import has been aborted.")); } PrismLauncher-11.0.3/launcher/modplatform/technic/SolderPackManifest.cpp0000644000175100017510000000341215224505336025747 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "SolderPackManifest.h" #include "Json.h" namespace TechnicSolder { void loadPack(Pack& v, QJsonObject& obj) { v.recommended = Json::requireString(obj, "recommended"); v.latest = Json::requireString(obj, "latest"); auto builds = Json::requireArray(obj, "builds"); for (const auto buildRaw : builds) { auto build = Json::requireString(buildRaw); v.builds.append(build); } } static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj) { b.name = Json::requireString(obj, "name"); b.version = obj["version"].toString(""); b.md5 = Json::requireString(obj, "md5"); b.url = Json::requireString(obj, "url"); } void loadPackBuild(PackBuild& v, QJsonObject& obj) { v.minecraft = Json::requireString(obj, "minecraft"); auto mods = Json::requireArray(obj, "mods"); for (const auto modRaw : mods) { auto modObj = Json::requireObject(modRaw); PackBuildMod mod; loadPackBuildMod(mod, modObj); v.mods.append(mod); } } } // namespace TechnicSolder PrismLauncher-11.0.3/launcher/modplatform/flame/0000755000175100017510000000000015224505336021174 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/modplatform/flame/FileResolvingTask.cpp0000644000175100017510000002545515224505336025306 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "FileResolvingTask.h" #include #include "Json.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" #include "net/NetJob.h" #include "tasks/Task.h" #include "Application.h" static const FlameAPI flameAPI; static ModrinthAPI modrinthAPI; Flame::FileResolvingTask::FileResolvingTask(Flame::Manifest& toProcess) : m_manifest(toProcess) {} bool Flame::FileResolvingTask::abort() { bool aborted = true; if (m_task) { aborted = m_task->abort(); } return aborted ? Task::abort() : false; } void Flame::FileResolvingTask::executeTask() { if (m_manifest.files.isEmpty()) { // no file to resolve so leave it empty and emit success immediately emitSucceeded(); return; } setStatus(tr("Resolving mod IDs...")); setProgress(0, 3); QStringList fileIds; for (auto file : m_manifest.files) { fileIds.push_back(QString::number(file.fileId)); } auto [task, response] = flameAPI.getFiles(fileIds); m_task = task; auto step_progress = std::make_shared(); connect(m_task.get(), &Task::succeeded, this, [this, response, step_progress]() { step_progress->state = TaskStepState::Succeeded; stepProgress(*step_progress); netJobFinished(response); }); connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { step_progress->state = TaskStepState::Failed; stepProgress(*step_progress); emitFailed(reason); }); connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) { qDebug() << "Resolve slug progress" << current << total; step_progress->update(current, total); stepProgress(*step_progress); }); connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) { step_progress->status = status; stepProgress(*step_progress); }); m_task->start(); } ModPlatform::ResourceType getResourceType(int classId) { switch (classId) { case 17: // Worlds return ModPlatform::ResourceType::World; case 6: // Mods return ModPlatform::ResourceType::Mod; case 12: // Resource Packs // return ModPlatform::ResourceType::ResourcePack; // not really a resourcepack /* fallthrough */ case 4546: // Customization // return ModPlatform::ResourceType::ShaderPack; // not really a shaderPack /* fallthrough */ case 4471: // Modpacks /* fallthrough */ case 5: // Bukkit Plugins /* fallthrough */ case 4559: // Addons /* fallthrough */ default: return ModPlatform::ResourceType::Unknown; } } void Flame::FileResolvingTask::netJobFinished(QByteArray* response) { setProgress(1, 3); // job to check modrinth for blocked projects QJsonDocument doc; QJsonArray array; try { doc = Json::requireDocument(*response); array = Json::requireArray(doc.object()["data"]); } catch (Json::JsonException& e) { qCritical() << "Non-JSON data returned from the CF API"; qCritical() << e.cause(); emitFailed(tr("Invalid data returned from the API.")); return; } QStringList hashes; for (QJsonValueRef file : array) { try { 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()) { hashes.push_back(version.hash); } } catch (Json::JsonException& e) { qCritical() << "Non-JSON data returned from the CF API"; qCritical() << e.cause(); emitFailed(tr("Invalid data returned from the API.")); return; } } if (hashes.isEmpty()) { getFlameProjects(); return; } auto [modrinthTask, modrinthResponse] = modrinthAPI.currentVersions(hashes, "sha1"); m_task = modrinthTask; (dynamic_cast(m_task.get()))->setAskRetry(false); auto step_progress = std::make_shared(); connect(m_task.get(), &Task::succeeded, this, [this, modrinthResponse, step_progress]() { step_progress->state = TaskStepState::Succeeded; stepProgress(*step_progress); QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*modrinthResponse, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *modrinthResponse; getFlameProjects(); return; } if (APPLICATION->settings()->get("FallbackMRBlockedMods").toBool()){ try { auto entries = Json::requireObject(doc); for (auto& out : m_manifest.files) { auto url = QUrl(out.version.downloadUrl, QUrl::TolerantMode); if (!url.isValid() && "sha1" == out.version.hash_type && !out.version.hash.isEmpty()) { try { auto entry = Json::requireObject(entries, out.version.hash); auto file = Modrinth::loadIndexedPackVersion(entry); out.version.downloadUrl = file.downloadUrl; qDebug() << "Found alternative on modrinth" << out.version.fileName; } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; } } } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } } getFlameProjects(); }); connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { step_progress->state = TaskStepState::Failed; stepProgress(*step_progress); getFlameProjects(); }); connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) { qDebug() << "Resolve slug progress" << current << total; step_progress->update(current, total); stepProgress(*step_progress); }); connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) { step_progress->status = status; stepProgress(*step_progress); }); m_task->start(); } void Flame::FileResolvingTask::getFlameProjects() { setProgress(2, 3); QStringList addonIds; for (auto file : m_manifest.files) { addonIds.push_back(QString::number(file.projectId)); } auto [task, response] = flameAPI.getProjects(addonIds); m_task = task; auto step_progress = std::make_shared(); connect(m_task.get(), &Task::succeeded, this, [this, response, step_progress] { QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Modrinth projects task at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } try { QJsonArray entries; entries = Json::requireArray(Json::requireObject(doc), "data"); for (auto entry : entries) { auto entry_obj = Json::requireObject(entry); auto id = Json::requireInteger(entry_obj, "id"); auto file = std::find_if(m_manifest.files.begin(), m_manifest.files.end(), [id](const Flame::File& file) { return file.projectId == id; }); if (file == m_manifest.files.end()) { continue; } setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName)); FlameMod::loadIndexedPack(file->pack, entry_obj); file->resourceType = getResourceType(Json::requireInteger(entry_obj, "classId", "modClassId")); if (file->resourceType == ModPlatform::ResourceType::World) { file->targetFolder = "saves"; } } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } step_progress->state = TaskStepState::Succeeded; stepProgress(*step_progress); emitSucceeded(); }); connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { step_progress->state = TaskStepState::Failed; stepProgress(*step_progress); emitFailed(reason); }); connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) { qDebug() << "Resolve slug progress" << current << total; step_progress->update(current, total); stepProgress(*step_progress); }); connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) { step_progress->status = status; stepProgress(*step_progress); }); m_task->start(); } PrismLauncher-11.0.3/launcher/modplatform/flame/PackManifest.h0000644000175100017510000000500015224505336023705 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include "modplatform/ModIndex.h" #include "modplatform/ResourceType.h" namespace Flame { struct File { int projectId = 0; int fileId = 0; // NOTE: the opposite to 'optional' bool required = true; ModPlatform::IndexedPack pack; ModPlatform::IndexedVersion version; // our QString targetFolder = QStringLiteral("mods"); ModPlatform::ResourceType resourceType; }; struct Modloader { QString id; bool primary = false; }; struct Minecraft { QString version; QString libraries; QList modLoaders; int recommendedRAM; }; struct Manifest { QString manifestType; int manifestVersion = 0; Flame::Minecraft minecraft; QString name; QString version; QString author; // File id -> File QMap files; QString overrides; bool is_loaded = false; }; void loadManifest(Flame::Manifest& m, const QString& filepath); } // namespace Flame PrismLauncher-11.0.3/launcher/modplatform/flame/FlameModIndex.h0000644000175100017510000000101015224505336024011 0ustar runnerrunner// // Created by timoreo on 16/01/2022. // #pragma once #include "modplatform/ModIndex.h" #include "BaseInstance.h" namespace FlameMod { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadBody(ModPlatform::IndexedPack& m); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr); ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false); } // namespace FlameMod PrismLauncher-11.0.3/launcher/modplatform/flame/FlameInstanceCreationTask.h0000644000175100017510000000632115224505336026370 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "InstanceCreationTask.h" #include #include "minecraft/MinecraftInstance.h" #include "modplatform/flame/FileResolvingTask.h" #include "net/NetJob.h" #include "ui/dialogs/BlockedModsDialog.h" class FlameCreationTask final : public InstanceCreationTask { Q_OBJECT public: FlameCreationTask(const QString& staging_path, SettingsObject* global_settings, QWidget* parent, QString id, QString version_id, QString original_instance_id = {}) : InstanceCreationTask(), m_parent(parent), m_managedId(std::move(id)), m_managedVersionId(std::move(version_id)) { setStagingPath(staging_path); setParentSettings(global_settings); m_original_instance_id = std::move(original_instance_id); } bool abort() override; bool updateInstance() override; std::unique_ptr createInstance() override; private slots: void idResolverSucceeded(QEventLoop&); void setupDownloadJob(QEventLoop&); void copyBlockedMods(QList const& blocked_mods); void validateOtherResources(QEventLoop& loop); QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion); private: QWidget* m_parent = nullptr; shared_qobject_ptr m_modIdResolver; Flame::Manifest m_pack; // Handle to allow aborting Task::Ptr m_processUpdateFileInfoJob = nullptr; NetJob::Ptr m_filesJob = nullptr; QString m_managedId, m_managedVersionId; QList> m_otherResources; std::optional m_instance; QStringList m_selectedOptionalMods; }; PrismLauncher-11.0.3/launcher/modplatform/flame/FlamePackExportTask.h0000644000175100017510000000424015224505336025215 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "MMCZip.h" #include "minecraft/MinecraftInstance.h" #include "modplatform/flame/FlameAPI.h" #include "tasks/Task.h" struct FlamePackExportOptions { QString name; QString version; QString author; bool optionalFiles; MinecraftInstance* instance; QString output; MMCZip::FilterFileFunction filter; int recommendedRAM; }; class FlamePackExportTask : public Task { Q_OBJECT public: FlamePackExportTask(FlamePackExportOptions&& options); protected: void executeTask() override; bool abort() override; private: static const QString TEMPLATE; static const QStringList FILE_EXTENSIONS; // inputs struct ResolvedFile { int addonId; int version; bool enabled; bool isMod; QString name; QString slug; QString authors; }; struct HashInfo { QString name; QString path; bool enabled; bool isMod; }; FlamePackExportOptions m_options; QDir m_gameRoot; FlameAPI api; QFileInfoList m_files; QMap pendingHashes{}; QMap resolvedFiles{}; Task::Ptr task; void collectFiles(); void collectHashes(); void makeApiRequest(); void getProjectsInfo(); void buildZip(); QByteArray generateIndex(); QByteArray generateHTML(); }; PrismLauncher-11.0.3/launcher/modplatform/flame/FileResolvingTask.h0000644000175100017510000000260715224505336024745 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "PackManifest.h" #include "tasks/Task.h" namespace Flame { class FileResolvingTask : public Task { Q_OBJECT public: explicit FileResolvingTask(Flame::Manifest& toProcess); virtual ~FileResolvingTask() = default; bool canAbort() const override { return true; } bool abort() override; const Flame::Manifest& getResults() const { return m_manifest; } protected: virtual void executeTask() override; protected slots: void netJobFinished(QByteArray* response); private: void getFlameProjects(); private: /* data */ Flame::Manifest m_manifest; Task::Ptr m_task; }; } // namespace Flame PrismLauncher-11.0.3/launcher/modplatform/flame/FlameInstanceCreationTask.cpp0000644000175100017510000006663015224505336026734 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "FlameInstanceCreationTask.h" #include "InstanceTask.h" #include "QObjectPtr.h" #include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" #include "modplatform/flame/PackManifest.h" #include "Application.h" #include "FileSystem.h" #include "InstanceList.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/helpers/OverrideUtils.h" #include "settings/INISettingsObject.h" #include "SysInfo.h" #include "tasks/ConcurrentTask.h" #include "ui/dialogs/BlockedModsDialog.h" #include "ui/dialogs/CustomMessageBox.h" #include #include #include "HardwareInfo.h" #include "meta/Index.h" #include "minecraft/World.h" #include "minecraft/mod/tasks/LocalResourceParse.h" #include "net/ApiDownload.h" #include "ui/pages/modplatform/OptionalModDialog.h" static const FlameAPI api; bool FlameCreationTask::abort() { if (!canAbort()) return false; if (m_processUpdateFileInfoJob) m_processUpdateFileInfoJob->abort(); if (m_filesJob) m_filesJob->abort(); if (m_modIdResolver) m_modIdResolver->abort(); return InstanceCreationTask::abort(); } bool FlameCreationTask::updateInstance() { auto instance_list = APPLICATION->instances(); // FIXME: How to handle situations when there's more than one install already for a given modpack? BaseInstance* inst; if (auto original_id = originalInstanceID(); !original_id.isEmpty()) { inst = instance_list->getInstanceById(original_id); Q_ASSERT(inst); } else { inst = instance_list->getInstanceByManagedName(originalName()); if (!inst) { inst = instance_list->getInstanceById(originalName()); if (!inst) return false; } } QString index_path(FS::PathCombine(m_stagingPath, "manifest.json")); try { Flame::loadManifest(m_pack, index_path); } catch (const JSONValidationError& e) { setError(tr("Could not understand pack manifest:\n") + e.cause()); return false; } auto version_id = inst->getManagedPackVersionName(); auto version_str = !version_id.isEmpty() ? tr(" (version %1)").arg(version_id) : ""; if (shouldConfirmUpdate()) { auto should_update = askIfShouldUpdate(m_parent, version_str); if (should_update == ShouldUpdate::SkipUpdating) return false; if (should_update == ShouldUpdate::Cancel) { m_abort = true; return false; } } QDir old_inst_dir(inst->instanceRoot()); QString old_index_folder(FS::PathCombine(old_inst_dir.absolutePath(), "flame")); QString old_index_path(FS::PathCombine(old_index_folder, "manifest.json")); QFileInfo old_index_file(old_index_path); if (old_index_file.exists()) { Flame::Manifest old_pack; Flame::loadManifest(old_pack, old_index_path); auto& old_files = old_pack.files; auto& files = m_pack.files; // Remove repeated files, we don't need to download them! auto files_iterator = files.begin(); while (files_iterator != files.end()) { auto const& file = files_iterator; auto old_file = old_files.find(file.key()); if (old_file != old_files.end()) { // We found a match, but is it a different version? if (old_file->fileId == file->fileId) { qDebug() << "Removed file at" << file->targetFolder << "with id" << file->fileId << "from list of downloads"; old_files.remove(file.key()); files_iterator = files.erase(files_iterator); if (files_iterator != files.begin()) files_iterator--; } } files_iterator++; } QDir old_minecraft_dir(inst->gameRoot()); // We will remove all the previous overrides, to prevent duplicate files! // TODO: Currently 'overrides' will always override the stuff on update. How do we preserve unchanged overrides? // FIXME: We may want to do something about disabled mods. auto old_overrides = Override::readOverrides("overrides", old_index_folder); for (const auto& entry : old_overrides) { scheduleToDelete(m_parent, old_minecraft_dir, entry); } // Remove remaining old files (we need to do an API request to know which ids are which files...) QStringList fileIds; for (auto& file : old_files) { fileIds.append(QString::number(file.fileId)); } auto [job, raw_response] = api.getFiles(fileIds); QEventLoop loop; connect(job.get(), &Task::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] { // Parse the API response QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*raw_response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Flame files task at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *raw_response; return; } try { QJsonArray entries; if (fileIds.size() == 1) entries = { Json::requireObject(Json::requireObject(doc), "data") }; else entries = Json::requireArray(Json::requireObject(doc), "data"); for (auto entry : entries) { auto entry_obj = Json::requireObject(entry); Flame::File file; // We don't care about blocked mods, we just need local data to delete the file file.version = FlameMod::loadIndexedPackVersion(entry_obj); auto id = Json::requireInteger(entry_obj, "id"); old_files.insert(id, file); } } catch (Json::JsonException& e) { qCritical() << e.cause() << e.what(); } // Delete the files for (auto& file : old_files) { if (file.version.fileName.isEmpty() || file.targetFolder.isEmpty()) continue; QString relative_path(FS::PathCombine(file.targetFolder, file.version.fileName)); scheduleToDelete(m_parent, old_minecraft_dir, relative_path, true); } }); connect(job.get(), &Task::failed, this, [](QString reason) { qCritical() << "Failed to get files:" << reason; }); connect(job.get(), &Task::finished, &loop, &QEventLoop::quit); m_processUpdateFileInfoJob = job; job->start(); loop.exec(); m_processUpdateFileInfoJob = nullptr; } else { // We don't have an old index file, so we may duplicate stuff! auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."), tr("We couldn't find a suitable index file for the older version. This may cause some " "of the files to be duplicated. Do you want to continue?"), QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel); if (dialog->exec() == QDialog::DialogCode::Rejected) { m_abort = true; return false; } } setOverride(true, inst->id()); qDebug() << "Will override instance!"; m_instance = inst; // We let it go through the createInstance() stage, just with a couple modifications for updating return false; } QString FlameCreationTask::getVersionForLoader(QString uid, QString loaderType, QString loaderVersion, QString mcVersion) { if (loaderVersion == "recommended") { auto vlist = APPLICATION->metadataIndex()->get(uid); if (!vlist) { setError(tr("Failed to get local metadata index for %1").arg(uid)); return {}; } if (!vlist->isLoaded()) { QEventLoop loadVersionLoop; auto task = vlist->getLoadTask(); connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit); if (!task->isRunning()) task->start(); loadVersionLoop.exec(); } for (auto version : vlist->versions()) { // first recommended build we find, we use. if (!version->isRecommended()) continue; auto reqs = version->requiredSet(); // filter by minecraft version, if the loader depends on a certain version. // not all mod loaders depend on a given Minecraft version, so we won't do this // filtering for those loaders. if (loaderType == "forge" || loaderType == "neoforge") { auto iter = std::find_if(reqs.begin(), reqs.end(), [mcVersion](const Meta::Require& req) { return req.uid == "net.minecraft" && req.equalsVersion == mcVersion; }); if (iter == reqs.end()) continue; } return version->descriptor(); } setError(tr("Failed to find version for %1 loader").arg(loaderType)); return {}; } if (loaderVersion.isEmpty()) { emitFailed(tr("No loader version set for modpack!")); return {}; } return loaderVersion; } std::unique_ptr FlameCreationTask::createInstance() { QEventLoop loop; QString parent_folder(FS::PathCombine(m_stagingPath, "flame")); try { QString index_path(FS::PathCombine(m_stagingPath, "manifest.json")); if (!m_pack.is_loaded) Flame::loadManifest(m_pack, index_path); // Keep index file in case we need it some other time (like when changing versions) QString new_index_place(FS::PathCombine(parent_folder, "manifest.json")); FS::ensureFilePathExists(new_index_place); FS::move(index_path, new_index_place); } catch (const JSONValidationError& e) { setError(tr("Could not understand pack manifest:\n") + e.cause()); return nullptr; } if (!m_pack.overrides.isEmpty()) { QString overridePath = FS::PathCombine(m_stagingPath, m_pack.overrides); if (QFile::exists(overridePath)) { // Create a list of overrides in "overrides.txt" inside flame/ Override::createOverrides("overrides", parent_folder, overridePath); QString mcPath = FS::PathCombine(m_stagingPath, "minecraft"); if (!FS::move(overridePath, mcPath)) { setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides); return nullptr; } } else { logWarning( tr("The specified overrides folder (%1) is missing. Maybe the modpack was already used before?").arg(m_pack.overrides)); } } QString loaderType; QString loaderUid; QString loaderVersion; for (auto& loader : m_pack.minecraft.modLoaders) { auto id = loader.id; if (id.startsWith("neoforge-")) { id.remove("neoforge-"); if (id.startsWith("1.20.1-")) id.remove("1.20.1-"); // this is a mess for curseforge loaderType = "neoforge"; loaderUid = "net.neoforged"; } else if (id.startsWith("forge-")) { id.remove("forge-"); loaderType = "forge"; loaderUid = "net.minecraftforge"; } else if (id.startsWith("fabric-")) { id.remove("fabric-"); loaderType = "fabric"; loaderUid = "net.fabricmc.fabric-loader"; } else if (id.startsWith("quilt-")) { id.remove("quilt-"); loaderType = "quilt"; loaderUid = "org.quiltmc.quilt-loader"; } else { logWarning(tr("Unknown mod loader in manifest: %1").arg(id)); continue; } loaderVersion = id; } QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_unique(configPath); auto instance = std::make_unique(m_globalSettings, std::move(instanceSettings), m_stagingPath); auto mcVersion = m_pack.minecraft.version; // Hack to correct some 'special sauce'... if (mcVersion.endsWith('.')) { static const QRegularExpression s_regex("[.]+$"); mcVersion.remove(s_regex); logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack.")); } auto components = instance->getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", mcVersion, true); if (!loaderType.isEmpty()) { auto version = getVersionForLoader(loaderUid, loaderType, loaderVersion, mcVersion); if (version.isEmpty()) return nullptr; components->setComponentVersion(loaderUid, version); } if (m_instIcon != "default") { instance->setIconKey(m_instIcon); } else { if (m_pack.name.contains("Direwolf20")) { instance->setIconKey("steve"); } else if (m_pack.name.contains("FTB") || m_pack.name.contains("Feed The Beast")) { instance->setIconKey("ftb_logo"); } else { instance->setIconKey("flame"); } } int recommendedRAM = m_pack.minecraft.recommendedRAM; // only set memory if this is a fresh instance if (!m_instance && recommendedRAM > 0) { const uint64_t sysMiB = HardwareInfo::totalRamMiB(); const uint64_t max = sysMiB * 0.9; if (static_cast(recommendedRAM) > max) { logWarning(tr("The recommended memory of the modpack exceeds 90% of your system RAM—reducing it from %1 MiB to %2 MiB!") .arg(recommendedRAM) .arg(max)); recommendedRAM = max; } instance->settings()->set("OverrideMemory", true); instance->settings()->set("MaxMemAlloc", recommendedRAM); } QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods"); QFileInfo jarmodsInfo(jarmodsPath); if (jarmodsInfo.isDir()) { // install all the jar mods qDebug() << "Found jarmods:"; QDir jarmodsDir(jarmodsPath); QStringList jarMods; for (const auto& info : jarmodsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) { qDebug() << info.fileName(); jarMods.push_back(info.absoluteFilePath()); } auto profile = instance->getPackProfile(); profile->installJarMods(jarMods); // nuke the original files FS::deletePath(jarmodsPath); } // Don't add managed info to packs without an ID (most likely imported from ZIP) if (!m_managedId.isEmpty()) instance->setManagedPack("flame", m_managedId, m_pack.name, m_managedVersionId, m_pack.version); else instance->setManagedPack("flame", "", name(), "", ""); instance->setName(name()); m_modIdResolver.reset(new Flame::FileResolvingTask(m_pack)); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); }); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) { m_modIdResolver.reset(); setError(tr("Unable to resolve mod IDs:\n") + reason); loop.quit(); }); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails); m_modIdResolver->start(); loop.exec(); bool did_succeed = getError().isEmpty(); // Update information of the already installed instance, if any. if (m_instance && did_succeed) { setAbortable(false); auto inst = m_instance.value(); inst->copyManagedPack(*instance); } if (did_succeed) { return instance; } return nullptr; } void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) { auto results = m_modIdResolver->getResults().files; QStringList optionalFiles; for (auto& result : results) { if (!result.required) { optionalFiles << FS::PathCombine(result.targetFolder, result.version.fileName); } } if (!optionalFiles.empty()) { OptionalModDialog optionalModDialog(m_parent, optionalFiles); if (optionalModDialog.exec() == QDialog::Rejected) { emitAborted(); loop.quit(); return; } m_selectedOptionalMods = optionalModDialog.getResult(); } // first check for blocked mods QList blocked_mods; auto anyBlocked = false; for (const auto& result : results.values()) { if (result.resourceType != ModPlatform::ResourceType::Mod) { m_otherResources.append(std::make_pair(result.version.fileName, result.targetFolder)); } // skip optional mods that were not selected if (result.version.downloadUrl.isEmpty()) { BlockedMod blocked_mod; blocked_mod.name = result.version.fileName; blocked_mod.websiteUrl = QString("%1/download/%2").arg(result.pack.websiteUrl, QString::number(result.fileId)); blocked_mod.hash = result.version.hash; blocked_mod.matched = false; blocked_mod.localPath = ""; blocked_mod.targetFolder = result.targetFolder; auto fileName = result.version.fileName; fileName = FS::RemoveInvalidPathChars(fileName); auto relpath = FS::PathCombine(result.targetFolder, fileName); blocked_mod.disabled = !result.required && !m_selectedOptionalMods.contains(relpath); blocked_mods.append(blocked_mod); anyBlocked = true; } } if (anyBlocked) { qWarning() << "Blocked mods found, displaying mod list"; BlockedModsDialog message_dialog(m_parent, tr("Blocked mods found"), tr("The following files are not available for download in third party launchers.
" "You will need to manually download them and add them to the instance."), blocked_mods); message_dialog.setModal(true); if (message_dialog.exec()) { qDebug() << "Post dialog blocked mods list:" << blocked_mods; copyBlockedMods(blocked_mods); setupDownloadJob(loop); } else { m_modIdResolver.reset(); setError("Canceled"); loop.quit(); } } else { setupDownloadJob(loop); } } void FlameCreationTask::setupDownloadJob(QEventLoop& loop) { m_filesJob.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network())); auto results = m_modIdResolver->getResults().files; for (const auto& result : results) { auto fileName = result.version.fileName; fileName = FS::RemoveInvalidPathChars(fileName); auto relpath = FS::PathCombine(result.targetFolder, fileName); if (!result.required && !m_selectedOptionalMods.contains(relpath)) { relpath += ".disabled"; } relpath = FS::PathCombine("minecraft", relpath); auto path = FS::PathCombine(m_stagingPath, relpath); if (!result.version.downloadUrl.isEmpty()) { qDebug() << "Will download" << result.version.downloadUrl << "to" << path; auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path); m_filesJob->addNetAction(dl); } } connect(m_filesJob.get(), &NetJob::finished, this, [this, &loop]() { m_filesJob.reset(); validateOtherResources(loop); }); connect(m_filesJob.get(), &NetJob::failed, [this](QString reason) { m_filesJob.reset(); setError(reason); }); connect(m_filesJob.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); connect(m_filesJob.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress); setStatus(tr("Downloading mods...")); m_filesJob->start(); } /// @brief copy the matched blocked mods to the instance staging area /// @param blocked_mods list of the blocked mods and their matched paths void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) { setStatus(tr("Copying Blocked Mods...")); setAbortable(false); int i = 0; int total = blocked_mods.length(); setProgress(i, total); for (auto const& mod : blocked_mods) { if (!mod.matched) { qDebug() << mod.name << "was not matched to a local file, skipping copy"; continue; } auto destPath = FS::PathCombine(m_stagingPath, "minecraft", mod.targetFolder, mod.name); if (mod.disabled) destPath += ".disabled"; setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); qDebug() << "Will try to copy" << mod.localPath << "to" << destPath; if (mod.move) { if (!FS::move(mod.localPath, destPath)) { qDebug() << "Move of" << mod.localPath << "to" << destPath << "Failed"; } } else { if (!FS::copy(mod.localPath, destPath)()) { qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed"; } } i++; setProgress(i, total); } setAbortable(true); } void FlameCreationTask::validateOtherResources(QEventLoop& loop) { qDebug() << "Validating whether other resources are in the right place"; QStringList zipMods; for (auto [fileName, targetFolder] : m_otherResources) { qDebug() << "Checking" << fileName << "..."; auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); /// @brief check the target and move the the file /// @return path where file can now be found auto validatePath = [&localPath, this](QString fileName, QString targetFolder, QString realTarget) { if (targetFolder != realTarget) { qDebug() << "Target folder of" << fileName << "is incorrect, it belongs in" << realTarget; auto destPath = FS::PathCombine(m_stagingPath, "minecraft", realTarget, fileName); qDebug() << "Moving" << localPath << "to" << destPath; if (FS::move(localPath, destPath)) { return destPath; } } else { qDebug() << "Target folder of" << fileName << "is correct at" << targetFolder; } return localPath; }; auto installWorld = [this](QString worldPath) { qDebug() << "Installing World from" << worldPath; QFileInfo worldFileInfo(worldPath); World w(worldFileInfo); if (!w.isValid()) { qDebug() << "World at" << worldPath << "is not valid, skipping install."; } else { w.install(FS::PathCombine(m_stagingPath, "minecraft", "saves")); } }; QFileInfo localFileInfo(localPath); auto type = ResourceUtils::identify(localFileInfo); QString worldPath; switch (type) { case ModPlatform::ResourceType::Mod: validatePath(fileName, targetFolder, "mods"); zipMods.push_back(fileName); break; case ModPlatform::ResourceType::ResourcePack: validatePath(fileName, targetFolder, "resourcepacks"); break; case ModPlatform::ResourceType::TexturePack: validatePath(fileName, targetFolder, "texturepacks"); break; case ModPlatform::ResourceType::DataPack: validatePath(fileName, targetFolder, "datapacks"); break; case ModPlatform::ResourceType::ShaderPack: // in theory flame API can't do this but who knows, that *may* change ? // better to handle it if it *does* occur in the future validatePath(fileName, targetFolder, "shaderpacks"); break; case ModPlatform::ResourceType::World: worldPath = validatePath(fileName, targetFolder, "saves"); installWorld(worldPath); break; case ModPlatform::ResourceType::Unknown: /* fallthrough */ default: qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is."; break; } } // TODO make this work with other sorts of resource auto task = makeShared("CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); auto results = m_modIdResolver->getResults().files; auto folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index"); for (auto file : results) { if (file.targetFolder != "mods" || (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))) { continue; } task->addTask(makeShared(folder, file.pack, file.version)); } connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); m_processUpdateFileInfoJob = task; task->start(); } PrismLauncher-11.0.3/launcher/modplatform/flame/FlameAPI.cpp0000644000175100017510000002426015224505336023262 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "FlameAPI.h" #include #include #include "BuildConfig.h" #include "FlameModIndex.h" #include "Application.h" #include "Json.h" #include "modplatform/ModIndex.h" #include "net/ApiDownload.h" #include "net/ApiUpload.h" #include "net/NetJob.h" std::pair FlameAPI::matchFingerprints(const QList& fingerprints) { auto netJob = makeShared(QString("Flame::MatchFingerprints"), APPLICATION->network()); QJsonObject body_obj; QJsonArray fingerprints_arr; for (auto& fp : fingerprints) { fingerprints_arr.append(QString("%1").arg(fp)); } body_obj["fingerprints"] = fingerprints_arr; QJsonDocument body(body_obj); auto body_raw = body.toJson(); auto [action, response] = Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/fingerprints"), body_raw); netJob->addNetAction(action); return { netJob, response }; } QString FlameAPI::getModFileChangelog(int modId, int fileId) { QEventLoop lock; QString changelog; auto netJob = makeShared(QString("Flame::FileChangelog"), APPLICATION->network()); auto [action, response] = Net::ApiDownload::makeByteArray( QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2/changelog") .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId)))); netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &changelog] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Flame::FileChangelog at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; netJob->failed(parse_error.errorString()); return; } changelog = doc.object()["data"].toString(); }); QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); }); netJob->start(); lock.exec(); return changelog; } QString FlameAPI::getModDescription(int modId) { QEventLoop lock; QString description; auto netJob = makeShared(QString("Flame::ModDescription"), APPLICATION->network()); auto [action, response] = Net::ApiDownload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/description").arg(QString::number(modId))); netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &description] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Flame::ModDescription at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; netJob->failed(parse_error.errorString()); return; } description = doc.object()["data"].toString(); }); QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); }); netJob->start(); lock.exec(); return description; } std::pair FlameAPI::getProjects(QStringList addonIds) const { auto netJob = makeShared(QString("Flame::GetProjects"), APPLICATION->network()); QJsonObject body_obj; QJsonArray addons_arr; for (auto& addonId : addonIds) { addons_arr.append(addonId); } body_obj["modIds"] = addons_arr; QJsonDocument body(body_obj); auto body_raw = body.toJson(); auto [action, response] = Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods"), body_raw); netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return { netJob, response }; } std::pair FlameAPI::getFiles(const QStringList& fileIds) const { auto netJob = makeShared(QString("Flame::GetFiles"), APPLICATION->network()); QJsonObject body_obj; QJsonArray files_arr; for (auto& fileId : fileIds) { files_arr.append(fileId); } body_obj["fileIds"] = files_arr; QJsonDocument body(body_obj); auto body_raw = body.toJson(); auto [action, response] = Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods/files"), body_raw); netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return { netJob, response }; } std::pair FlameAPI::getFile(const QString& addonId, const QString& fileId) const { auto netJob = makeShared(QString("Flame::GetFile"), APPLICATION->network()); auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2").arg(addonId, fileId))); netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::failed, [addonId, fileId] { qDebug() << "Flame API file failure" << addonId << fileId; }); return { netJob, response }; } QList FlameAPI::getSortingMethods() const { // https://docs.curseforge.com/?python#tocS_ModsSearchSortField return { { 1, "Featured", QObject::tr("Sort by Featured") }, { 2, "Popularity", QObject::tr("Sort by Popularity") }, { 3, "LastUpdated", QObject::tr("Sort by Last Updated") }, { 4, "Name", QObject::tr("Sort by Name") }, { 5, "Author", QObject::tr("Sort by Author") }, { 6, "TotalDownloads", QObject::tr("Sort by Downloads") }, { 7, "Category", QObject::tr("Sort by Category") }, { 8, "GameVersion", QObject::tr("Sort by Game Version") } }; } std::pair FlameAPI::getCategories(ModPlatform::ResourceType type) { auto netJob = makeShared(QString("Flame::GetCategories"), APPLICATION->network()); auto [action, response] = Net::ApiDownload::makeByteArray( QUrl(QString(BuildConfig.FLAME_BASE_URL + "/categories?gameId=432&classId=%1").arg(getClassId(type)))); netJob->addNetAction(action); QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; }); return { netJob, response }; } std::pair FlameAPI::getModCategories() { return getCategories(ModPlatform::ResourceType::Mod); } QList FlameAPI::loadModCategories(const QByteArray& response) { QList categories; QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from categories at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return categories; } try { auto obj = Json::requireObject(doc); auto arr = Json::requireArray(obj, "data"); for (auto val : arr) { auto cat = Json::requireObject(val); auto id = Json::requireInteger(cat, "id"); auto name = Json::requireString(cat, "name"); categories.push_back({ name, QString::number(id) }); } } catch (Json::JsonException& e) { qCritical() << "Failed to parse response from a version request."; qCritical() << e.what(); qDebug() << doc; } return categories; }; std::optional FlameAPI::getLatestVersion(QList versions, QList instanceLoaders, ModPlatform::ModLoaderTypes modLoaders, bool checkLoaders) { static const auto noLoader = ModPlatform::ModLoaderType(0); if (!checkLoaders) { std::optional ver; for (auto file_tmp : versions) { if (!ver.has_value() || file_tmp.date > ver->date) { ver = file_tmp; } } return ver; } QHash bestMatch; auto checkVersion = [&bestMatch](const ModPlatform::IndexedVersion& version, const ModPlatform::ModLoaderType& loader) { if (bestMatch.contains(loader)) { auto best = bestMatch.value(loader); if (version.date > best.date) { bestMatch[loader] = version; } } else { bestMatch[loader] = version; } }; for (auto file_tmp : versions) { auto loaders = ModPlatform::modLoaderTypesToList(file_tmp.loaders); if (loaders.isEmpty()) { checkVersion(file_tmp, noLoader); } else { for (auto loader : loaders) { checkVersion(file_tmp, loader); } } } // 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(noLoader); // add a fallback in case the versions do not define a loader for (auto loader : currentLoaders) { if (bestMatch.contains(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 {}; } PrismLauncher-11.0.3/launcher/modplatform/flame/FlameAPI.h0000644000175100017510000001717615224505336022737 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include "BuildConfig.h" #include "Json.h" #include "Version.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "modplatform/flame/FlameModIndex.h" class FlameAPI : public ResourceAPI { public: QString getModFileChangelog(int modId, int fileId); QString getModDescription(int modId); std::optional getLatestVersion(QList versions, QList instanceLoaders, ModPlatform::ModLoaderTypes fallback, bool checkLoaders); std::pair getProjects(QStringList addonIds) const override; std::pair matchFingerprints(const QList& fingerprints); std::pair getFiles(const QStringList& fileIds) const; std::pair getFile(const QString& addonId, const QString& fileId) const; static std::pair getCategories(ModPlatform::ResourceType type); static std::pair getModCategories(); static QList loadModCategories(const QByteArray& response); QList getSortingMethods() const override; static inline bool validateModLoaders(ModPlatform::ModLoaderTypes loaders) { return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt); } private: static int getClassId(ModPlatform::ResourceType type) { switch (type) { default: case ModPlatform::ResourceType::Mod: return 6; case ModPlatform::ResourceType::ResourcePack: return 12; case ModPlatform::ResourceType::ShaderPack: return 6552; case ModPlatform::ResourceType::Modpack: return 4471; case ModPlatform::ResourceType::DataPack: return 6945; } } static int getMappedModLoader(ModPlatform::ModLoaderType loaders) { // https://docs.curseforge.com/?http#tocS_ModLoaderType switch (loaders) { case ModPlatform::Forge: return 1; case ModPlatform::Cauldron: return 2; case ModPlatform::LiteLoader: return 3; case ModPlatform::Fabric: return 4; case ModPlatform::Quilt: return 5; case ModPlatform::NeoForge: return 6; case ModPlatform::DataPack: case ModPlatform::Babric: case ModPlatform::BTA: case ModPlatform::LegacyFabric: case ModPlatform::Ornithe: case ModPlatform::Rift: break; // not supported } return 0; } static const QStringList getModLoaderStrings(const ModPlatform::ModLoaderTypes types) { QStringList l; for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt }) { if (types & loader) { l << QString::number(getMappedModLoader(loader)); } } return l; } static const QString getModLoaderFilters(ModPlatform::ModLoaderTypes types) { return "[" + getModLoaderStrings(types).join(',') + "]"; } public: std::optional getSearchURL(const SearchArgs& args) const override { QStringList get_arguments; get_arguments.append(QString("classId=%1").arg(getClassId(args.type))); get_arguments.append(QString("index=%1").arg(args.offset)); get_arguments.append("pageSize=25"); if (args.search.has_value()) get_arguments.append(QString("searchFilter=%1").arg(args.search.value())); if (args.sorting.has_value()) get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index)); get_arguments.append("sortOrder=desc"); if (args.loaders.has_value()) { ModPlatform::ModLoaderTypes loaders = args.loaders.value(); loaders &= ~static_cast(ModPlatform::ModLoaderType::DataPack); if (loaders != 0) get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(loaders))); } if (args.categoryIds.has_value() && !args.categoryIds->empty()) get_arguments.append(QString("categoryIds=[%1]").arg(args.categoryIds->join(","))); if (args.versions.has_value() && !args.versions.value().empty()) get_arguments.append(QString("gameVersion=%1").arg(args.versions.value().front().toString())); return BuildConfig.FLAME_BASE_URL + "/mods/search?gameId=432&" + get_arguments.join('&'); } std::optional getVersionsURL(const VersionSearchArgs& args) const override { auto addonId = args.pack->addonId.toString(); QString url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000").arg(addonId); if (args.mcVersions.has_value()) url += QString("&gameVersion=%1").arg(args.mcVersions.value().front().toString()); if (args.loaders.has_value() && args.loaders.value() != ModPlatform::ModLoaderType::DataPack && ModPlatform::hasSingleModLoaderSelected(args.loaders.value())) { int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loaders.value()))); url += QString("&modLoaderType=%1").arg(mappedModLoader); } return url; } QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.object()["data"].toArray(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { FlameMod::loadIndexedPack(m, obj); } ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType resourceType) const override { auto arr = FlameMod::loadIndexedPackVersion(obj); if (resourceType != ModPlatform::ResourceType::TexturePack) { return arr; } // FIXME: Client-side version filtering. This won't take into account any user-selected filtering. const auto& mc_versions = arr.mcVersion; if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(), [](const auto& mc_version) { return Version(mc_version) <= Version("1.6"); })) { return arr; } return {}; }; void loadExtraPackInfo(ModPlatform::IndexedPack& m, [[maybe_unused]] QJsonObject&) const override { FlameMod::loadBody(m); } private: std::optional getInfoURL(const QString& id) const override { return QString(BuildConfig.FLAME_BASE_URL + "/mods/%1").arg(id); } std::optional getDependencyURL(const DependencySearchArgs& args) const override { auto addonId = args.dependency.addonId.toString(); auto url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000&gameVersion=%2").arg(addonId, args.mcVersion.toString()); if (args.loader && ModPlatform::hasSingleModLoaderSelected(args.loader)) { int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loader))); url += QString("&modLoaderType=%1").arg(mappedModLoader); } return url; } }; PrismLauncher-11.0.3/launcher/modplatform/flame/FlameCheckUpdate.cpp0000644000175100017510000001761215224505336025034 0ustar runnerrunner#include "FlameCheckUpdate.h" #include "Application.h" #include "FlameAPI.h" #include "FlameModIndex.h" #include #include #include "Json.h" #include "QObjectPtr.h" #include "ResourceDownloadTask.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "net/ApiDownload.h" #include "net/NetJob.h" #include "tasks/Task.h" bool FlameCheckUpdate::abort() { bool result = false; if (m_task && m_task->canAbort()) { result = m_task->abort(); } Task::abort(); return result; } /* Check for update: * - Get latest version available * - Compare hash of the latest version with the current hash * - If equal, no updates, else, there's updates, so add to the list * */ void FlameCheckUpdate::executeTask() { setStatus(tr("Preparing resources for CurseForge...")); auto* netJob = new NetJob("Get latest versions", APPLICATION->network()); connect(netJob, &Task::finished, this, &FlameCheckUpdate::collectBlockedMods); connect(netJob, &Task::progress, this, &FlameCheckUpdate::setProgress); connect(netJob, &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress); connect(netJob, &Task::details, this, &FlameCheckUpdate::setDetails); for (auto* resource : m_resources) { auto project = std::make_shared(); project->addonId = resource->metadata()->project_id.toString(); auto versionsUrlOptional = FlameAPI().getVersionsURL({ .pack = project, .mcVersions = m_gameVersions }); if (!versionsUrlOptional.has_value()) { continue; } auto [task, response] = Net::ApiDownload::makeByteArray(versionsUrlOptional.value()); connect(task.get(), &Task::succeeded, this, [this, resource, response] { getLatestVersionCallback(resource, response); }); netJob->addNetAction(task); } m_task.reset(netJob); m_task->start(); } void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, QByteArray* response) { QJsonParseError parseError{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError); if (parseError.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from latest mod version at" << parseError.offset << "reason:" << parseError.errorString(); qWarning() << *response; return; } // Fake pack with the necessary info to pass to the download task :) auto pack = std::make_shared(); pack->name = resource->name(); pack->slug = resource->metadata()->slug; pack->addonId = resource->metadata()->project_id; pack->provider = ModPlatform::ResourceProvider::FLAME; try { auto obj = Json::requireObject(doc); auto arr = Json::requireArray(obj, "data"); FlameMod::loadIndexedPackVersions(*pack.get(), arr); } catch (Json::JsonException& e) { qCritical() << "Failed to parse response from a version request."; qCritical() << e.what(); qDebug() << doc; } auto latestVer = FlameAPI().getLatestVersion(pack->versions, m_loadersList, resource->metadata()->loaders, !m_loadersList.isEmpty()); setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name())); if (!latestVer.has_value() || !latestVer->addonId.isValid()) { QString reason; if (dynamic_cast(resource) != nullptr) { reason = tr("No valid version found for this resource. It's probably unavailable for the current game " "version / mod loader."); } else { reason = tr("No valid version found for this resource. It's probably unavailable for the current game version."); } emit checkFailed(resource, reason); return; } if (latestVer->downloadUrl.isEmpty() && latestVer->fileId != resource->metadata()->file_id) { m_blocked[resource] = latestVer->fileId.toString(); return; } if (!latestVer->hash.isEmpty() && (resource->metadata()->hash != latestVer->hash || resource->status() == ResourceStatus::NOT_INSTALLED)) { auto oldVersion = resource->metadata()->version_number; if (oldVersion.isEmpty()) { if (resource->status() == ResourceStatus::NOT_INSTALLED) { oldVersion = tr("Not installed"); } else { oldVersion = tr("Unknown"); } } auto downloadTask = makeShared(pack, latestVer.value(), m_resourceModel, true, "update"); m_updates.emplace_back(pack->name, resource->metadata()->hash, oldVersion, latestVer->version, latestVer->version_type, FlameAPI().getModFileChangelog(latestVer->addonId.toInt(), latestVer->fileId.toInt()), ModPlatform::ResourceProvider::FLAME, downloadTask, resource->enabled()); } m_deps.append(std::make_shared(pack, latestVer.value())); } void FlameCheckUpdate::collectBlockedMods() { QStringList addonIds; QHash quickSearch; for (const auto& resource : m_blocked.keys()) { auto addonId = resource->metadata()->project_id.toString(); addonIds.append(addonId); quickSearch[addonId] = resource; } Task::Ptr projTask; QByteArray* response = nullptr; if (addonIds.isEmpty()) { emitSucceeded(); return; } if (addonIds.size() == 1) { std::tie(projTask, response) = FlameAPI().getProject(*addonIds.begin()); } else { std::tie(projTask, response) = FlameAPI().getProjects(addonIds); } connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds, quickSearch] { QJsonParseError parseError{}; auto doc = QJsonDocument::fromJson(*response, &parseError); if (parseError.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Flame projects task at" << parseError.offset << "reason:" << parseError.errorString(); qWarning() << *response; return; } try { QJsonArray entries; if (addonIds.size() == 1) { entries = { Json::requireObject(Json::requireObject(doc), "data") }; } else { entries = Json::requireArray(Json::requireObject(doc), "data"); } for (auto entry : entries) { auto entryObj = Json::requireObject(entry); auto id = QString::number(Json::requireInteger(entryObj, "id")); auto* resource = quickSearch.find(id).value(); ModPlatform::IndexedPack pack; try { setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name())); FlameMod::loadIndexedPack(pack, entryObj); auto recoverUrl = QString("%1/download/%2").arg(pack.websiteUrl, m_blocked[resource]); emit checkFailed(resource, tr("Resource has a new update available, but is not downloadable using CurseForge."), recoverUrl); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; } } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } }); connect(projTask.get(), &Task::finished, this, &FlameCheckUpdate::emitSucceeded); // do not care much about error connect(projTask.get(), &Task::progress, this, &FlameCheckUpdate::setProgress); connect(projTask.get(), &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress); connect(projTask.get(), &Task::details, this, &FlameCheckUpdate::setDetails); m_task.reset(projTask); m_task->start(); } PrismLauncher-11.0.3/launcher/modplatform/flame/PackManifest.cpp0000644000175100017510000000456615224505336024260 0ustar runnerrunner#include "PackManifest.h" #include "Json.h" static void loadFileV1(Flame::File& f, QJsonObject& file) { f.projectId = Json::requireInteger(file, "projectID"); f.fileId = Json::requireInteger(file, "fileID"); f.required = file["required"].toBool(true); } static void loadModloaderV1(Flame::Modloader& m, QJsonObject& modLoader) { m.id = Json::requireString(modLoader, "id"); m.primary = modLoader["primary"].toBool(); } static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft) { m.version = Json::requireString(minecraft, "version"); // extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack // intended use is likely hardcoded in the 'Flame' client, the manifest says nothing m.libraries = minecraft["libraries"].toString(); auto arr = minecraft["modLoaders"].toArray(); for (QJsonValueRef item : arr) { auto obj = Json::requireObject(item); Flame::Modloader loader; loadModloaderV1(loader, obj); m.modLoaders.append(loader); } m.recommendedRAM = minecraft["recommendedRam"].toInt(); } static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest) { auto mc = Json::requireObject(manifest, "minecraft"); loadMinecraftV1(pack.minecraft, mc); pack.name = manifest["name"].toString("Unnamed"); pack.version = manifest["version"].toString(); pack.author = manifest["author"].toString("Anonymous"); auto arr = manifest["files"].toArray(); for (auto item : arr) { auto obj = Json::requireObject(item); Flame::File file; loadFileV1(file, obj); Q_ASSERT(file.projectId != 0); pack.files.insert(file.fileId, file); } pack.overrides = manifest["overrides"].toString("overrides"); pack.is_loaded = true; } void Flame::loadManifest(Flame::Manifest& m, const QString& filepath) { auto doc = Json::requireDocument(filepath); auto obj = Json::requireObject(doc); m.manifestType = Json::requireString(obj, "manifestType"); if (m.manifestType != "minecraftModpack") { throw JSONValidationError("Not a modpack manifest!"); } m.manifestVersion = Json::requireInteger(obj, "manifestVersion"); if (m.manifestVersion != 1) { throw JSONValidationError(QString("Unknown manifest version (%1)").arg(m.manifestVersion)); } loadManifestV1(m, obj); } PrismLauncher-11.0.3/launcher/modplatform/flame/FlameCheckUpdate.h0000644000175100017510000000140315224505336024470 0ustar runnerrunner#pragma once #include "modplatform/CheckUpdateTask.h" class FlameCheckUpdate : public CheckUpdateTask { Q_OBJECT public: FlameCheckUpdate(QList& resources, std::vector& mcVersions, QList loadersList, ResourceFolderModel* resourceModel) : CheckUpdateTask(resources, mcVersions, std::move(loadersList), resourceModel) {} public slots: bool abort() override; protected slots: void executeTask() override; private slots: void getLatestVersionCallback(Resource* resource, QByteArray* response); void collectBlockedMods(); private: Task::Ptr m_task = nullptr; QHash m_blocked; }; PrismLauncher-11.0.3/launcher/modplatform/flame/FlameModIndex.cpp0000644000175100017510000001656115224505336024365 0ustar runnerrunner#include "FlameModIndex.h" #include "FileSystem.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" static FlameAPI api; void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); pack.provider = ModPlatform::ResourceProvider::FLAME; pack.name = Json::requireString(obj, "name"); pack.slug = Json::requireString(obj, "slug"); pack.websiteUrl = obj["links"].toObject()["websiteUrl"].toString(""); pack.description = obj["summary"].toString(""); QJsonObject logo = obj["logo"].toObject(); pack.logoName = logo["title"].toString(); pack.logoUrl = logo["thumbnailUrl"].toString(); if (pack.logoUrl.isEmpty()) { pack.logoUrl = logo["url"].toString(); } auto authors = obj["authors"].toArray(); if (!authors.isEmpty()) { pack.authors.clear(); for (auto authorIter : authors) { auto author = Json::requireObject(authorIter); ModPlatform::ModpackAuthor packAuthor; packAuthor.name = Json::requireString(author, "name"); packAuthor.url = Json::requireString(author, "url"); pack.authors.append(packAuthor); } } pack.extraDataLoaded = false; loadURLs(pack, obj); } void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) { auto links_obj = obj["links"].toObject(); pack.extraData.issuesUrl = links_obj["issuesUrl"].toString(); if (pack.extraData.issuesUrl.endsWith('/')) pack.extraData.issuesUrl.chop(1); pack.extraData.sourceUrl = links_obj["sourceUrl"].toString(); if (pack.extraData.sourceUrl.endsWith('/')) pack.extraData.sourceUrl.chop(1); pack.extraData.wikiUrl = links_obj["wikiUrl"].toString(); if (pack.extraData.wikiUrl.endsWith('/')) pack.extraData.wikiUrl.chop(1); if (!pack.extraData.body.isEmpty()) pack.extraDataLoaded = true; } void FlameMod::loadBody(ModPlatform::IndexedPack& pack) { pack.extraData.body = api.getModDescription(pack.addonId.toInt()); if (!pack.extraData.issuesUrl.isEmpty() || !pack.extraData.sourceUrl.isEmpty() || !pack.extraData.wikiUrl.isEmpty()) pack.extraDataLoaded = true; } static QString enumToString(int hash_algorithm) { switch (hash_algorithm) { default: case 1: return "sha1"; case 2: return "md5"; } } void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr) { QList unsortedVersions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj); if (!file.addonId.isValid()) file.addonId = pack.addonId; if (file.fileId.isValid()) // Heuristic to check if the returned value is valid unsortedVersions.append(file); } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { // dates are in RFC 3339 format return a.date > b.date; }; std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); pack.versions = unsortedVersions; pack.versionsLoaded = true; } auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> ModPlatform::IndexedVersion { auto versionArray = Json::requireArray(obj, "gameVersions"); ModPlatform::IndexedVersion file; for (auto mcVer : versionArray) { auto str = mcVer.toString(); if (str.contains('.')) file.mcVersion.append(str); file.side = ModPlatform::Side::NoSide; if (auto loader = str.toLower(); loader == "neoforge") file.loaders |= ModPlatform::NeoForge; else if (loader == "forge") file.loaders |= ModPlatform::Forge; else if (loader == "cauldron") file.loaders |= ModPlatform::Cauldron; else if (loader == "liteloader") file.loaders |= ModPlatform::LiteLoader; else if (loader == "fabric") file.loaders |= ModPlatform::Fabric; else if (loader == "quilt") file.loaders |= ModPlatform::Quilt; else if (loader == "server" || loader == "client") { if (file.side == ModPlatform::Side::NoSide) file.side = ModPlatform::SideUtils::fromString(loader); else if (file.side != ModPlatform::SideUtils::fromString(loader)) file.side = ModPlatform::Side::UniversalSide; } } file.addonId = Json::requireInteger(obj, "modId"); file.fileId = Json::requireInteger(obj, "id"); file.date = Json::requireString(obj, "fileDate"); file.version = Json::requireString(obj, "displayName"); file.downloadUrl = obj["downloadUrl"].toString(); file.fileName = Json::requireString(obj, "fileName"); file.fileName = FS::RemoveInvalidPathChars(file.fileName); ModPlatform::IndexedVersionType ver_type; switch (Json::requireInteger(obj, "releaseType")) { case 1: ver_type = ModPlatform::IndexedVersionType::Release; break; case 2: ver_type = ModPlatform::IndexedVersionType::Beta; break; case 3: ver_type = ModPlatform::IndexedVersionType::Alpha; break; default: ver_type = ModPlatform::IndexedVersionType::Unknown; break; } file.version_type = ver_type; auto hash_list = obj["hashes"].toArray(); for (auto h : hash_list) { auto hash_entry = h.toObject(); auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::FLAME); auto hash_algo = enumToString(hash_entry["algo"].toInt(1)); if (hash_types.contains(hash_algo)) { file.hash = Json::requireString(hash_entry, "value"); file.hash_type = hash_algo; break; } } auto dependencies = obj["dependencies"].toArray(); for (auto d : dependencies) { auto dep = d.toObject(); ModPlatform::Dependency dependency; dependency.addonId = Json::requireInteger(dep, "modId"); switch (Json::requireInteger(dep, "relationType")) { case 1: // EmbeddedLibrary dependency.type = ModPlatform::DependencyType::EMBEDDED; break; case 2: // OptionalDependency dependency.type = ModPlatform::DependencyType::OPTIONAL; break; case 3: // RequiredDependency dependency.type = ModPlatform::DependencyType::REQUIRED; break; case 4: // Tool dependency.type = ModPlatform::DependencyType::TOOL; break; case 5: // Incompatible dependency.type = ModPlatform::DependencyType::INCOMPATIBLE; break; case 6: // Include dependency.type = ModPlatform::DependencyType::INCLUDE; break; default: dependency.type = ModPlatform::DependencyType::UNKNOWN; break; } file.dependencies.append(dependency); } if (load_changelog) file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt()); return file; } PrismLauncher-11.0.3/launcher/modplatform/flame/FlamePackExportTask.cpp0000644000175100017510000004050215224505336025551 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "FlamePackExportTask.h" #include #include #include #include #include #include #include #include #include #include "Application.h" #include "Json.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/ModFolderModel.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/Task.h" #include "archive/ExportToZipTask.h" const QString FlamePackExportTask::TEMPLATE = "
  • {name}{authors}
  • \n"; const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" }); FlamePackExportTask::FlamePackExportTask(FlamePackExportOptions&& options) : m_options(std::move(options)), m_gameRoot(m_options.instance->gameRoot()) {} void FlamePackExportTask::executeTask() { setStatus(tr("Searching for files...")); setProgress(0, 5); collectFiles(); } bool FlamePackExportTask::abort() { if (task) { task->abort(); return true; } return false; } void FlamePackExportTask::collectFiles() { setAbortable(false); QCoreApplication::processEvents(); m_files.clear(); if (!MMCZip::collectFileListRecursively(m_options.instance->gameRoot(), nullptr, &m_files, m_options.filter)) { emitFailed(tr("Could not search for files")); return; } pendingHashes.clear(); resolvedFiles.clear(); m_options.instance->loaderModList()->update(); connect(m_options.instance->loaderModList(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes); } void FlamePackExportTask::collectHashes() { setAbortable(true); setStatus(tr("Finding file hashes...")); setProgress(1, 5); auto allMods = m_options.instance->loaderModList()->allMods(); ConcurrentTask::Ptr hashingTask(new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); task.reset(hashingTask); for (const QFileInfo& file : m_files) { const QString relative = m_gameRoot.relativeFilePath(file.absoluteFilePath()); // require sensible file types if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) { return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); })) continue; if (relative.startsWith("resourcepacks/") && (relative.endsWith(".zip") || relative.endsWith(".zip.disabled"))) { // is resourcepack auto hashTask = Hashing::createHasher(file.absoluteFilePath(), ModPlatform::ResourceProvider::FLAME); connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, relative, file](QString hash) { if (m_state == Task::State::Running) { pendingHashes.insert(hash, { relative, file.absoluteFilePath(), relative.endsWith(".zip") }); } }); connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); hashingTask->addTask(hashTask); continue; } if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; }); modIter != allMods.end()) { const Mod* mod = *modIter; if (!mod || mod->type() == ResourceType::FOLDER) { continue; } if (mod->metadata() && mod->metadata()->provider == ModPlatform::ResourceProvider::FLAME) { resolvedFiles.insert(mod->fileinfo().absoluteFilePath(), { mod->metadata()->project_id.toInt(), mod->metadata()->file_id.toInt(), mod->enabled(), true, mod->metadata()->name, mod->metadata()->slug, mod->authors().join(", ") }); continue; } auto hashTask = Hashing::createHasher(mod->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::FLAME); connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { if (m_state == Task::State::Running) { pendingHashes.insert(hash, { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true }); } }); connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); hashingTask->addTask(hashTask); } } auto progressStep = std::make_shared(); connect(hashingTask.get(), &Task::finished, this, [this, progressStep] { progressStep->state = TaskStepState::Succeeded; stepProgress(*progressStep); }); connect(hashingTask.get(), &Task::succeeded, this, &FlamePackExportTask::makeApiRequest); connect(hashingTask.get(), &Task::failed, this, [this, progressStep](QString reason) { progressStep->state = TaskStepState::Failed; stepProgress(*progressStep); emitFailed(reason); }); connect(hashingTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propagateStepProgress); connect(hashingTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { progressStep->update(current, total); stepProgress(*progressStep); }); connect(hashingTask.get(), &Task::status, this, [this, progressStep](QString status) { progressStep->status = status; stepProgress(*progressStep); }); connect(hashingTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); hashingTask->start(); } void FlamePackExportTask::makeApiRequest() { if (pendingHashes.isEmpty()) { buildZip(); return; } setStatus(tr("Finding versions for hashes...")); setProgress(2, 5); QList fingerprints; for (auto& murmur : pendingHashes.keys()) { fingerprints.push_back(murmur.toUInt()); } auto [matchTask, response] = api.matchFingerprints(fingerprints); task = matchTask; connect(task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parseError{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError); if (parseError.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from CurseForge::CurrentVersions at" << parseError.offset << "reason:" << parseError.errorString(); qWarning() << *response; emitFailed(parseError.errorString()); return; } try { auto docObj = Json::requireObject(doc); auto dataObj = Json::requireObject(docObj, "data"); auto dataArr = Json::requireArray(dataObj, "exactMatches"); if (dataArr.isEmpty()) { qWarning() << "No matches found for fingerprint search!"; getProjectsInfo(); return; } for (auto match : dataArr) { auto matchObj = match.toObject(); auto fileObj = matchObj["file"].toObject(); if (matchObj.isEmpty() || fileObj.isEmpty()) { qWarning() << "Fingerprint match is empty!"; return; } auto fingerprint = QString::number(fileObj["fileFingerprint"].toInteger()); auto mod = pendingHashes.find(fingerprint); if (mod == pendingHashes.end()) { qWarning() << "Invalid fingerprint from the API response."; continue; } setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name)); if (fileObj["isAvailable"].toBool()) resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fileObj, "id"), mod->enabled, mod->isMod }); } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } pendingHashes.clear(); getProjectsInfo(); }); connect(task.get(), &Task::failed, this, &FlamePackExportTask::getProjectsInfo); connect(task.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); task->start(); } void FlamePackExportTask::getProjectsInfo() { setStatus(tr("Finding project info from CurseForge...")); setProgress(3, 5); QStringList addonIds; for (const auto& resolved : resolvedFiles) { if (resolved.slug.isEmpty()) { addonIds << QString::number(resolved.addonId); } } Task::Ptr projTask; QByteArray* response; if (addonIds.isEmpty()) { buildZip(); return; } else if (addonIds.size() == 1) { std::tie(projTask, response) = api.getProject(*addonIds.begin()); } else { std::tie(projTask, response) = api.getProjects(addonIds); } connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parseError{}; auto doc = QJsonDocument::fromJson(*response, &parseError); if (parseError.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from CurseForge projects task at" << parseError.offset << "reason:" << parseError.errorString(); qWarning() << *response; emitFailed(parseError.errorString()); return; } try { QJsonArray entries; if (addonIds.size() == 1) entries = { Json::requireObject(Json::requireObject(doc), "data") }; else entries = Json::requireArray(Json::requireObject(doc), "data"); for (auto entry : entries) { auto entryObj = Json::requireObject(entry); try { setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(Json::requireString(entryObj, "name"))); ModPlatform::IndexedPack pack; FlameMod::loadIndexedPack(pack, entryObj); for (auto key : resolvedFiles.keys()) { auto val = resolvedFiles.value(key); if (val.addonId == pack.addonId) { val.name = pack.name; val.slug = pack.slug; QStringList authors; for (auto author : pack.authors) authors << author.name; val.authors = authors.join(", "); resolvedFiles[key] = val; } } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; } } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } buildZip(); }); connect(projTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); connect(projTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); task.reset(projTask); task->start(); } void FlamePackExportTask::buildZip() { setStatus(tr("Adding files...")); setProgress(4, 5); auto zipTask = makeShared(m_options.output, m_gameRoot, m_files, "overrides/", true); zipTask->addExtraFile("manifest.json", generateIndex()); zipTask->addExtraFile("modlist.html", generateHTML()); QStringList exclude; std::transform(resolvedFiles.keyBegin(), resolvedFiles.keyEnd(), std::back_insert_iterator(exclude), [this](QString file) { return m_gameRoot.relativeFilePath(file); }); zipTask->setExcludeFiles(exclude); auto progressStep = std::make_shared(); connect(zipTask.get(), &Task::finished, this, [this, progressStep] { progressStep->state = TaskStepState::Succeeded; stepProgress(*progressStep); }); connect(zipTask.get(), &Task::succeeded, this, &FlamePackExportTask::emitSucceeded); connect(zipTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) { progressStep->state = TaskStepState::Failed; stepProgress(*progressStep); emitFailed(reason); }); connect(zipTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propagateStepProgress); connect(zipTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { progressStep->update(current, total); stepProgress(*progressStep); }); connect(zipTask.get(), &Task::status, this, [this, progressStep](QString status) { progressStep->status = status; stepProgress(*progressStep); }); task.reset(zipTask); zipTask->start(); } QByteArray FlamePackExportTask::generateIndex() { QJsonObject obj; obj["manifestType"] = "minecraftModpack"; obj["manifestVersion"] = 1; obj["name"] = m_options.name; obj["version"] = m_options.version; obj["author"] = m_options.author; obj["overrides"] = "overrides"; QJsonObject version; auto profile = m_options.instance->getPackProfile(); // collect all supported components const ComponentPtr minecraft = profile->getComponent("net.minecraft"); const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader"); const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader"); const ComponentPtr forge = profile->getComponent("net.minecraftforge"); const ComponentPtr neoforge = profile->getComponent("net.neoforged"); // convert all available components to mrpack dependencies if (minecraft != nullptr) version["version"] = minecraft->m_version; QString id; if (quilt != nullptr) id = "quilt-" + quilt->m_version; else if (fabric != nullptr) id = "fabric-" + fabric->m_version; else if (forge != nullptr) id = "forge-" + forge->m_version; else if (neoforge != nullptr) { id = "neoforge-"; if (minecraft->m_version == "1.20.1") id += "1.20.1-"; id += neoforge->m_version; } version["modLoaders"] = QJsonArray(); if (!id.isEmpty()) { QJsonObject loader; loader["id"] = id; loader["primary"] = true; version["modLoaders"] = QJsonArray({ loader }); } if (m_options.recommendedRAM > 0) version["recommendedRam"] = m_options.recommendedRAM; obj["minecraft"] = version; QJsonArray files; for (auto mod : resolvedFiles) { QJsonObject file; file["projectID"] = mod.addonId; file["fileID"] = mod.version; file["required"] = mod.enabled || !m_options.optionalFiles; files << file; } obj["files"] = files; return QJsonDocument(obj).toJson(QJsonDocument::Compact); } QByteArray FlamePackExportTask::generateHTML() { QString content = ""; for (auto mod : resolvedFiles) { if (mod.isMod) { content += QString(TEMPLATE) .replace("{name}", mod.name.toHtmlEscaped()) .replace("{url}", ModPlatform::getMetaURL(ModPlatform::ResourceProvider::FLAME, mod.addonId).toHtmlEscaped()) .replace("{authors}", !mod.authors.isEmpty() ? QString(" (by %1)").arg(mod.authors).toHtmlEscaped() : ""); } } content = "
      " + content + "
    "; return content.toUtf8(); } PrismLauncher-11.0.3/launcher/modplatform/legacy_ftb/0000755000175100017510000000000015224505336022207 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/modplatform/legacy_ftb/PrivatePackManager.h0000644000175100017510000000127615224505336026072 0ustar runnerrunner#pragma once #include #include #include namespace LegacyFTB { class PrivatePackManager { public: ~PrivatePackManager() { save(); } void load(); void save() const; bool empty() const { return currentPacks.empty(); } const QSet& getCurrentPackCodes() const { return currentPacks; } void add(const QString& code) { currentPacks.insert(code); dirty = true; } void remove(const QString& code) { currentPacks.remove(code); dirty = true; } private: QSet currentPacks; QString m_filename = "private_packs.txt"; mutable bool dirty = false; }; } // namespace LegacyFTB PrismLauncher-11.0.3/launcher/modplatform/legacy_ftb/PackHelpers.h0000644000175100017510000000134315224505336024562 0ustar runnerrunner#pragma once #include #include #include #include namespace LegacyFTB { // Header for structs etc... enum class PackType { Public, ThirdParty, Private }; struct Modpack { QString name; QString description; QString author; QStringList oldVersions; QString currentVersion; QString mcVersion; QString mods; QString logo; // Technical data QString dir; QString file; //<- Url in the xml, but doesn't make much sense bool bugged = false; bool broken = false; PackType type; QString packCode; }; using ModpackList = QList; } // namespace LegacyFTB // We need it for the proxy model Q_DECLARE_METATYPE(LegacyFTB::Modpack) PrismLauncher-11.0.3/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp0000644000175100017510000000441615224505336026424 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "PrivatePackManager.h" #include #include "FileSystem.h" namespace LegacyFTB { void PrivatePackManager::load() { try { auto foo = QString::fromUtf8(FS::read(m_filename)).split('\n', Qt::SkipEmptyParts); currentPacks = QSet(foo.begin(), foo.end()); dirty = false; } catch (...) { currentPacks = {}; qWarning() << "Failed to read third party FTB pack codes from" << m_filename; } } void PrivatePackManager::save() const { if (!dirty) { return; } try { QStringList list = currentPacks.values(); FS::write(m_filename, list.join('\n').toUtf8()); dirty = false; } catch (...) { qWarning() << "Failed to write third party FTB pack codes to" << m_filename; } } } // namespace LegacyFTB PrismLauncher-11.0.3/launcher/modplatform/legacy_ftb/PackInstallTask.cpp0000644000175100017510000001660515224505336025753 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "PackInstallTask.h" #include #include "BaseInstance.h" #include "FileSystem.h" #include "MMCZip.h" #include "minecraft/GradleSpecifier.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "settings/INISettingsObject.h" #include "Application.h" #include "BuildConfig.h" #include "net/ApiDownload.h" namespace LegacyFTB { PackInstallTask::PackInstallTask(QNetworkAccessManager* network, const Modpack& pack, QString version) { m_pack = pack; m_version = version; m_network = network; } void PackInstallTask::executeTask() { downloadPack(); } void PackInstallTask::downloadPack() { setStatus(tr("Downloading zip for %1").arg(m_pack.name)); setProgress(1, 4); setAbortable(false); auto path = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); auto entry = APPLICATION->metacache()->resolveEntry("FTBPacks", path); entry->setStale(true); archivePath = entry->getFullPath(); netJobContainer.reset(new NetJob("Download FTB Pack", m_network)); QString url; if (m_pack.type == PackType::Private) { url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(path); } else { url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(path); } netJobContainer->addNetAction(Net::ApiDownload::makeCached(url, entry)); connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip); connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed); connect(netJobContainer.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress); connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::emitAborted); netJobContainer->start(); setAbortable(true); progress(1, 4); } void PackInstallTask::unzip() { setStatus(tr("Extracting modpack")); setAbortable(false); progress(2, 4); QDir extractDir(m_stagingPath); m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip"); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onUnzipFinished); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::onUnzipCanceled); m_extractFutureWatcher.setFuture(m_extractFuture); } void PackInstallTask::onUnzipFinished() { install(); } void PackInstallTask::onUnzipCanceled() { emitAborted(); } void PackInstallTask::install() { setStatus(tr("Installing modpack")); progress(3, 4); QDir unzipMcDir(m_stagingPath + "/unzip/minecraft"); if (unzipMcDir.exists()) { // ok, found minecraft dir, move contents to instance dir if (!FS::move(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) { emitFailed(tr("Failed to move unpacked Minecraft!")); return; } } QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); MinecraftInstance instance(m_globalSettings, std::make_unique(instanceConfigPath), m_stagingPath); { SettingsObject::Lock lock(instance.settings()); auto components = instance.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); bool fallback = true; // handle different versions QFile packJson(m_stagingPath + "/minecraft/pack.json"); QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods"); if (packJson.exists()) { if (packJson.open(QIODevice::ReadOnly | QIODevice::Text)) { QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll()); packJson.close(); // we only care about the libs QJsonArray libs = doc.object().value("libraries").toArray(); for (const auto& value : libs) { QString nameValue = value.toObject().value("name").toString(); if (!nameValue.startsWith("net.minecraftforge")) { continue; } GradleSpecifier forgeVersion(nameValue); components->setComponentVersion("net.minecraftforge", forgeVersion.version().replace(m_pack.mcVersion, "").replace("-", "")); packJson.remove(); fallback = false; break; } } else { qWarning() << "Failed to open file" << packJson.fileName() << "for reading:" << packJson.errorString(); } } if (jarmodDir.exists()) { qDebug() << "Found jarmods, installing..."; QStringList jarmods; for (auto info : jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) { qDebug() << "Jarmod:" << info.fileName(); jarmods.push_back(info.absoluteFilePath()); } components->installJarMods(jarmods); fallback = false; } // just nuke unzip directory, it s not needed anymore FS::deletePath(m_stagingPath + "/unzip"); if (fallback) { // TODO: Some fallback mechanism... or just keep failing! emitFailed(tr("No installation method found!")); return; } components->saveNow(); progress(4, 4); instance.setName(name()); if (m_instIcon == "default") { m_instIcon = "ftb_logo"; } instance.setIconKey(m_instIcon); } emitSucceeded(); } bool PackInstallTask::abort() { if (!canAbort()) { return false; } netJobContainer->abort(); return InstanceTask::abort(); } } // namespace LegacyFTB PrismLauncher-11.0.3/launcher/modplatform/legacy_ftb/PackInstallTask.h0000644000175100017510000000207715224505336025416 0ustar runnerrunner#pragma once #include "InstanceTask.h" #include "PackHelpers.h" #include "meta/Index.h" #include "meta/Version.h" #include "meta/VersionList.h" #include "net/NetJob.h" #include namespace LegacyFTB { class PackInstallTask : public InstanceTask { Q_OBJECT public: explicit PackInstallTask(QNetworkAccessManager* network, const Modpack& pack, QString version); virtual ~PackInstallTask() {} bool canAbort() const override { return true; } bool abort() override; protected: //! Entry point for tasks. virtual void executeTask() override; private: void downloadPack(); void unzip(); void install(); private slots: void onUnzipFinished(); void onUnzipCanceled(); private: /* data */ QNetworkAccessManager* m_network; bool abortable = false; QFuture> m_extractFuture; QFutureWatcher> m_extractFutureWatcher; NetJob::Ptr netJobContainer; QString archivePath; Modpack m_pack; QString m_version; }; } // namespace LegacyFTB PrismLauncher-11.0.3/launcher/modplatform/legacy_ftb/PackFetchTask.h0000644000175100017510000000215415224505336025035 0ustar runnerrunner#pragma once #include #include #include #include "PackHelpers.h" #include "net/NetJob.h" namespace LegacyFTB { class PackFetchTask : public QObject { Q_OBJECT public: PackFetchTask(QNetworkAccessManager* network) : QObject(nullptr), m_network(network) {}; virtual ~PackFetchTask() = default; void fetch(); void fetchPrivate(const QStringList& toFetch); private: QNetworkAccessManager* m_network; NetJob::Ptr jobPtr; bool parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list); ModpackList publicPacks; ModpackList thirdPartyPacks; protected slots: void fileDownloadFinished(QByteArray* publicResponse, QByteArray* thirdPartyResponse); void fileDownloadFailed(QString reason); void fileDownloadAborted(); signals: void finished(ModpackList publicPacks, ModpackList thirdPartyPacks); void failed(QString reason); void aborted(); void privateFileDownloadFinished(const Modpack& modpack); void privateFileDownloadFailed(QString reason, QString packCode); }; } // namespace LegacyFTB PrismLauncher-11.0.3/launcher/modplatform/legacy_ftb/PackFetchTask.cpp0000644000175100017510000001602515224505336025372 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "PackFetchTask.h" #include "PrivatePackManager.h" #include #include "Application.h" #include "BuildConfig.h" #include "net/ApiDownload.h" namespace LegacyFTB { void PackFetchTask::fetch() { publicPacks.clear(); thirdPartyPacks.clear(); jobPtr.reset(new NetJob("LegacyFTB::ModpackFetch", m_network)); QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml"); qDebug() << "Downloading public version info from" << publicPacksUrl.toString(); auto [publicAction, publicResponse] = Net::ApiDownload::makeByteArray(publicPacksUrl); jobPtr->addNetAction(publicAction); QUrl thirdPartyUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml"); qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString(); auto [thirdPartyAction, thirdPartyResponse] = Net::Download::makeByteArray(thirdPartyUrl); jobPtr->addNetAction(thirdPartyAction); connect(jobPtr.get(), &NetJob::succeeded, this, [this, publicResponse, thirdPartyResponse] { fileDownloadFinished(publicResponse, thirdPartyResponse); }); connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); connect(jobPtr.get(), &NetJob::aborted, this, &PackFetchTask::fileDownloadAborted); jobPtr->start(); } void PackFetchTask::fetchPrivate(const QStringList& toFetch) { QString privatePackBaseUrl = BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml"; for (auto& packCode : toFetch) { NetJob* job = new NetJob("Fetching private pack", m_network); auto [action, data] = Net::ApiDownload::makeByteArray(privatePackBaseUrl.arg(packCode)); job->addNetAction(action); job->setAskRetry(false); connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { ModpackList packs; parseAndAddPacks(*data, PackType::Private, packs); for (auto& currentPack : packs) { currentPack.packCode = packCode; emit privateFileDownloadFinished(currentPack); } job->deleteLater(); }); connect(job, &NetJob::failed, this, [this, job, packCode](QString reason) { emit privateFileDownloadFailed(reason, packCode); job->deleteLater(); }); connect(job, &NetJob::aborted, this, [this, job] { job->deleteLater(); emit aborted(); }); job->start(); } } void PackFetchTask::fileDownloadFinished(QByteArray* publicPtr, QByteArray* thirdPartyPtr) { QStringList failedLists; if (!parseAndAddPacks(*publicPtr, PackType::Public, publicPacks)) { failedLists.append(tr("Public Packs")); } if (!parseAndAddPacks(*thirdPartyPtr, PackType::ThirdParty, thirdPartyPacks)) { failedLists.append(tr("Third Party Packs")); } // NOTE(TheKodeToad): we don't want to reset the jobPtr earlier as it may invalidate the responses! jobPtr.reset(); if (failedLists.size() > 0) { emit failed(tr("Failed to download some pack lists: %1").arg(failedLists.join("\n- "))); } else { emit finished(publicPacks, thirdPartyPacks); } } bool PackFetchTask::parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list) { QDomDocument doc; QString errorMsg = "Unknown error."; int errorLine = -1; int errorCol = -1; if (!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) { auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:%3!").arg(errorMsg).arg(errorLine).arg(errorCol); qWarning() << fullErrMsg; return false; } QDomNodeList nodes = doc.elementsByTagName("modpack"); for (int i = 0; i < nodes.length(); i++) { QDomElement element = nodes.at(i).toElement(); Modpack modpack; modpack.name = element.attribute("name"); modpack.currentVersion = element.attribute("version"); modpack.mcVersion = element.attribute("mcVersion"); modpack.description = element.attribute("description"); modpack.mods = element.attribute("mods"); modpack.logo = element.attribute("logo"); modpack.oldVersions = element.attribute("oldVersions").split(";"); modpack.broken = false; modpack.bugged = false; // remove empty if the xml is bugged for (QString curr : modpack.oldVersions) { if (curr.isNull() || curr.isEmpty()) { modpack.oldVersions.removeAll(curr); modpack.bugged = true; qWarning() << "Removed some empty versions from" << modpack.name; } } if (modpack.oldVersions.size() < 1) { if (!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty()) { modpack.oldVersions.append(modpack.currentVersion); qWarning() << "Added current version to oldVersions because oldVersions was empty! (" + modpack.name + ")"; } else { modpack.broken = true; qWarning() << "Broken pack:" << modpack.name << "=> No valid version!"; } } modpack.author = element.attribute("author"); modpack.dir = element.attribute("dir"); modpack.file = element.attribute("url"); modpack.type = packType; list.append(modpack); } return true; } void PackFetchTask::fileDownloadFailed(QString reason) { qWarning() << "Fetching FTBPacks failed:" << reason; emit failed(reason); } void PackFetchTask::fileDownloadAborted() { emit aborted(); } } // namespace LegacyFTB PrismLauncher-11.0.3/launcher/modplatform/ModIndex.cpp0000644000175100017510000001442415224505336022330 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "modplatform/ModIndex.h" #include #include #include namespace ModPlatform { ModLoaderType operator|(ModLoaderType lhs, ModLoaderType rhs) { return static_cast(static_cast(lhs) | static_cast(rhs)); } static const QMap s_indexed_version_type_names = { { "release", IndexedVersionType::Release }, { "beta", IndexedVersionType::Beta }, { "alpha", IndexedVersionType::Alpha } }; static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric, Babric, BTA, LegacyFabric, Ornithe, Rift }; QList modLoaderTypesToList(ModLoaderTypes flags) { QList flagList; for (auto flag : loaderList) { if (flags.testFlag(flag)) { flagList.append(flag); } } return flagList; } QString IndexedVersionType::toString() const { return s_indexed_version_type_names.key(m_type, "unknown"); } IndexedVersionType IndexedVersionType::fromString(const QString& type) { return s_indexed_version_type_names.value(type, IndexedVersionType::Unknown); } const char* ProviderCapabilities::name(ResourceProvider p) { switch (p) { case ResourceProvider::MODRINTH: return "modrinth"; case ResourceProvider::FLAME: return "curseforge"; } return {}; } QString ProviderCapabilities::readableName(ResourceProvider p) { switch (p) { case ResourceProvider::MODRINTH: return "Modrinth"; case ResourceProvider::FLAME: return "CurseForge"; } return {}; } QStringList ProviderCapabilities::hashType(ResourceProvider p) { switch (p) { case ResourceProvider::MODRINTH: return { "sha512", "sha1" }; case ResourceProvider::FLAME: // Try newer formats first, fall back to old format return { "sha1", "md5", "murmur2" }; } return {}; } QString getMetaURL(ResourceProvider provider, QVariant projectID) { return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/" : "https://modrinth.com/mod/") + projectID.toString(); } auto getModLoaderAsString(ModLoaderType type) -> const QString { switch (type) { case NeoForge: return "neoforge"; case Forge: return "forge"; case Cauldron: return "cauldron"; case LiteLoader: return "liteloader"; case Fabric: return "fabric"; case Quilt: return "quilt"; case DataPack: return "datapack"; case Babric: return "babric"; case BTA: return "bta-babric"; case LegacyFabric: return "legacy-fabric"; case Ornithe: return "ornithe"; case Rift: return "rift"; default: break; } return ""; } auto getModLoaderFromString(QString type) -> ModLoaderType { if (type == "neoforge") return NeoForge; if (type == "forge") return Forge; if (type == "cauldron") return Cauldron; if (type == "liteloader") return LiteLoader; if (type == "fabric") return Fabric; if (type == "quilt") return Quilt; if (type == "babric") return Babric; if (type == "bta-babric") return BTA; if (type == "legacy-fabric") return LegacyFabric; if (type == "ornithe") return Ornithe; if (type == "rift") return Rift; return {}; } QString SideUtils::toString(Side side) { switch (side) { case Side::ClientSide: return "client"; case Side::ServerSide: return "server"; case Side::UniversalSide: return "both"; case Side::NoSide: break; } return {}; } Side SideUtils::fromString(QString side) { if (side == "client") return Side::ClientSide; if (side == "server") return Side::ServerSide; if (side == "both") return Side::UniversalSide; return Side::UniversalSide; } QString DependencyTypeUtils::toString(DependencyType type) { switch (type) { case DependencyType::REQUIRED: return "REQUIRED"; case DependencyType::OPTIONAL: return "OPTIONAL"; case DependencyType::INCOMPATIBLE: return "INCOMPATIBLE"; case DependencyType::EMBEDDED: return "EMBEDDED"; case DependencyType::TOOL: return "TOOL"; case DependencyType::INCLUDE: return "INCLUDE"; case DependencyType::UNKNOWN: return "UNKNOWN"; } return "UNKNOWN"; } DependencyType DependencyTypeUtils::fromString(const QString& str) { static const QHash map = { { "REQUIRED", DependencyType::REQUIRED }, { "OPTIONAL", DependencyType::OPTIONAL }, { "INCOMPATIBLE", DependencyType::INCOMPATIBLE }, { "EMBEDDED", DependencyType::EMBEDDED }, { "TOOL", DependencyType::TOOL }, { "INCLUDE", DependencyType::INCLUDE }, { "UNKNOWN", DependencyType::UNKNOWN }, }; return map.value(str.toUpper(), DependencyType::UNKNOWN); } } // namespace ModPlatform PrismLauncher-11.0.3/launcher/modplatform/ResourceAPI.cpp0000644000175100017510000002772415224505336022751 0ustar runnerrunner#include "modplatform/ResourceAPI.h" #include "Application.h" #include "Json.h" #include "net/NetJob.h" #include "modplatform/ModIndex.h" #include "net/ApiDownload.h" Task::Ptr ResourceAPI::searchProjects(SearchArgs&& args, Callback>&& callbacks) const { auto search_url_optional = getSearchURL(args); if (!search_url_optional.has_value()) { callbacks.on_fail("Failed to create search URL", -1); return nullptr; } auto search_url = search_url_optional.value(); auto netJob = makeShared(QString("%1::Search").arg(debugName()), APPLICATION->network()); auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(search_url)); netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from" << debugName() << "at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; callbacks.on_fail(parse_error.errorString(), -1); return; } QList newList; auto packs = documentToArray(doc); for (auto packRaw : packs) { auto packObj = packRaw.toObject(); ModPlatform::IndexedPack::Ptr pack = std::make_shared(); try { loadIndexedPack(*pack, packObj); newList << pack; } catch (const JSONValidationError& e) { qWarning().nospace() << "Error while loading resource from " << debugName() << ": " << e.cause(); continue; } } callbacks.on_succeed(newList); }); // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. // This prevents the lambda from extending the lifetime of the shared resource, // as it only temporarily locks the resource when needed. auto weak = netJob.toWeakRef(); QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { int network_error_code = -1; if (auto netJob = weak.lock()) { if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) network_error_code = failed_action->replyStatusCode(); } callbacks.on_fail(reason, network_error_code); }); QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { if (callbacks.on_abort != nullptr) callbacks.on_abort(); }); return netJob; } Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback>&& callbacks) const { auto versions_url_optional = getVersionsURL(args); if (!versions_url_optional.has_value()) return nullptr; auto versions_url = versions_url_optional.value(); auto netJob = makeShared(QString("%1::Versions").arg(args.pack->name), APPLICATION->network()); auto [action, response] = Net::ApiDownload::makeByteArray(versions_url); netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response for getting versions at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } QVector unsortedVersions; try { auto arr = doc.isObject() ? doc.object()["data"].toArray() : doc.array(); for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj, args.resourceType); if (!file.addonId.isValid()) { file.addonId = args.pack->addonId; } if (file.fileId.isValid() && !file.downloadUrl.isEmpty()) { // Heuristic to check if the returned value is valid unsortedVersions.append(file); } } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { // dates are in RFC 3339 format return a.date > b.date; }; std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); } catch (const JSONValidationError& e) { qDebug() << doc; qWarning() << "Error while reading" << debugName() << "resource version:" << e.cause(); } callbacks.on_succeed(unsortedVersions); }); // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. // This prevents the lambda from extending the lifetime of the shared resource, // as it only temporarily locks the resource when needed. auto weak = netJob.toWeakRef(); QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { int network_error_code = -1; if (auto netJob = weak.lock()) { if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) network_error_code = failed_action->replyStatusCode(); } callbacks.on_fail(reason, network_error_code); }); QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { if (callbacks.on_abort != nullptr) callbacks.on_abort(); }); return netJob; } Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback&& callbacks, bool askRetry) const { auto [job, response] = getProject(args.pack->addonId.toString(), askRetry); QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] { auto pack = args.pack; QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response for mod info at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } try { auto obj = Json::requireObject(doc); if (obj.contains("data")) obj = Json::requireObject(obj, "data"); loadIndexedPack(*pack, obj); loadExtraPackInfo(*pack, obj); } catch (const JSONValidationError& e) { qDebug() << doc; qWarning() << "Error while reading" << debugName() << "resource info:" << e.cause(); } callbacks.on_succeed(pack); }); // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. // This prevents the lambda from extending the lifetime of the shared resource, // as it only temporarily locks the resource when needed. auto weak = job.toWeakRef(); QObject::connect(job.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { int network_error_code = -1; if (auto job = weak.lock()) { if (auto netJob = qSharedPointerDynamicCast(job)) { if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) { network_error_code = failed_action->replyStatusCode(); } } } callbacks.on_fail(reason, network_error_code); }); QObject::connect(job.get(), &NetJob::aborted, [callbacks] { if (callbacks.on_abort != nullptr) callbacks.on_abort(); }); return job; } Task::Ptr ResourceAPI::getDependencyVersion(DependencySearchArgs&& args, Callback&& callbacks) const { auto versions_url_optional = getDependencyURL(args); if (!versions_url_optional.has_value()) return nullptr; auto versions_url = versions_url_optional.value(); auto netJob = makeShared(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network()); auto [action, response] = Net::ApiDownload::makeByteArray(versions_url); netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response for getting dependency version at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } QJsonArray arr; if (args.dependency.version.length() != 0 && doc.isObject()) { arr.append(doc.object()); } else { arr = doc.isObject() ? doc.object()["data"].toArray() : doc.array(); } QVector versions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj, ModPlatform::ResourceType::Mod); if (!file.addonId.isValid()) file.addonId = args.dependency.addonId; if (file.fileId.isValid() && (!file.loaders || args.loader & file.loaders)) // Heuristic to check if the returned value is valid versions.append(file); } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { // dates are in RFC 3339 format return a.date > b.date; }; std::sort(versions.begin(), versions.end(), orderSortPredicate); auto bestMatch = versions.size() != 0 ? versions.front() : ModPlatform::IndexedVersion(); callbacks.on_succeed(bestMatch); }); // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. // This prevents the lambda from extending the lifetime of the shared resource, // as it only temporarily locks the resource when needed. auto weak = netJob.toWeakRef(); QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { int network_error_code = -1; if (auto netJob = weak.lock()) { if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) network_error_code = failed_action->replyStatusCode(); } callbacks.on_fail(reason, network_error_code); }); return netJob; } QString ResourceAPI::getGameVersionsString(std::vector mcVersions) const { QString s; for (auto& ver : mcVersions) { s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver)); } s.remove(s.length() - 1, 1); // remove last comma return s; } QString ResourceAPI::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; } std::pair ResourceAPI::getProject(QString addonId, bool askRetry) const { auto project_url_optional = getInfoURL(addonId); if (!project_url_optional.has_value()) return { nullptr, nullptr }; auto project_url = project_url_optional.value(); auto netJob = makeShared(QString("%1::GetProject").arg(addonId), APPLICATION->network()); netJob->setAskRetry(askRetry); auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(project_url)); netJob->addNetAction(action); return { netJob, response }; } PrismLauncher-11.0.3/launcher/modplatform/helpers/0000755000175100017510000000000015224505336021552 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/modplatform/helpers/OverrideUtils.cpp0000644000175100017510000000336015224505336025060 0ustar runnerrunner#include "OverrideUtils.h" #include #include "FileSystem.h" namespace Override { void createOverrides(const QString& name, const QString& parent_folder, const QString& override_path) { QString file_path(FS::PathCombine(parent_folder, name + ".txt")); if (QFile::exists(file_path)) FS::deletePath(file_path); FS::ensureFilePathExists(file_path); QFile file(file_path); if (!file.open(QFile::WriteOnly)) { qWarning() << "Failed to open file" << file.fileName() << "for writing:" << file.errorString(); return; } QDirIterator override_iterator(override_path, QDirIterator::Subdirectories); while (override_iterator.hasNext()) { auto override_file_path = override_iterator.next(); QFileInfo info(override_file_path); if (info.isFile()) { // Absolute path with temp directory -> relative path override_file_path = override_file_path.split(name).last().remove(0, 1); file.write(override_file_path.toUtf8()); file.write("\n"); } } file.close(); } QStringList readOverrides(const QString& name, const QString& parent_folder) { QString file_path(FS::PathCombine(parent_folder, name + ".txt")); QFile file(file_path); if (!file.exists()) return {}; QStringList previous_overrides; if (!file.open(QFile::ReadOnly)) { qWarning() << "Failed to open file" << file.fileName() << "for reading:" << file.errorString(); return previous_overrides; } QString entry; do { entry = file.readLine(); previous_overrides.append(entry.trimmed()); } while (!entry.isEmpty()); file.close(); return previous_overrides; } } // namespace Override PrismLauncher-11.0.3/launcher/modplatform/helpers/HashUtils.cpp0000644000175100017510000001134615224505336024167 0ustar runnerrunner#include "HashUtils.h" #include #include #include #include #include namespace Hashing { Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider) { switch (provider) { case ModPlatform::ResourceProvider::MODRINTH: return makeShared(file_path, ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()); case ModPlatform::ResourceProvider::FLAME: return makeShared(file_path, Algorithm::Murmur2); default: qCritical() << "[Hashing]" << "Unrecognized mod platform!"; return nullptr; } } Hasher::Ptr createHasher(QString file_path, QString type) { return makeShared(file_path, type); } class QIODeviceReader : public Murmur2::Reader { public: QIODeviceReader(QIODevice* device) : m_device(device) {} virtual ~QIODeviceReader() = default; virtual int read(char* s, int n) { return m_device->read(s, n); } virtual bool eof() { return m_device->atEnd(); } virtual void goToBeginning() { m_device->seek(0); } virtual void close() { m_device->close(); } private: QIODevice* m_device; }; QString algorithmToString(Algorithm type) { switch (type) { case Algorithm::Md4: return "md4"; case Algorithm::Md5: return "md5"; case Algorithm::Sha1: return "sha1"; case Algorithm::Sha256: return "sha256"; case Algorithm::Sha512: return "sha512"; case Algorithm::Murmur2: return "murmur2"; // case Algorithm::Unknown: default: break; } return "unknown"; } Algorithm algorithmFromString(QString type) { if (type == "md4") return Algorithm::Md4; if (type == "md5") return Algorithm::Md5; if (type == "sha1") return Algorithm::Sha1; if (type == "sha256") return Algorithm::Sha256; if (type == "sha512") return Algorithm::Sha512; if (type == "murmur2") return Algorithm::Murmur2; return Algorithm::Unknown; } QString hash(QIODevice* device, Algorithm type) { if (!device->isOpen() && !device->open(QFile::ReadOnly)) return ""; QCryptographicHash::Algorithm alg = QCryptographicHash::Sha1; switch (type) { case Algorithm::Md4: alg = QCryptographicHash::Algorithm::Md4; break; case Algorithm::Md5: alg = QCryptographicHash::Algorithm::Md5; break; case Algorithm::Sha1: alg = QCryptographicHash::Algorithm::Sha1; break; case Algorithm::Sha256: alg = QCryptographicHash::Algorithm::Sha256; break; case Algorithm::Sha512: alg = QCryptographicHash::Algorithm::Sha512; break; case Algorithm::Murmur2: { // CF-specific auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); }; auto reader = std::make_unique(device); auto result = QString::number(Murmur2::hash(reader.get(), 4 * MiB, should_filter_out)); device->close(); return result; } case Algorithm::Unknown: device->close(); return ""; } QCryptographicHash hash(alg); if (!hash.addData(device)) qCritical() << "Failed to read JAR to create hash!"; Q_ASSERT(hash.result().length() == hash.hashLength(alg)); auto result = hash.result().toHex(); device->close(); return result; } QString hash(QString fileName, Algorithm type) { QFile file(fileName); return hash(&file, type); } QString hash(QByteArray data, Algorithm type) { QBuffer buff(&data); return hash(&buff, type); } void Hasher::executeTask() { m_future = QtConcurrent::run( QThreadPool::globalInstance(), [](QString fileName, Algorithm type) { return hash(fileName, type); }, m_path, m_alg); connect(&m_watcher, &QFutureWatcher::finished, this, [this] { if (m_future.isCanceled()) { emitAborted(); } else if (m_result = m_future.result(); m_result.isEmpty()) { emitFailed("Empty hash!"); } else { emit resultsReady(m_result); emitSucceeded(); } }); m_watcher.setFuture(m_future); } bool Hasher::abort() { if (m_future.isRunning()) { m_future.cancel(); // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not // occur immediately. return true; } return false; } } // namespace Hashing PrismLauncher-11.0.3/launcher/modplatform/helpers/ExportToModList.cpp0000644000175100017510000001654415224505336025350 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ExportToModList.h" #include #include #include namespace ExportToModList { QString toHTML(QList mods, OptionalData extraData) { QStringList lines; for (auto mod : mods) { auto meta = mod->metadata(); auto modName = mod->name().toHtmlEscaped(); if (extraData & Url) { auto url = mod->homepage().toHtmlEscaped(); if (!url.isEmpty()) modName = QString("%2").arg(url, modName); } auto line = modName; if (extraData & Version) { auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); if (!ver.isEmpty()) line += QString(" [%1]").arg(ver.toHtmlEscaped()); } if (extraData & Authors && !mod->authors().isEmpty()) line += " by " + mod->authors().join(", ").toHtmlEscaped(); if (extraData & FileName) line += QString(" (%1)").arg(mod->fileinfo().fileName().toHtmlEscaped()); lines.append(QString("
  • %1
  • ").arg(line)); } return QString("
      \n\t%1\n
    ").arg(lines.join("\n\t")); } QString toMarkdownEscaped(QString src) { for (auto ch : "\\`*_{}[]<>()#+-.!|") src.replace(ch, QString("\\%1").arg(ch)); return src; } QString toMarkdown(QList mods, OptionalData extraData) { QStringList lines; for (auto mod : mods) { auto meta = mod->metadata(); auto modName = toMarkdownEscaped(mod->name()); if (extraData & Url) { auto url = mod->homepage(); if (!url.isEmpty()) modName = QString("[%1](%2)").arg(modName, url); } auto line = modName; if (extraData & Version) { auto ver = toMarkdownEscaped(mod->version()); if (ver.isEmpty() && meta != nullptr) ver = toMarkdownEscaped(meta->version().toString()); if (!ver.isEmpty()) line += QString(" [%1]").arg(ver); } if (extraData & Authors && !mod->authors().isEmpty()) line += " by " + toMarkdownEscaped(mod->authors().join(", ")); if (extraData & FileName) line += QString(" (%1)").arg(toMarkdownEscaped(mod->fileinfo().fileName())); lines << "- " + line; } return lines.join("\n"); } QString toPlainTXT(QList mods, OptionalData extraData) { QStringList lines; for (auto mod : mods) { auto meta = mod->metadata(); auto modName = mod->name(); auto line = modName; if (extraData & Url) { auto url = mod->homepage(); if (!url.isEmpty()) line += QString(" (%1)").arg(url); } if (extraData & Version) { auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); if (!ver.isEmpty()) line += QString(" [%1]").arg(ver); } if (extraData & Authors && !mod->authors().isEmpty()) line += " by " + mod->authors().join(", "); if (extraData & FileName) line += QString(" (%1)").arg(mod->fileinfo().fileName()); lines << line; } return lines.join("\n"); } QString toJSON(QList mods, OptionalData extraData) { QJsonArray lines; for (auto mod : mods) { auto meta = mod->metadata(); auto modName = mod->name(); QJsonObject line; line["name"] = modName; if (extraData & Url) { auto url = mod->homepage(); if (!url.isEmpty()) line["url"] = url; } if (extraData & Version) { auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); if (!ver.isEmpty()) line["version"] = ver; } if (extraData & Authors && !mod->authors().isEmpty()) line["authors"] = QJsonArray::fromStringList(mod->authors()); if (extraData & FileName) line["filename"] = mod->fileinfo().fileName(); lines << line; } QJsonDocument doc; doc.setArray(lines); return doc.toJson(); } QString toCSV(QList mods, OptionalData extraData) { QStringList lines; for (auto mod : mods) { QStringList data; auto meta = mod->metadata(); auto modName = mod->name(); data << modName; if (extraData & Url) data << mod->homepage(); if (extraData & Version) { auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); data << ver; } if (extraData & Authors) { QString authors; if (mod->authors().length() == 1) authors = mod->authors().back(); else if (mod->authors().length() > 1) authors = QString("\"%1\"").arg(mod->authors().join(",")); data << authors; } if (extraData & FileName) data << mod->fileinfo().fileName(); lines << data.join(","); } return lines.join("\n"); } QString exportToModList(QList mods, Formats format, OptionalData extraData) { switch (format) { case HTML: return toHTML(mods, extraData); case MARKDOWN: return toMarkdown(mods, extraData); case PLAINTXT: return toPlainTXT(mods, extraData); case JSON: return toJSON(mods, extraData); case CSV: return toCSV(mods, extraData); default: { return QString("unknown format:%1").arg(format); } } } QString exportToModList(QList mods, QString lineTemplate) { QStringList lines; for (auto mod : mods) { auto meta = mod->metadata(); auto modName = mod->name(); auto modID = mod->mod_id(); auto url = mod->homepage(); auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); auto authors = mod->authors().join(", "); auto filename = mod->fileinfo().fileName(); lines << QString(lineTemplate) .replace("{name}", modName) .replace("{mod_id}", modID) .replace("{url}", url) .replace("{version}", ver) .replace("{authors}", authors) .replace("{filename}", filename); } return lines.join("\n"); } } // namespace ExportToModList PrismLauncher-11.0.3/launcher/modplatform/helpers/OverrideUtils.h0000644000175100017510000000115215224505336024522 0ustar runnerrunner#pragma once #include namespace Override { /** This creates a file in `parent_folder` that holds information about which * overrides are in `override_path`. * * If there's already an existing such file, it will be ovewritten. */ void createOverrides(const QString& name, const QString& parent_folder, const QString& override_path); /** This reads an existing overrides archive, returning a list of overrides. * * If there's no such file in `parent_folder`, it will return an empty list. */ QStringList readOverrides(const QString& name, const QString& parent_folder); } // namespace Override PrismLauncher-11.0.3/launcher/modplatform/helpers/ExportToModList.h0000644000175100017510000000224415224505336025005 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "minecraft/mod/Mod.h" namespace ExportToModList { enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM }; enum OptionalData { Authors = 1 << 0, Url = 1 << 1, Version = 1 << 2, FileName = 1 << 3 }; QString exportToModList(QList mods, Formats format, OptionalData extraData); QString exportToModList(QList mods, QString lineTemplate); } // namespace ExportToModList PrismLauncher-11.0.3/launcher/modplatform/helpers/HashUtils.h0000644000175100017510000000242515224505336023632 0ustar runnerrunner#pragma once #include #include #include #include #include "modplatform/ModIndex.h" #include "tasks/Task.h" namespace Hashing { enum class Algorithm { Md4, Md5, Sha1, Sha256, Sha512, Murmur2, Unknown }; QString algorithmToString(Algorithm type); Algorithm algorithmFromString(QString type); QString hash(QIODevice* device, Algorithm type); QString hash(QString fileName, Algorithm type); QString hash(QByteArray data, Algorithm type); class Hasher : public Task { Q_OBJECT public: using Ptr = shared_qobject_ptr; Hasher(QString file_path, Algorithm alg) : m_path(file_path), m_alg(alg) {} Hasher(QString file_path, QString alg) : Hasher(file_path, algorithmFromString(alg)) {} bool abort() override; void executeTask() override; QString getResult() const { return m_result; }; QString getPath() const { return m_path; }; signals: void resultsReady(QString hash); private: QString m_result; QString m_path; Algorithm m_alg; QFuture m_future; QFutureWatcher m_watcher; }; Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider); Hasher::Ptr createHasher(QString file_path, QString type); } // namespace Hashing PrismLauncher-11.0.3/launcher/modplatform/import_ftb/0000755000175100017510000000000015224505336022255 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/modplatform/import_ftb/PackHelpers.cpp0000644000175100017510000001401115224505336025157 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "modplatform/import_ftb/PackHelpers.h" #include #include #include #include #include "FileSystem.h" #include "Json.h" namespace FTBImportAPP { QIcon loadFTBIcon(const QString& imagePath) { // Map of type byte to image type string static const QHash imageTypeMap = { { 0x00, "png" }, { 0x01, "jpg" }, { 0x02, "gif" }, { 0x03, "webp" } }; QFile file(imagePath); if (!file.exists() || !file.open(QIODevice::ReadOnly)) { return QIcon(); } char type; if (!file.getChar(&type)) { qDebug() << "Missing FTB image type header at" << imagePath; return QIcon(); } if (!imageTypeMap.contains(type)) { qDebug().nospace().noquote() << "Don't recognize FTB image type 0x" << QString::number(type, 16); return QIcon(); } auto imageType = imageTypeMap[type]; // Extract actual image data beyond the first byte QImageReader reader(&file, imageType); auto pixmap = QPixmap::fromImageReader(&reader); if (pixmap.isNull()) { qDebug() << "The FTB image at" << imagePath << "is not valid"; return QIcon(); } return QIcon(pixmap); } Modpack parseDirectory(QString path) { Modpack modpack{ path }; auto instanceFile = QFileInfo(FS::PathCombine(path, "instance.json")); if (!instanceFile.exists() || !instanceFile.isFile()) return {}; try { auto doc = Json::requireDocument(instanceFile.absoluteFilePath(), "FTB_APP instance JSON file"); const auto root = doc.object(); modpack.uuid = Json::requireString(root, "uuid", "uuid"); modpack.id = Json::requireInteger(root, "id", "id"); modpack.versionId = Json::requireInteger(root, "versionId", "versionId"); modpack.name = Json::requireString(root, "name", "name"); modpack.version = Json::requireString(root, "version", "version"); modpack.mcVersion = Json::requireString(root, "mcVersion", "mcVersion"); modpack.jvmArgs = root["jvmArgs"].toVariant(); modpack.totalPlayTime = Json::requireInteger(root, "totalPlayTime", "totalPlayTime"); auto modLoader = Json::requireString(root, "modLoader", "modLoader"); if (!modLoader.isEmpty()) { const auto parts = modLoader.split('-', Qt::KeepEmptyParts); if (parts.size() >= 2) { const auto loader = parts.first().toLower(); modpack.loaderVersion = parts.at(1).trimmed(); if (loader == "neoforge") { modpack.loaderType = ModPlatform::NeoForge; } else if (loader == "forge") { modpack.loaderType = ModPlatform::Forge; } else if (loader == "fabric") { modpack.loaderType = ModPlatform::Fabric; } else if (loader == "quilt") { modpack.loaderType = ModPlatform::Quilt; } } } } catch (const Exception& e) { qDebug() << "Couldn't load ftb instance json:" << e.cause(); return {}; } if (!modpack.loaderType.has_value()) { legacyInstanceParsing(path, &modpack.loaderType, &modpack.loaderVersion); } auto iconFile = QFileInfo(FS::PathCombine(path, "folder.jpg")); if (iconFile.exists() && iconFile.isFile()) { modpack.icon = QIcon(iconFile.absoluteFilePath()); } else { // the logo is a file that the first bit denotes the image tipe followed by the actual image data modpack.icon = loadFTBIcon(FS::PathCombine(path, ".ftbapp", "logo")); } return modpack; } void legacyInstanceParsing(QString path, std::optional* loaderType, QString* loaderVersion) { auto versionsFile = QFileInfo(FS::PathCombine(path, ".ftbapp", "version.json")); if (!versionsFile.exists() || !versionsFile.isFile()) { versionsFile = QFileInfo(FS::PathCombine(path, "version.json")); } if (!versionsFile.exists() || !versionsFile.isFile()) { qDebug() << "Couldn't find ftb version json"; return; } try { auto doc = Json::requireDocument(versionsFile.absoluteFilePath(), "FTB_APP version JSON file"); const auto root = doc.object(); auto targets = Json::requireArray(root, "targets", "targets"); for (auto target : targets) { auto obj = Json::requireObject(target, "target"); auto name = Json::requireString(obj, "name", "name"); auto version = Json::requireString(obj, "version", "version"); if (name == "neoforge") { *loaderType = ModPlatform::NeoForge; *loaderVersion = version; break; } else if (name == "forge") { *loaderType = ModPlatform::Forge; *loaderVersion = version; break; } else if (name == "fabric") { *loaderType = ModPlatform::Fabric; *loaderVersion = version; break; } else if (name == "quilt") { *loaderType = ModPlatform::Quilt; *loaderVersion = version; break; } } } catch (const Exception& e) { qDebug() << "Couldn't load ftb version json:" << e.cause(); return; } } } // namespace FTBImportAPP PrismLauncher-11.0.3/launcher/modplatform/import_ftb/PackHelpers.h0000644000175100017510000000304715224505336024633 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include #include "modplatform/ResourceAPI.h" namespace FTBImportAPP { struct Modpack { QString path; // json data QString uuid; int id; int versionId; QString name; QString version; QString mcVersion; int totalPlayTime; // not needed for instance creation QVariant jvmArgs; std::optional loaderType; QString loaderVersion; QIcon icon; }; using ModpackList = QList; Modpack parseDirectory(QString path); void legacyInstanceParsing(QString path, std::optional* loaderType, QString* loaderVersion); } // namespace FTBImportAPP // We need it for the proxy model Q_DECLARE_METATYPE(FTBImportAPP::Modpack) PrismLauncher-11.0.3/launcher/modplatform/import_ftb/PackInstallTask.cpp0000644000175100017510000001031415224505336026010 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "PackInstallTask.h" #include #include "BaseInstance.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/ResourceAPI.h" #include "modplatform/import_ftb/PackHelpers.h" #include "settings/INISettingsObject.h" namespace FTBImportAPP { void PackInstallTask::executeTask() { setStatus(tr("Copying files...")); setAbortable(false); progress(1, 2); m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { FS::copy folderCopy(m_pack.path, FS::PathCombine(m_stagingPath, "minecraft")); return folderCopy(); }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::copySettings); connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::emitAborted); m_copyFutureWatcher.setFuture(m_copyFuture); } void PackInstallTask::copySettings() { setStatus(tr("Copying settings...")); progress(2, 2); QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); MinecraftInstance instance(m_globalSettings, std::make_unique(instanceConfigPath), m_stagingPath); { SettingsObject::Lock lock(instance.settings()); instance.settings()->set("InstanceType", "OneSix"); instance.settings()->set("totalTimePlayed", m_pack.totalPlayTime / 1000); if (m_pack.jvmArgs.isValid() && !m_pack.jvmArgs.toString().isEmpty()) { instance.settings()->set("OverrideJavaArgs", true); instance.settings()->set("JvmArgs", m_pack.jvmArgs.toString()); } auto components = instance.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); auto modloader = m_pack.loaderType; if (modloader.has_value()) switch (modloader.value()) { case ModPlatform::NeoForge: { components->setComponentVersion("net.neoforged", m_pack.loaderVersion, true); break; } case ModPlatform::Forge: { components->setComponentVersion("net.minecraftforge", m_pack.loaderVersion, true); break; } case ModPlatform::Fabric: { components->setComponentVersion("net.fabricmc.fabric-loader", m_pack.loaderVersion, true); break; } case ModPlatform::Quilt: { components->setComponentVersion("org.quiltmc.quilt-loader", m_pack.loaderVersion, true); break; } case ModPlatform::Cauldron: break; case ModPlatform::LiteLoader: break; case ModPlatform::DataPack: break; case ModPlatform::Babric: break; case ModPlatform::BTA: break; case ModPlatform::LegacyFabric: break; case ModPlatform::Ornithe: break; case ModPlatform::Rift: break; } components->saveNow(); instance.setName(name()); if (m_instIcon == "default") m_instIcon = "ftb_logo"; instance.setIconKey(m_instIcon); } emitSucceeded(); } } // namespace FTBImportAPP PrismLauncher-11.0.3/launcher/modplatform/import_ftb/PackInstallTask.h0000644000175100017510000000244415224505336025462 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "InstanceTask.h" #include "PackHelpers.h" namespace FTBImportAPP { class PackInstallTask : public InstanceTask { Q_OBJECT public: explicit PackInstallTask(const Modpack& pack) : m_pack(pack) {} virtual ~PackInstallTask() = default; protected: virtual void executeTask() override; private slots: void copySettings(); private: QFuture m_copyFuture; QFutureWatcher m_copyFutureWatcher; const Modpack m_pack; }; } // namespace FTBImportAPP PrismLauncher-11.0.3/launcher/modplatform/atlauncher/0000755000175100017510000000000015224505336022236 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/modplatform/atlauncher/ATLPackIndex.h0000644000175100017510000000226415224505336024622 0ustar runnerrunner/* * Copyright 2020-2021 Jamie Mansfield * * 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 "ATLPackManifest.h" #include #include #include namespace ATLauncher { struct IndexedVersion { QString version; QString minecraft; }; struct IndexedPack { int id; int position; QString name; PackType type; QList versions; bool system; QString description; QString safeName; }; void loadIndexedPack(IndexedPack& m, QJsonObject& obj); } // namespace ATLauncher Q_DECLARE_METATYPE(ATLauncher::IndexedPack) Q_DECLARE_METATYPE(QList) PrismLauncher-11.0.3/launcher/modplatform/atlauncher/ATLShareCode.cpp0000644000175100017510000000355415224505336025147 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ATLShareCode.h" #include "Json.h" namespace ATLauncher { static void loadShareCodeMod(ShareCodeMod& m, QJsonObject& obj) { m.selected = Json::requireBoolean(obj, "selected"); m.name = Json::requireString(obj, "name"); } static void loadShareCode(ShareCode& c, QJsonObject& obj) { c.pack = Json::requireString(obj, "pack"); c.version = Json::requireString(obj, "version"); auto mods = Json::requireObject(obj, "mods"); auto optional = Json::requireArray(mods, "optional"); for (const auto modRaw : optional) { auto modObj = Json::requireObject(modRaw); ShareCodeMod mod; loadShareCodeMod(mod, modObj); c.mods.append(mod); } } void loadShareCodeResponse(ShareCodeResponse& r, QJsonObject& obj) { r.error = Json::requireBoolean(obj, "error"); r.code = Json::requireInteger(obj, "code"); if (obj.contains("message") && !obj.value("message").isNull()) r.message = Json::requireString(obj, "message"); if (!r.error) { auto dataRaw = Json::requireObject(obj, "data"); loadShareCode(r.data, dataRaw); } } } // namespace ATLauncher PrismLauncher-11.0.3/launcher/modplatform/atlauncher/ATLPackInstallTask.h0000644000175100017510000001132115224505336025776 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 Jamie Mansfield * Copyright 2021 Petr Mrazek * * 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 #include "ATLPackManifest.h" #include "InstanceTask.h" #include "meta/Version.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "net/NetJob.h" #include #include namespace ATLauncher { enum class InstallMode : std::uint8_t { Install, Reinstall, Update, }; class UserInteractionSupport { public: /** * Requests a user interaction to select which optional mods should be installed. */ virtual std::optional> chooseOptionalMods(const PackVersion& version, QList mods) = 0; /** * Requests a user interaction to select a component version from a given version list * and constrained to a given Minecraft version. */ virtual QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) = 0; /** * Requests a user interaction to display a message. */ virtual void displayMessage(QString message) = 0; virtual ~UserInteractionSupport() = default; }; class PackInstallTask : public InstanceTask { Q_OBJECT public: explicit PackInstallTask(UserInteractionSupport* support, QString packName, QString version, InstallMode installMode = InstallMode::Install); ~PackInstallTask() override { delete m_support; } bool canAbort() const override { return true; } bool abort() override; protected: void executeTask() override; private slots: void onDownloadSucceeded(QByteArray* responsePtr); void onDownloadFailed(QString reason); void onDownloadAborted(); void onModsDownloaded(); void onModsExtracted(); private: QString getDirForModType(ModType type, const QString& raw); QString getVersionForLoader(const QString& uid); static QString detectLibrary(const VersionLibrary& library); bool createLibrariesComponent(const QString& instanceRoot, PackProfile* profile); bool createPackComponent(const QString& instanceRoot, PackProfile* profile); void deleteExistingFiles(); void installConfigs(); void extractConfigs(); void downloadMods(); bool extractMods(const QMap& toExtract, const QMap& toDecomp, const QMap& toCopy); void install(); private: UserInteractionSupport* m_support; bool abortable = false; NetJob::Ptr jobPtr; InstallMode m_install_mode; QString m_pack_name; QString m_pack_safe_name; QString m_version_name; PackVersion m_version; QMap modsToExtract; QMap modsToDecomp; QMap modsToCopy; QString archivePath; QStringList jarmods; Meta::Version::Ptr minecraftVersion; QMap componentsToInstall; QFuture> m_extractFuture; QFutureWatcher> m_extractFutureWatcher; QFuture m_modExtractFuture; QFutureWatcher m_modExtractFutureWatcher; }; } // namespace ATLauncher PrismLauncher-11.0.3/launcher/modplatform/atlauncher/ATLShareCode.h0000644000175100017510000000226015224505336024605 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include namespace ATLauncher { struct ShareCodeMod { bool selected; QString name; }; struct ShareCode { QString pack; QString version; QList mods; }; struct ShareCodeResponse { bool error; int code; QString message; ShareCode data; }; void loadShareCodeResponse(ShareCodeResponse& r, QJsonObject& obj); } // namespace ATLauncher PrismLauncher-11.0.3/launcher/modplatform/atlauncher/ATLPackManifest.cpp0000644000175100017510000002735015224505336025657 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 Jamie Mansfield * Copyright 2021 Petr Mrazek * * 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 "ATLPackManifest.h" #include "Json.h" static ATLauncher::DownloadType parseDownloadType(QString rawType) { if (rawType == QString("server")) { return ATLauncher::DownloadType::Server; } else if (rawType == QString("browser")) { return ATLauncher::DownloadType::Browser; } else if (rawType == QString("direct")) { return ATLauncher::DownloadType::Direct; } return ATLauncher::DownloadType::Unknown; } static ATLauncher::ModType parseModType(QString rawType) { // See https://wiki.atlauncher.com/mod_types if (rawType == QString("root")) { return ATLauncher::ModType::Root; } else if (rawType == QString("forge")) { return ATLauncher::ModType::Forge; } else if (rawType == QString("jar")) { return ATLauncher::ModType::Jar; } else if (rawType == QString("mods")) { return ATLauncher::ModType::Mods; } else if (rawType == QString("flan")) { return ATLauncher::ModType::Flan; } else if (rawType == QString("dependency") || rawType == QString("depandency")) { return ATLauncher::ModType::Dependency; } else if (rawType == QString("ic2lib")) { return ATLauncher::ModType::Ic2Lib; } else if (rawType == QString("denlib")) { return ATLauncher::ModType::DenLib; } else if (rawType == QString("coremods")) { return ATLauncher::ModType::Coremods; } else if (rawType == QString("mcpc")) { return ATLauncher::ModType::MCPC; } else if (rawType == QString("plugins")) { return ATLauncher::ModType::Plugins; } else if (rawType == QString("extract")) { return ATLauncher::ModType::Extract; } else if (rawType == QString("decomp")) { return ATLauncher::ModType::Decomp; } else if (rawType == QString("texturepack")) { return ATLauncher::ModType::TexturePack; } else if (rawType == QString("resourcepack")) { return ATLauncher::ModType::ResourcePack; } else if (rawType == QString("shaderpack")) { return ATLauncher::ModType::ShaderPack; } else if (rawType == QString("texturepackextract")) { return ATLauncher::ModType::TexturePackExtract; } else if (rawType == QString("resourcepackextract")) { return ATLauncher::ModType::ResourcePackExtract; } else if (rawType == QString("millenaire")) { return ATLauncher::ModType::Millenaire; } return ATLauncher::ModType::Unknown; } static void loadVersionLoader(ATLauncher::VersionLoader& p, QJsonObject& obj) { p.type = Json::requireString(obj, "type"); p.choose = obj["choose"].toBool(); auto metadata = Json::requireObject(obj, "metadata"); p.latest = metadata["latest"].toBool(); p.recommended = metadata["recommended"].toBool(); // Minecraft Forge if (p.type == "forge" || p.type == "neoforge") { p.version = metadata["version"].toString(""); } // Fabric Loader if (p.type == "fabric") { p.version = metadata["loader"].toString(""); } } static void loadVersionLibrary(ATLauncher::VersionLibrary& p, QJsonObject& obj) { p.url = Json::requireString(obj, "url"); p.file = Json::requireString(obj, "file"); p.md5 = Json::requireString(obj, "md5"); p.download_raw = Json::requireString(obj, "download"); p.download = parseDownloadType(p.download_raw); p.server = obj["server"].toString(""); } static void loadVersionConfigs(ATLauncher::VersionConfigs& p, QJsonObject& obj) { p.filesize = Json::requireInteger(obj, "filesize"); p.sha1 = Json::requireString(obj, "sha1"); } static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj) { p.name = Json::requireString(obj, "name"); p.version = Json::requireString(obj, "version"); p.url = Json::requireString(obj, "url"); p.file = Json::requireString(obj, "file"); p.md5 = obj["md5"].toString(""); p.download_raw = Json::requireString(obj, "download"); p.download = parseDownloadType(p.download_raw); p.type_raw = Json::requireString(obj, "type"); p.type = parseModType(p.type_raw); // This contributes to the Minecraft Forge detection, where we rely on mod.type being "Forge" // when the mod represents Forge. As there is little difference between "Jar" and "Forge, some // packs regretfully use "Jar". This will correct the type to "Forge" in these cases (as best // it can). if (p.name == QString("Minecraft Forge") && p.type == ATLauncher::ModType::Jar) { p.type_raw = "forge"; p.type = ATLauncher::ModType::Forge; } if (obj.contains("extractTo")) { p.extractTo_raw = Json::requireString(obj, "extractTo"); p.extractTo = parseModType(p.extractTo_raw); p.extractFolder = obj["extractFolder"].toString("").replace("%s%", "/"); } if (obj.contains("decompType")) { p.decompType_raw = Json::requireString(obj, "decompType"); p.decompType = parseModType(p.decompType_raw); p.decompFile = Json::requireString(obj, "decompFile"); } p.description = obj["description"].toString(""); p.optional = obj["optional"].toBool(); p.recommended = obj["recommended"].toBool(); p.selected = obj["selected"].toBool(); p.hidden = obj["hidden"].toBool(); p.library = obj["library"].toBool(); p.group = obj["group"].toString(""); if (obj.contains("depends")) { auto dependsArr = Json::requireArray(obj, "depends"); for (const auto depends : dependsArr) { p.depends.append(Json::requireString(depends)); } } p.colour = obj["colour"].toString(""); p.warning = obj["warning"].toString(""); p.client = obj["client"].toBool(); // computed p.effectively_hidden = p.hidden || p.library; } static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj) { m.install = obj["install"].toString(""); m.update = obj["update"].toString(""); } static void loadVersionMainClass(ATLauncher::PackVersionMainClass& m, QJsonObject& obj) { m.mainClass = obj["mainClass"].toString(""); m.depends = obj["depends"].toString(""); } static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a, QJsonObject& obj) { a.arguments = obj["arguments"].toString(""); a.depends = obj["depends"].toString(""); } static void loadVersionKeep(ATLauncher::VersionKeep& k, QJsonObject& obj) { k.base = Json::requireString(obj, "base"); k.target = Json::requireString(obj, "target"); } static void loadVersionKeeps(ATLauncher::VersionKeeps& k, QJsonObject& obj) { if (obj.contains("files")) { auto files = Json::requireArray(obj, "files"); for (const auto keepRaw : files) { auto keepObj = Json::requireObject(keepRaw); ATLauncher::VersionKeep keep; loadVersionKeep(keep, keepObj); k.files.append(keep); } } if (obj.contains("folders")) { auto folders = Json::requireArray(obj, "folders"); for (const auto keepRaw : folders) { auto keepObj = Json::requireObject(keepRaw); ATLauncher::VersionKeep keep; loadVersionKeep(keep, keepObj); k.folders.append(keep); } } } static void loadVersionDelete(ATLauncher::VersionDelete& d, QJsonObject& obj) { d.base = Json::requireString(obj, "base"); d.target = Json::requireString(obj, "target"); } static void loadVersionDeletes(ATLauncher::VersionDeletes& d, QJsonObject& obj) { if (obj.contains("files")) { auto files = Json::requireArray(obj, "files"); for (const auto deleteRaw : files) { auto deleteObj = Json::requireObject(deleteRaw); ATLauncher::VersionDelete versionDelete; loadVersionDelete(versionDelete, deleteObj); d.files.append(versionDelete); } } if (obj.contains("folders")) { auto folders = Json::requireArray(obj, "folders"); for (const auto deleteRaw : folders) { auto deleteObj = Json::requireObject(deleteRaw); ATLauncher::VersionDelete versionDelete; loadVersionDelete(versionDelete, deleteObj); d.folders.append(versionDelete); } } } void ATLauncher::loadVersion(PackVersion& v, QJsonObject& obj) { v.version = Json::requireString(obj, "version"); v.minecraft = Json::requireString(obj, "minecraft"); v.noConfigs = obj["noConfigs"].toBool(); if (obj.contains("mainClass")) { auto main = Json::requireObject(obj, "mainClass"); loadVersionMainClass(v.mainClass, main); } if (obj.contains("extraArguments")) { auto arguments = Json::requireObject(obj, "extraArguments"); loadVersionExtraArguments(v.extraArguments, arguments); } if (obj.contains("loader")) { auto loader = Json::requireObject(obj, "loader"); loadVersionLoader(v.loader, loader); } if (obj.contains("libraries")) { auto libraries = Json::requireArray(obj, "libraries"); for (const auto libraryRaw : libraries) { auto libraryObj = Json::requireObject(libraryRaw); ATLauncher::VersionLibrary target; loadVersionLibrary(target, libraryObj); v.libraries.append(target); } } if (obj.contains("mods")) { auto mods = Json::requireArray(obj, "mods"); for (const auto modRaw : mods) { auto modObj = Json::requireObject(modRaw); ATLauncher::VersionMod mod; loadVersionMod(mod, modObj); v.mods.append(mod); } } if (obj.contains("configs")) { auto configsObj = Json::requireObject(obj, "configs"); loadVersionConfigs(v.configs, configsObj); } auto colourObj = obj["colours"].toObject(); for (const auto& key : colourObj.keys()) { v.colours[key] = Json::requireString(colourObj.value(key), "colour"); } auto warningsObj = obj["warnings"].toObject(); for (const auto& key : warningsObj.keys()) { v.warnings[key] = Json::requireString(warningsObj.value(key), "warning"); } auto messages = obj["messages"].toObject(); loadVersionMessages(v.messages, messages); auto keeps = obj["keeps"].toObject(); loadVersionKeeps(v.keeps, keeps); auto deletes = obj["deletes"].toObject(); loadVersionDeletes(v.deletes, deletes); } PrismLauncher-11.0.3/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp0000644000175100017510000011741715224505336026346 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 Jamie Mansfield * Copyright 2021 Petr Mrazek * * 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 "ATLPackInstallTask.h" #include #include #include #include "FileSystem.h" #include "Json.h" #include "MMCZip.h" #include "Version.h" #include "meta/Index.h" #include "meta/Version.h" #include "meta/VersionList.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/OneSixVersionFormat.h" #include "minecraft/PackProfile.h" #include "modplatform/atlauncher/ATLPackManifest.h" #include "net/ChecksumValidator.h" #include "settings/INISettingsObject.h" #include "net/ApiDownload.h" #include "Application.h" #include "BuildConfig.h" #include "ui/dialogs/BlockedModsDialog.h" namespace { bool isPathTraversal(const QString& basePath, const QString& entryName) { auto safeName = FS::RemoveInvalidPathChars(entryName); auto fullPath = FS::PathCombine(basePath, safeName); auto baseUrl = QUrl::fromLocalFile(basePath); return !baseUrl.isParentOf(QUrl::fromLocalFile(fullPath)); } Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version) { return APPLICATION->metadataIndex()->getLoadedVersion(uid, version); } } // namespace namespace ATLauncher { PackInstallTask::PackInstallTask(UserInteractionSupport* support, QString packName, QString version, InstallMode installMode) : m_support(support), m_install_mode(installMode), m_pack_name(packName), m_version_name(std::move(version)) { static const QRegularExpression s_regex("[^A-Za-z0-9]"); m_pack_safe_name = packName.replace(s_regex, ""); } bool PackInstallTask::abort() { if (abortable) { return jobPtr->abort(); } return false; } void PackInstallTask::executeTask() { qDebug() << "PackInstallTask::executeTask:" << QThread::currentThreadId(); NetJob::Ptr netJob{ new NetJob("ATLauncher::VersionFetch", APPLICATION->network()) }; auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json").arg(m_pack_safe_name).arg(m_version_name); auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(searchUrl)); netJob->addNetAction(action); connect(netJob.get(), &NetJob::succeeded, this, [this, response] { onDownloadSucceeded(response); }); connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); jobPtr = netJob; jobPtr->start(); } void PackInstallTask::onDownloadSucceeded(QByteArray* responsePtr) { qDebug() << "PackInstallTask::onDownloadSucceeded:" << QThread::currentThreadId(); // NOTE(TheKodeToad): moving the response out to avoid it from being destroyed by jobPtr.reset() QByteArray response = std::move(*responsePtr); jobPtr.reset(); QJsonParseError parseError{}; QJsonDocument doc = QJsonDocument::fromJson(response, &parseError); if (parseError.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from ATLauncher at" << parseError.offset << "reason:" << parseError.errorString(); qWarning() << response; return; } auto obj = doc.object(); ATLauncher::PackVersion version; try { ATLauncher::loadVersion(version, obj); } catch (const JSONValidationError& e) { emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); return; } m_version = version; // Derived from the installation mode QString message; bool resetDirectory = false; switch (m_install_mode) { case InstallMode::Reinstall: case InstallMode::Update: message = m_version.messages.update; resetDirectory = true; break; case InstallMode::Install: message = m_version.messages.install; resetDirectory = false; break; default: emitFailed(tr("Unsupported installation mode")); return; } // Display message if one exists if (!message.isEmpty()) { m_support->displayMessage(message); } auto ver = getComponentVersion("net.minecraft", m_version.minecraft); if (!ver) { emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft", m_version.minecraft)); return; } minecraftVersion = ver; if (resetDirectory) { deleteExistingFiles(); } if (m_version.noConfigs) { downloadMods(); } else { installConfigs(); } } void PackInstallTask::onDownloadFailed(QString reason) { qDebug() << "PackInstallTask::onDownloadFailed:" << QThread::currentThreadId(); jobPtr.reset(); emitFailed(std::move(reason)); } void PackInstallTask::onDownloadAborted() { jobPtr.reset(); emitAborted(); } void PackInstallTask::deleteExistingFiles() { setStatus(tr("Deleting existing files...")); // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/delete VersionDeletes deletes; deletes.folders.append(VersionDelete{ "root", "mods%s%" }); deletes.folders.append(VersionDelete{ "root", "configs%s%" }); deletes.folders.append(VersionDelete{ "root", "bin%s%" }); // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/keep VersionKeeps keeps; keeps.files.append(VersionKeep{ "root", "mods%s%PortalGunSounds.pak" }); keeps.folders.append(VersionKeep{ "root", "mods%s%rei_minimap%s%" }); keeps.folders.append(VersionKeep{ "root", "mods%s%VoxelMods%s%" }); keeps.files.append(VersionKeep{ "root", "config%s%NEI.cfg" }); keeps.files.append(VersionKeep{ "root", "options.txt" }); keeps.files.append(VersionKeep{ "root", "servers.dat" }); // Merge with version deletes and keeps for (const auto& item : m_version.deletes.files) { deletes.files.append(item); } for (const auto& item : m_version.deletes.folders) { deletes.folders.append(item); } for (const auto& item : m_version.keeps.files) { keeps.files.append(item); } for (const auto& item : m_version.keeps.folders) { keeps.folders.append(item); } auto getPathForBase = [this](const QString& base) { auto minecraftPath = FS::PathCombine(m_stagingPath, "minecraft"); if (base == "root") { return minecraftPath; } if (base == "config") { return FS::PathCombine(minecraftPath, "config"); } qWarning() << "Unrecognised base path" << base; return minecraftPath; }; auto convertToSystemPath = [](const QString& path) { auto t = path; t.replace("%s%", QDir::separator()); return t; }; auto shouldKeep = [keeps, getPathForBase, convertToSystemPath](const QString& fullPath) { if (std::ranges::any_of(keeps.files, [&fullPath, &getPathForBase, &convertToSystemPath](const auto& item) { auto basePath = getPathForBase(item.base); auto targetPath = convertToSystemPath(item.target); auto path = FS::PathCombine(basePath, targetPath); return fullPath == path; })) { return true; } if (std::ranges::any_of(keeps.folders, [&fullPath, &getPathForBase, &convertToSystemPath](const auto& item) { auto basePath = getPathForBase(item.base); auto targetPath = convertToSystemPath(item.target); auto path = FS::PathCombine(basePath, targetPath); return fullPath.startsWith(path); })) { return true; } return false; }; // Keep track of files to delete QSet filesToDelete; for (const auto& item : deletes.files) { auto basePath = getPathForBase(item.base); auto targetPath = convertToSystemPath(item.target); auto fullPath = FS::PathCombine(basePath, targetPath); if (shouldKeep(fullPath)) { continue; } filesToDelete.insert(fullPath); } for (const auto& item : deletes.folders) { auto basePath = getPathForBase(item.base); auto targetPath = convertToSystemPath(item.target); auto fullPath = FS::PathCombine(basePath, targetPath); QDirIterator it(fullPath, QDirIterator::Subdirectories); while (it.hasNext()) { auto path = it.next(); if (shouldKeep(path)) { continue; } filesToDelete.insert(path); } } // Delete the files for (const auto& item : filesToDelete) { FS::deletePath(item); } } QString PackInstallTask::getDirForModType(ModType type, const QString& raw) { switch (type) { // Mod types that can either be ignored at this stage, or ignored // completely. case ModType::Root: case ModType::Extract: case ModType::Decomp: case ModType::TexturePackExtract: case ModType::ResourcePackExtract: case ModType::MCPC: return Q_NULLPTR; case ModType::Forge: // Forge detection happens later on, if it cannot be detected it will // install a jarmod component. case ModType::Jar: return "jarmods"; case ModType::Mods: return "mods"; case ModType::Flan: return "Flan"; case ModType::Dependency: return FS::PathCombine("mods", m_version.minecraft); case ModType::Ic2Lib: return FS::PathCombine("mods", "ic2"); case ModType::DenLib: return FS::PathCombine("mods", "denlib"); case ModType::Coremods: return "coremods"; case ModType::Plugins: return "plugins"; case ModType::TexturePack: return "texturepacks"; case ModType::ResourcePack: return "resourcepacks"; case ModType::ShaderPack: return "shaderpacks"; case ModType::Millenaire: qWarning() << "Unsupported mod type: " + raw; return Q_NULLPTR; case ModType::Unknown: emitFailed(tr("Unknown mod type: %1").arg(raw)); return Q_NULLPTR; } return Q_NULLPTR; } QString PackInstallTask::getVersionForLoader(const QString& uid) { if (m_version.loader.recommended || m_version.loader.latest || m_version.loader.choose) { auto vlist = APPLICATION->metadataIndex()->get(uid); if (!vlist) { emitFailed(tr("Failed to get local metadata index for %1").arg(uid)); return Q_NULLPTR; } vlist->waitToLoad(); if (m_version.loader.recommended || m_version.loader.latest) { for (int i = 0; i < vlist->versions().size(); i++) { auto version = vlist->versions().at(i); auto reqs = version->requiredSet(); // filter by minecraft version, if the loader depends on a certain version. // not all mod loaders depend on a given Minecraft version, so we won't do this // filtering for those loaders. if (m_version.loader.type != "fabric") { auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require& req) { return req.uid == "net.minecraft"; }); if (iter == reqs.end()) { continue; } if (iter->equalsVersion != m_version.minecraft) { continue; } } if (m_version.loader.recommended) { // first recommended build we find, we use. if (!version->isRecommended()) { continue; } } return version->descriptor(); } emitFailed(tr("Failed to find version for %1 loader").arg(m_version.loader.type)); return Q_NULLPTR; } if (m_version.loader.choose) { // Fabric Loader doesn't depend on a given Minecraft version. if (m_version.loader.type == "fabric") { return m_support->chooseVersion(vlist, Q_NULLPTR); } return m_support->chooseVersion(vlist, m_version.minecraft); } } if (m_version.loader.version == Q_NULLPTR || m_version.loader.version.isEmpty()) { emitFailed(tr("No loader version set for modpack!")); return Q_NULLPTR; } return m_version.loader.version; } QString PackInstallTask::detectLibrary(const VersionLibrary& library) { // Try to detect what the library is if (!library.server.isEmpty() && library.server.split("/").length() >= 3) { auto lastSlash = library.server.lastIndexOf("/"); auto locationAndVersion = library.server.mid(0, lastSlash); auto fileName = library.server.mid(lastSlash + 1); lastSlash = locationAndVersion.lastIndexOf("/"); auto location = locationAndVersion.mid(0, lastSlash); auto version = locationAndVersion.mid(lastSlash + 1); lastSlash = location.lastIndexOf("/"); auto group = location.mid(0, lastSlash).replace("/", "."); auto artefact = location.mid(lastSlash + 1); return group + ":" + artefact + ":" + version; } if (library.file.contains("-")) { auto lastSlash = library.file.lastIndexOf("-"); auto name = library.file.mid(0, lastSlash); auto version = library.file.mid(lastSlash + 1).remove(".jar"); if (name == QString("guava")) { return "com.google.guava:guava:" + version; } if (name == QString("commons-lang3")) { return "org.apache.commons:commons-lang3:" + version; } } return "org.multimc.atlauncher:" + library.md5 + ":1"; } bool PackInstallTask::createLibrariesComponent(const QString& instanceRoot, PackProfile* profile) { if (m_version.libraries.isEmpty()) { return true; } QList exempt; for (const auto& componentUid : componentsToInstall.keys()) { auto componentVersion = componentsToInstall.value(componentUid); if (componentVersion->data()) { for (const auto& library : componentVersion->data()->libraries) { GradleSpecifier lib(library->rawName()); exempt.append(lib); } } } if (minecraftVersion->data()) { for (const auto& library : minecraftVersion->data()->libraries) { GradleSpecifier lib(library->rawName()); exempt.append(lib); } } auto id = QUuid::createUuid().toString(QUuid::WithoutBraces); auto targetId = "org.multimc.atlauncher." + id; auto patchDir = FS::PathCombine(instanceRoot, "patches"); if (!FS::ensureFolderPathExists(patchDir)) { return false; } auto patchFileName = FS::PathCombine(patchDir, targetId + ".json"); auto f = std::make_shared(); f->name = m_pack_name + " " + m_version_name + " (libraries)"; const static QMap s_liteLoaderMap = { { "61179803bcd5fb7790789b790908663d", "1.12-SNAPSHOT" }, { "1420785ecbfed5aff4a586c5c9dd97eb", "1.12.2-SNAPSHOT" }, { "073f68e2fcb518b91fd0d99462441714", "1.6.2_03" }, { "10a15b52fc59b1bfb9c05b56de1097d6", "1.6.2_02" }, { "b52f90f08303edd3d4c374e268a5acf1", "1.6.2_04" }, { "ea747e24e03e24b7cad5bc8a246e0319", "1.6.2_01" }, { "55785ccc82c07ff0ba038fe24be63ea2", "1.7.10_01" }, { "63ada46e033d0cb6782bada09ad5ca4e", "1.7.10_04" }, { "7983e4b28217c9ae8569074388409c86", "1.7.10_03" }, { "c09882458d74fe0697c7681b8993097e", "1.7.10_02" }, { "db7235aefd407ac1fde09a7baba50839", "1.7.10_00" }, { "6e9028816027f53957bd8fcdfabae064", "1.8" }, { "5e732dc446f9fe2abe5f9decaec40cde", "1.10-SNAPSHOT" }, { "3a98b5ed95810bf164e71c1a53be568d", "1.11.2-SNAPSHOT" }, { "ba8e6285966d7d988a96496f48cbddaa", "1.8.9-SNAPSHOT" }, { "8524af3ac3325a82444cc75ae6e9112f", "1.11-SNAPSHOT" }, { "53639d52340479ccf206a04f5e16606f", "1.5.2_01" }, { "1fcdcf66ce0a0806b7ad8686afdce3f7", "1.6.4_00" }, { "531c116f71ae2b11033f9a11a0f8e668", "1.6.4_01" }, { "4009eeb99c9068f608d3483a6439af88", "1.7.2_03" }, { "66f343354b8417abce1a10d557d2c6e9", "1.7.2_04" }, { "ab554c21f28fbc4ae9b098bcb5f4cceb", "1.7.2_05" }, { "e1d76a05a3723920e2f80a5e66c45f16", "1.7.2_02" }, { "00318cb0c787934d523f63cdfe8ddde4", "1.9-SNAPSHOT" }, { "986fd1ee9525cb0dcab7609401cef754", "1.9.4-SNAPSHOT" }, { "571ad5e6edd5ff40259570c9be588bb5", "1.9.4" }, { "1cdd72f7232e45551f16cc8ffd27ccf3", "1.10.2-SNAPSHOT" }, { "8a7c21f32d77ee08b393dd3921ced8eb", "1.10.2" }, { "b9bef8abc8dc309069aeba6fbbe58980", "1.12.1-SNAPSHOT" } }; for (const auto& lib : m_version.libraries) { // If the library is LiteLoader, we need to ignore it and handle it separately. if (s_liteLoaderMap.contains(lib.md5)) { auto ver = getComponentVersion("com.mumfrey.liteloader", s_liteLoaderMap.value(lib.md5)); if (ver) { componentsToInstall.insert("com.mumfrey.liteloader", ver); continue; } } auto libName = detectLibrary(lib); GradleSpecifier libSpecifier(libName); bool libExempt = false; for (const auto& existingLib : exempt) { if (libSpecifier.matchName(existingLib)) { // If the pack specifies a newer version of the lib, use that! libExempt = Version(libSpecifier.version()) >= Version(existingLib.version()); } } if (libExempt) { continue; } auto library = std::make_shared(); library->setRawName(libName); switch (lib.download) { case DownloadType::Server: library->setAbsoluteUrl(BuildConfig.ATL_DOWNLOAD_SERVER_URL + lib.url); break; case DownloadType::Direct: library->setAbsoluteUrl(lib.url); break; case DownloadType::Browser: case DownloadType::Unknown: emitFailed(tr("Unknown or unsupported download type: %1").arg(lib.download_raw)); return false; } f->libraries.append(library); } if (f->libraries.isEmpty()) { return true; } QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { qCritical() << "Error opening" << file.fileName() << "for reading:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); profile->appendComponent(ComponentPtr{ new Component(profile, targetId, f) }); return true; } bool PackInstallTask::createPackComponent(const QString& instanceRoot, PackProfile* profile) { if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.arguments.isEmpty()) { return true; } auto mainClass = m_version.mainClass.mainClass; auto extraArguments = m_version.extraArguments.arguments; auto hasMainClassDepends = !m_version.mainClass.depends.isEmpty(); auto hasExtraArgumentsDepends = !m_version.extraArguments.depends.isEmpty(); if (hasMainClassDepends || hasExtraArgumentsDepends) { QSet mods; for (const auto& item : m_version.mods) { mods.insert(item.name); } if (hasMainClassDepends && !mods.contains(m_version.mainClass.depends)) { mainClass = ""; } if (hasExtraArgumentsDepends && !mods.contains(m_version.extraArguments.depends)) { extraArguments = ""; } } if (mainClass.isEmpty() && extraArguments.isEmpty()) { return true; } auto id = QUuid::createUuid().toString(QUuid::WithoutBraces); auto targetId = "org.multimc.atlauncher." + id; auto patchDir = FS::PathCombine(instanceRoot, "patches"); if (!FS::ensureFolderPathExists(patchDir)) { return false; } auto patchFileName = FS::PathCombine(patchDir, targetId + ".json"); QStringList mainClasses; QStringList tweakers; for (const auto& componentUid : componentsToInstall.keys()) { auto componentVersion = componentsToInstall.value(componentUid); if (componentVersion->data()) { if (componentVersion->data()->mainClass != QString("")) { mainClasses.append(componentVersion->data()->mainClass); } tweakers.append(componentVersion->data()->addTweakers); } } auto f = std::make_shared(); f->name = m_pack_name + " " + m_version_name; if (!mainClass.isEmpty() && !mainClasses.contains(mainClass)) { f->mainClass = mainClass; } // Parse out tweakers auto args = extraArguments.split(" "); QString previous; for (auto arg : args) { if (arg.startsWith("--tweakClass=") || previous == "--tweakClass") { auto tweakClass = arg.remove("--tweakClass="); if (tweakers.contains(tweakClass)) { continue; } f->addTweakers.append(tweakClass); } previous = arg; } if (f->mainClass == QString() && f->addTweakers.isEmpty()) { return true; } QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { qCritical() << "Error opening" << file.fileName() << "for writing:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); profile->appendComponent(ComponentPtr{ new Component(profile, targetId, f) }); return true; } void PackInstallTask::installConfigs() { qDebug() << "PackInstallTask::installConfigs:" << QThread::currentThreadId(); setStatus(tr("Downloading configs...")); jobPtr.reset(new NetJob(tr("Config download"), APPLICATION->network())); auto path = QString("Configs/%1/%2.zip").arg(m_pack_safe_name).arg(m_version_name); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip").arg(m_pack_safe_name).arg(m_version_name); auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", path); entry->setStale(true); auto dl = Net::ApiDownload::makeCached(url, entry); if (!m_version.configs.sha1.isEmpty()) { dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, m_version.configs.sha1)); } jobPtr->addNetAction(dl); archivePath = entry->getFullPath(); connect(jobPtr.get(), &NetJob::succeeded, this, [this]() { abortable = false; jobPtr.reset(); extractConfigs(); }); connect(jobPtr.get(), &NetJob::failed, [this](QString reason) { abortable = false; jobPtr.reset(); emitFailed(std::move(reason)); }); connect(jobPtr.get(), &NetJob::progress, [this](qint64 current, qint64 total) { abortable = true; setProgress(current, total); }); connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress); connect(jobPtr.get(), &NetJob::aborted, [this] { abortable = false; jobPtr.reset(); emitAborted(); }); jobPtr->start(); } void PackInstallTask::extractConfigs() { qDebug() << "PackInstallTask::extractConfigs:" << QThread::currentThreadId(); setStatus(tr("Extracting configs...")); QDir extractDir(m_stagingPath); m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/minecraft"); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, [this]() { downloadMods(); }); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, [this]() { emitAborted(); }); m_extractFutureWatcher.setFuture(m_extractFuture); } void PackInstallTask::downloadMods() { qDebug() << "PackInstallTask::installMods:" << QThread::currentThreadId(); QList optionalMods; for (const auto& mod : m_version.mods) { if (mod.optional) { optionalMods.push_back(mod); } } // Select optional mods, if pack contains any QList selectedMods; if (!optionalMods.isEmpty()) { setStatus(tr("Selecting optional mods...")); auto mods = m_support->chooseOptionalMods(m_version, optionalMods); if (!mods.has_value()) { emitAborted(); return; } selectedMods = mods.value(); } setStatus(tr("Downloading mods...")); jarmods.clear(); jobPtr.reset(new NetJob(tr("Mod download"), APPLICATION->network())); QList blockedMods; for (const auto& mod : m_version.mods) { // skip non-client mods if (!mod.client) { continue; } // skip optional mods that were not selected if (mod.optional && !selectedMods.contains(mod.name)) { continue; } QString url; switch (mod.download) { case DownloadType::Server: url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url; break; case DownloadType::Browser: { blockedMods.append(mod); continue; } case DownloadType::Direct: url = mod.url; break; case DownloadType::Unknown: emitFailed(tr("Unknown download type: %1").arg(mod.download_raw)); return; } QFileInfo fileName(mod.file); auto cacheName = fileName.completeBaseName() + "-" + mod.md5 + "." + fileName.suffix(); if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); entry->setStale(true); modsToExtract.insert(entry->getFullPath(), mod); auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } jobPtr->addNetAction(dl); } else if (mod.type == ModType::Decomp) { auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); entry->setStale(true); modsToDecomp.insert(entry->getFullPath(), mod); auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } jobPtr->addNetAction(dl); } else { auto relpath = getDirForModType(mod.type, mod.type_raw); if (relpath == Q_NULLPTR) { continue; } auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); entry->setStale(true); auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } jobPtr->addNetAction(dl); auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); if (mod.type == ModType::Forge) { auto ver = getComponentVersion("net.minecraftforge", mod.version); if (ver) { componentsToInstall.insert("net.minecraftforge", ver); continue; } qDebug() << "Jarmod: " + path; jarmods.push_back(path); } if (mod.type == ModType::Jar) { qDebug() << "Jarmod: " + path; jarmods.push_back(path); } // Download after Forge handling, to avoid downloading Forge twice. qDebug() << "Will download" << url << "to" << path; modsToCopy[entry->getFullPath()] = path; } } if (!blockedMods.isEmpty()) { QList mods; for (const auto& mod : blockedMods) { BlockedMod blockedMod; blockedMod.name = mod.file; blockedMod.websiteUrl = mod.url; blockedMod.hash = mod.md5; blockedMod.matched = false; blockedMod.localPath = ""; mods.append(blockedMod); } qWarning() << "Blocked mods found, displaying mod list"; BlockedModsDialog messageDialog(nullptr, tr("Blocked mods found"), tr("The following files are not available for download in third party launchers.
    " "You will need to manually download them and add them to the instance."), mods, "md5"); messageDialog.setModal(true); if (messageDialog.exec() != 0) { qDebug() << "Post dialog blocked mods list:" << mods; for (const auto& blocked : mods) { if (!blocked.matched) { qDebug() << blocked.name << "was not matched to a local file, skipping copy"; continue; } auto modIter = std::ranges::find_if(blockedMods, [blocked](const VersionMod& mod) { return mod.url == blocked.websiteUrl; }); if (modIter == blockedMods.end()) { continue; } const auto& mod = *modIter; if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { modsToExtract.insert(blocked.localPath, mod); } else if (mod.type == ModType::Decomp) { modsToDecomp.insert(blocked.localPath, mod); } else { auto relpath = getDirForModType(mod.type, mod.type_raw); if (relpath == Q_NULLPTR) { continue; } auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); if (mod.type == ModType::Forge) { auto ver = getComponentVersion("net.minecraftforge", mod.version); if (ver) { componentsToInstall.insert("net.minecraftforge", ver); continue; } qDebug() << "Jarmod: " + path; jarmods.push_back(path); } if (mod.type == ModType::Jar) { qDebug() << "Jarmod: " + path; jarmods.push_back(path); } modsToCopy[blocked.localPath] = path; } } } else { emitFailed(tr("Unknown download type: %1").arg("browser")); return; } } connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded); connect(jobPtr.get(), &NetJob::progress, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); abortable = true; setProgress(current, total); }); connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress); connect(jobPtr.get(), &NetJob::aborted, &PackInstallTask::emitAborted); connect(jobPtr.get(), &NetJob::failed, &PackInstallTask::emitFailed); jobPtr->start(); } void PackInstallTask::onModsDownloaded() { abortable = false; qDebug() << "PackInstallTask::onModsDownloaded:" << QThread::currentThreadId(); jobPtr.reset(); if (!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) { m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), &PackInstallTask::extractMods, this, modsToExtract, modsToDecomp, modsToCopy); connect(&m_modExtractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onModsExtracted); connect(&m_modExtractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::emitAborted); m_modExtractFutureWatcher.setFuture(m_modExtractFuture); } else { install(); } } void PackInstallTask::onModsExtracted() { qDebug() << "PackInstallTask::onModsExtracted:" << QThread::currentThreadId(); if (m_modExtractFuture.result()) { install(); } else { emitFailed(tr("Failed to extract mods...")); } } bool PackInstallTask::extractMods(const QMap& toExtract, const QMap& toDecomp, const QMap& toCopy) { qDebug() << "PackInstallTask::extractMods:" << QThread::currentThreadId(); setStatus(tr("Extracting mods...")); for (auto iter = toExtract.begin(); iter != toExtract.end(); iter++) { const auto& modPath = iter.key(); const auto& mod = iter.value(); QString extractToDir; if (mod.type == ModType::Extract) { extractToDir = getDirForModType(mod.extractTo, mod.extractTo_raw); } else if (mod.type == ModType::TexturePackExtract) { extractToDir = FS::PathCombine("texturepacks", "extracted"); } else if (mod.type == ModType::ResourcePackExtract) { extractToDir = FS::PathCombine("resourcepacks", "extracted"); } QDir extractDir(m_stagingPath); auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir); QString folderToExtract = ""; if (mod.type == ModType::Extract) { folderToExtract = mod.extractFolder; static const QRegularExpression s_regex("^/"); folderToExtract.remove(s_regex); if (isPathTraversal(extractToPath, folderToExtract)) { qWarning() << "Blocked path traversal in" << mod.extractFolder; return false; } } qDebug() << "Extracting " + mod.file + " to " + extractToDir; if (!MMCZip::extractDir(modPath, folderToExtract, extractToPath)) { // assume error return false; } } for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) { const auto& modPath = iter.key(); const auto& mod = iter.value(); auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw); QDir extractDir(m_stagingPath); auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile); if (isPathTraversal(extractToPath, mod.decompFile)) { qWarning() << "Blocked path traversal in decompFile" << mod.decompFile; return false; } qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir; if (!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) { qWarning() << "Failed to extract" << mod.decompFile; return false; } } for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) { const auto& from = iter.key(); const auto& to = iter.value(); // If the file already exists, assume the mod is the correct copy - and remove // the copy from the Configs.zip QFileInfo fileInfo(to); if (fileInfo.exists()) { if (!FS::deletePath(to)) { qWarning() << "Failed to delete" << to; return false; } } FS::copy fileCopyOperation(from, to); if (!fileCopyOperation()) { qWarning() << "Failed to copy" << from << "to" << to; return false; } } return true; } void PackInstallTask::install() { qDebug() << "PackInstallTask::install:" << QThread::currentThreadId(); setStatus(tr("Installing modpack")); auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); MinecraftInstance instance(m_globalSettings, std::make_unique(instanceConfigPath), m_stagingPath); { SettingsObject::Lock lock(instance.settings()); auto* components = instance.getPackProfile(); components->buildingFromScratch(); // Use a component to add libraries BEFORE Minecraft if (!createLibrariesComponent(instance.instanceRoot(), components)) { emitFailed(tr("Failed to create libraries component")); return; } // Minecraft components->setComponentVersion("net.minecraft", m_version.minecraft, true); // Loader if (m_version.loader.type == QString("forge")) { auto version = getVersionForLoader("net.minecraftforge"); if (version == Q_NULLPTR) { return; } components->setComponentVersion("net.minecraftforge", version); } else if (m_version.loader.type == QString("neoforge")) { auto version = getVersionForLoader("net.neoforged"); if (version == Q_NULLPTR) { return; } components->setComponentVersion("net.neoforged", version); } else if (m_version.loader.type == QString("fabric")) { auto version = getVersionForLoader("net.fabricmc.fabric-loader"); if (version == Q_NULLPTR) { return; } components->setComponentVersion("net.fabricmc.fabric-loader", version); } else if (m_version.loader.type != QString()) { emitFailed(tr("Unknown loader type: ") + m_version.loader.type); return; } for (const auto& componentUid : componentsToInstall.keys()) { auto version = componentsToInstall.value(componentUid); components->setComponentVersion(componentUid, version->version()); } components->installJarMods(jarmods); // Use a component to fill in the rest of the data // todo: use more detection if (!createPackComponent(instance.instanceRoot(), components)) { emitFailed(tr("Failed to create pack component")); return; } components->saveNow(); instance.setName(name()); instance.setIconKey(m_instIcon); instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name); jarmods.clear(); } emitSucceeded(); } } // namespace ATLauncher PrismLauncher-11.0.3/launcher/modplatform/atlauncher/ATLPackIndex.cpp0000644000175100017510000000347515224505336025162 0ustar runnerrunner/* * Copyright 2020-2021 Jamie Mansfield * Copyright 2021 Petr Mrazek * * 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 "ATLPackIndex.h" #include #include "Json.h" static void loadIndexedVersion(ATLauncher::IndexedVersion& v, QJsonObject& obj) { v.version = Json::requireString(obj, "version"); v.minecraft = Json::requireString(obj, "minecraft"); } void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack& m, QJsonObject& obj) { m.id = Json::requireInteger(obj, "id"); m.position = Json::requireInteger(obj, "position"); m.name = Json::requireString(obj, "name"); m.type = Json::requireString(obj, "type") == "private" ? ATLauncher::PackType::Private : ATLauncher::PackType::Public; auto versionsArr = Json::requireArray(obj, "versions"); for (const auto versionRaw : versionsArr) { auto versionObj = Json::requireObject(versionRaw); ATLauncher::IndexedVersion version; loadIndexedVersion(version, versionObj); m.versions.append(version); } m.system = obj["system"].toBool(); m.description = obj["description"].toString(""); static const QRegularExpression s_regex("[^A-Za-z0-9]"); m.safeName = Json::requireString(obj, "name").replace(s_regex, "").toLower() + ".png"; } PrismLauncher-11.0.3/launcher/modplatform/atlauncher/ATLPackManifest.h0000644000175100017510000000776115224505336025330 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020 Jamie Mansfield * * 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 #include #include #include namespace ATLauncher { enum class PackType { Public, Private }; enum class ModType { Root, Forge, Jar, Mods, Flan, Dependency, Ic2Lib, DenLib, Coremods, MCPC, Plugins, Extract, Decomp, TexturePack, ResourcePack, ShaderPack, TexturePackExtract, ResourcePackExtract, Millenaire, Unknown }; enum class DownloadType { Server, Browser, Direct, Unknown }; struct VersionLoader { QString type; bool latest; bool recommended; bool choose; QString version; }; struct VersionLibrary { QString url; QString file; QString server; QString md5; DownloadType download; QString download_raw; }; struct VersionMod { QString name; QString version; QString url; QString file; QString md5; DownloadType download; QString download_raw; ModType type; QString type_raw; ModType extractTo; QString extractTo_raw; QString extractFolder; ModType decompType; QString decompType_raw; QString decompFile; QString description; bool optional; bool recommended; bool selected; bool hidden; bool library; QString group; QStringList depends; QString colour; QString warning; bool client; // computed bool effectively_hidden; }; struct VersionConfigs { int filesize; QString sha1; }; struct VersionMessages { QString install; QString update; }; struct VersionKeep { QString base; QString target; }; struct VersionKeeps { QList files; QList folders; }; struct VersionDelete { QString base; QString target; }; struct VersionDeletes { QList files; QList folders; }; struct PackVersionMainClass { QString mainClass; QString depends; }; struct PackVersionExtraArguments { QString arguments; QString depends; }; struct PackVersion { QString version; QString minecraft; bool noConfigs; PackVersionMainClass mainClass; PackVersionExtraArguments extraArguments; VersionLoader loader; QList libraries; QList mods; VersionConfigs configs; QMap colours; QMap warnings; VersionMessages messages; VersionKeeps keeps; VersionDeletes deletes; }; void loadVersion(PackVersion& v, QJsonObject& obj); } // namespace ATLauncher PrismLauncher-11.0.3/launcher/modplatform/ftb/0000755000175100017510000000000015224505336020663 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/modplatform/ftb/FTBPackManifest.h0000644000175100017510000000663115224505336023743 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 Jamie Mansfield * Copyright 2020 Petr Mrazek * * 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 #include #include #include #include namespace FTB { struct Specs { int id; int minimum; int recommended; }; struct Tag { int id; QString name; }; struct Art { int id; QString url; QString type; int width; int height; bool compressed; QString sha1; int size; int64_t updated; }; struct Author { int id; QString name; QString type; QString website; int64_t updated; }; struct VersionInfo { int id; QString name; QString type; int64_t updated; Specs specs; }; struct Modpack { int id; QString name; QString synopsis; QString description; QString type; bool featured; int installs; int plays; int64_t updated; int64_t refreshed; QVector art; QVector authors; QVector versions; QVector tags; QString safeName; }; struct VersionTarget { int id; QString type; QString name; QString version; int64_t updated; }; struct VersionFileCurseForge { int project_id; int file_id; }; struct VersionFile { int id; QString type; QString path; QString name; QString version; QString url; QString sha1; int size; bool clientOnly; bool serverOnly; bool optional; int64_t updated; VersionFileCurseForge curseforge; }; struct Version { int id; int parent; QString name; QString type; int installs; int plays; int64_t updated; int64_t refreshed; Specs specs; QVector targets; QVector files; }; struct VersionChangelog { QString content; int64_t updated; }; void loadModpack(Modpack& m, QJsonObject& obj); void loadVersion(Version& m, QJsonObject& obj); } // namespace FTB Q_DECLARE_METATYPE(FTB::Modpack) PrismLauncher-11.0.3/launcher/modplatform/ftb/FTBPackInstallTask.h0000644000175100017510000000573215224505336024427 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 Jamie Mansfield * Copyright 2020-2021 Petr Mrazek * * 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 "FTBPackManifest.h" #include "InstanceTask.h" #include "QObjectPtr.h" #include "modplatform/flame/FileResolvingTask.h" #include "net/NetJob.h" #include "ui/dialogs/BlockedModsDialog.h" #include namespace FTB { class PackInstallTask final : public InstanceTask { Q_OBJECT public: explicit PackInstallTask(Modpack pack, QString version, QWidget* parent = nullptr); ~PackInstallTask() override = default; bool abort() override; protected: void executeTask() override; private slots: void onManifestDownloadSucceeded(QByteArray* responsePtr); void onResolveModsSucceeded(); void onCreateInstanceSucceeded(); void onModDownloadSucceeded(); void onManifestDownloadFailed(QString reason); void onResolveModsFailed(QString reason); void onCreateInstanceFailed(QString reason); void onModDownloadFailed(QString reason); private: void resolveMods(); void createInstance(); void downloadPack(); void copyBlockedMods(); private: NetJob::Ptr m_net_job = nullptr; shared_qobject_ptr m_modIdResolverTask = nullptr; QList m_fileIds; Modpack m_pack; QString m_versionName; Version m_version; QMap m_filesToCopy; QList m_blockedMods; // FIXME: nuke QWidget* m_parent; }; } // namespace FTB PrismLauncher-11.0.3/launcher/modplatform/ftb/FTBPackManifest.cpp0000644000175100017510000001575215224505336024302 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020 Jamie Mansfield * Copyright 2020-2021 Petr Mrazek * * 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 "FTBPackManifest.h" #include "Json.h" static void loadSpecs(FTB::Specs& s, QJsonObject& obj) { s.id = Json::requireInteger(obj, "id"); s.minimum = Json::requireInteger(obj, "minimum"); s.recommended = Json::requireInteger(obj, "recommended"); } static void loadTag(FTB::Tag& t, QJsonObject& obj) { t.id = Json::requireInteger(obj, "id"); t.name = Json::requireString(obj, "name"); } static void loadArt(FTB::Art& a, QJsonObject& obj) { a.id = Json::requireInteger(obj, "id"); a.url = Json::requireString(obj, "url"); a.type = Json::requireString(obj, "type"); a.width = Json::requireInteger(obj, "width"); a.height = Json::requireInteger(obj, "height"); a.compressed = Json::requireBoolean(obj, "compressed"); a.sha1 = Json::requireString(obj, "sha1"); a.size = obj["size"].toInt(); a.updated = Json::requireInteger(obj, "updated"); } static void loadAuthor(FTB::Author& a, QJsonObject& obj) { a.id = Json::requireInteger(obj, "id"); a.name = Json::requireString(obj, "name"); a.type = Json::requireString(obj, "type"); a.website = Json::requireString(obj, "website"); a.updated = Json::requireInteger(obj, "updated"); } static void loadVersionInfo(FTB::VersionInfo& v, QJsonObject& obj) { v.id = Json::requireInteger(obj, "id"); v.name = Json::requireString(obj, "name"); v.type = Json::requireString(obj, "type"); v.updated = Json::requireInteger(obj, "updated"); auto specs = Json::requireObject(obj, "specs"); loadSpecs(v.specs, specs); } void FTB::loadModpack(FTB::Modpack& m, QJsonObject& obj) { m.id = Json::requireInteger(obj, "id"); m.name = Json::requireString(obj, "name"); m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), "").toLower() + ".png"; m.synopsis = Json::requireString(obj, "synopsis"); m.description = Json::requireString(obj, "description"); m.type = Json::requireString(obj, "type"); m.featured = Json::requireBoolean(obj, "featured"); m.installs = Json::requireInteger(obj, "installs"); m.plays = Json::requireInteger(obj, "plays"); m.updated = Json::requireInteger(obj, "updated"); m.refreshed = obj["refreshed"].toInt(); auto artArr = Json::requireArray(obj, "art"); for (QJsonValueRef artRaw : artArr) { auto artObj = Json::requireObject(artRaw); FTB::Art art; loadArt(art, artObj); m.art.append(art); } auto authorArr = Json::requireArray(obj, "authors"); for (QJsonValueRef authorRaw : authorArr) { auto authorObj = Json::requireObject(authorRaw); FTB::Author author; loadAuthor(author, authorObj); m.authors.append(author); } auto versionArr = Json::requireArray(obj, "versions"); for (QJsonValueRef versionRaw : versionArr) { auto versionObj = Json::requireObject(versionRaw); FTB::VersionInfo version; loadVersionInfo(version, versionObj); m.versions.append(version); } auto tagArr = Json::requireArray(obj, "tags"); for (QJsonValueRef tagRaw : tagArr) { auto tagObj = Json::requireObject(tagRaw); FTB::Tag tag; loadTag(tag, tagObj); m.tags.append(tag); } m.updated = Json::requireInteger(obj, "updated"); } static void loadVersionTarget(FTB::VersionTarget& a, QJsonObject& obj) { a.id = Json::requireInteger(obj, "id"); a.name = Json::requireString(obj, "name"); a.type = Json::requireString(obj, "type"); a.version = Json::requireString(obj, "version"); a.updated = Json::requireInteger(obj, "updated"); } static void loadVersionFile(FTB::VersionFile& a, QJsonObject& obj) { a.id = Json::requireInteger(obj, "id"); a.type = Json::requireString(obj, "type"); a.path = Json::requireString(obj, "path"); a.name = Json::requireString(obj, "name"); a.version = Json::requireString(obj, "version"); a.url = obj["url"].toString(); // optional a.sha1 = Json::requireString(obj, "sha1"); a.size = obj["size"].toInt(); a.clientOnly = Json::requireBoolean(obj, "clientonly"); a.serverOnly = Json::requireBoolean(obj, "serveronly"); a.optional = Json::requireBoolean(obj, "optional"); a.updated = Json::requireInteger(obj, "updated"); auto curseforgeObj = obj["curseforge"].toObject(); // optional a.curseforge.project_id = curseforgeObj["project"].toInt(); a.curseforge.file_id = curseforgeObj["file"].toInt(); } void FTB::loadVersion(FTB::Version& m, QJsonObject& obj) { m.id = Json::requireInteger(obj, "id"); m.parent = Json::requireInteger(obj, "parent"); m.name = Json::requireString(obj, "name"); m.type = Json::requireString(obj, "type"); m.installs = Json::requireInteger(obj, "installs"); m.plays = Json::requireInteger(obj, "plays"); m.updated = Json::requireInteger(obj, "updated"); m.refreshed = obj["refreshed"].toInt(); auto specs = Json::requireObject(obj, "specs"); loadSpecs(m.specs, specs); auto targetArr = Json::requireArray(obj, "targets"); for (QJsonValueRef targetRaw : targetArr) { auto versionObj = Json::requireObject(targetRaw); FTB::VersionTarget target; loadVersionTarget(target, versionObj); m.targets.append(target); } auto fileArr = Json::requireArray(obj, "files"); for (QJsonValueRef fileRaw : fileArr) { auto fileObj = Json::requireObject(fileRaw); FTB::VersionFile file; loadVersionFile(file, fileObj); m.files.append(file); } } PrismLauncher-11.0.3/launcher/modplatform/ftb/FTBPackInstallTask.cpp0000644000175100017510000003151215224505336024755 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 flowln * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 Jamie Mansfield * Copyright 2020-2021 Petr Mrazek * * 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 "FTBPackInstallTask.h" #include "FileSystem.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/PackManifest.h" #include "net/ChecksumValidator.h" #include "settings/INISettingsObject.h" #include "Application.h" #include "BuildConfig.h" #include "ui/dialogs/BlockedModsDialog.h" namespace FTB { PackInstallTask::PackInstallTask(Modpack pack, QString version, QWidget* parent) : m_pack(std::move(pack)), m_versionName(std::move(version)), m_parent(parent) {} bool PackInstallTask::abort() { if (!canAbort()) return false; bool aborted = true; if (m_net_job) aborted &= m_net_job->abort(); if (m_modIdResolverTask) aborted &= m_modIdResolverTask->abort(); return aborted ? InstanceTask::abort() : false; } void PackInstallTask::executeTask() { setStatus(tr("Getting the manifest...")); setAbortable(false); // Find pack version auto version_it = std::find_if(m_pack.versions.constBegin(), m_pack.versions.constEnd(), [this](const FTB::VersionInfo& a) { return a.name == m_versionName; }); if (version_it == m_pack.versions.constEnd()) { emitFailed(tr("Failed to find pack version %1").arg(m_versionName)); return; } auto version = *version_it; auto netJob = makeShared("FTB::VersionFetch", APPLICATION->network()); auto searchUrl = QString(BuildConfig.FTB_API_BASE_URL + "/modpack/%1/%2").arg(m_pack.id).arg(version.id); auto [action, response] = Net::Download::makeByteArray(QUrl(searchUrl)); netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::succeeded, this, [this, response] { onManifestDownloadSucceeded(response); }); QObject::connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onManifestDownloadFailed); QObject::connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::abort); QObject::connect(netJob.get(), &NetJob::progress, this, &PackInstallTask::setProgress); m_net_job = netJob; setAbortable(true); netJob->start(); } void PackInstallTask::onManifestDownloadSucceeded(QByteArray* responsePtr) { // NOTE(TheKodeToad): moving the response out to avoid it from being destroyed by m_net_job.reset() QByteArray response = std::move(*responsePtr); m_net_job.reset(); QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } FTB::Version version; try { auto obj = Json::requireObject(doc); FTB::loadVersion(version, obj); } catch (const JSONValidationError& e) { emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); return; } m_version = version; resolveMods(); } void PackInstallTask::resolveMods() { setStatus(tr("Resolving mods...")); setAbortable(false); setProgress(0, 100); m_fileIds.clear(); Flame::Manifest manifest; for (const auto& file : m_version.files) { if (!file.serverOnly && file.url.isEmpty()) { if (file.curseforge.file_id <= 0) { emitFailed(tr("Invalid manifest: There's no information available to download the file '%1'!").arg(file.name)); return; } Flame::File flameFile; flameFile.projectId = file.curseforge.project_id; flameFile.fileId = file.curseforge.file_id; manifest.files.insert(flameFile.fileId, flameFile); m_fileIds.append(flameFile.fileId); } else { m_fileIds.append(-1); } } m_modIdResolverTask.reset(new Flame::FileResolvingTask(manifest)); connect(m_modIdResolverTask.get(), &Flame::FileResolvingTask::succeeded, this, &PackInstallTask::onResolveModsSucceeded); connect(m_modIdResolverTask.get(), &Flame::FileResolvingTask::failed, this, &PackInstallTask::onResolveModsFailed); connect(m_modIdResolverTask.get(), &Flame::FileResolvingTask::aborted, this, &PackInstallTask::abort); connect(m_modIdResolverTask.get(), &Flame::FileResolvingTask::progress, this, &PackInstallTask::setProgress); setAbortable(true); m_modIdResolverTask->start(); } void PackInstallTask::onResolveModsSucceeded() { auto anyBlocked = false; Flame::Manifest results = m_modIdResolverTask->getResults(); for (int index = 0; index < m_fileIds.size(); index++) { const auto file_id = m_fileIds.at(index); if (file_id < 0) continue; Flame::File resultsFile = results.files[file_id]; VersionFile& localFile = m_version.files[index]; // First check for blocked mods if (resultsFile.version.downloadUrl.isEmpty()) { BlockedMod blocked_mod; blocked_mod.name = resultsFile.version.fileName; blocked_mod.websiteUrl = QString("%1/download/%2").arg(resultsFile.pack.websiteUrl, QString::number(resultsFile.fileId)); blocked_mod.hash = resultsFile.version.hash; blocked_mod.matched = false; blocked_mod.localPath = ""; blocked_mod.targetFolder = resultsFile.targetFolder; m_blockedMods.append(blocked_mod); anyBlocked = true; } else { localFile.url = resultsFile.version.downloadUrl; } } m_modIdResolverTask.reset(); if (anyBlocked) { qDebug() << "Blocked files found, displaying file list"; BlockedModsDialog message_dialog(m_parent, tr("Blocked files found"), tr("The following files are not available for download in third party launchers.
    " "You will need to manually download them and add them to the instance."), m_blockedMods); message_dialog.setModal(true); if (message_dialog.exec() == QDialog::Accepted) { qDebug() << "Post dialog blocked mods list: " << m_blockedMods; createInstance(); } else { abort(); } } else { createInstance(); } } void PackInstallTask::createInstance() { setAbortable(false); setStatus(tr("Creating the instance...")); QCoreApplication::processEvents(); auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_unique(instanceConfigPath); MinecraftInstance instance(m_globalSettings, std::move(instanceSettings), m_stagingPath); auto components = instance.getPackProfile(); components->buildingFromScratch(); for (auto target : m_version.targets) { if (target.type == "game" && target.name == "minecraft") { components->setComponentVersion("net.minecraft", target.version, true); break; } } for (auto target : m_version.targets) { if (target.type != "modloader") continue; if (target.name == "forge") { components->setComponentVersion("net.minecraftforge", target.version); } else if (target.name == "fabric") { components->setComponentVersion("net.fabricmc.fabric-loader", target.version); } else if (target.name == "neoforge") { components->setComponentVersion("net.neoforged", target.version); } else if (target.name == "quilt") { components->setComponentVersion("org.quiltmc.quilt-loader", target.version); } } // install any jar mods QDir jarModsDir(FS::PathCombine(m_stagingPath, "minecraft", "jarmods")); if (jarModsDir.exists()) { QStringList jarMods; for (const auto& info : jarModsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) { jarMods.push_back(info.absoluteFilePath()); } components->installJarMods(jarMods); } components->saveNow(); instance.setName(name()); instance.setIconKey(m_instIcon); instance.setManagedPack("ftb", QString::number(m_pack.id), m_pack.name, QString::number(m_version.id), m_version.name); instance.saveNow(); onCreateInstanceSucceeded(); } void PackInstallTask::onCreateInstanceSucceeded() { downloadPack(); } void PackInstallTask::downloadPack() { setStatus(tr("Downloading mods...")); setAbortable(false); auto jobPtr = makeShared(tr("Mod download"), APPLICATION->network()); for (const auto& file : m_version.files) { if (file.serverOnly || file.url.isEmpty()) continue; auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path, file.name); qDebug() << "Will try to download" << file.url << "to" << path; QFileInfo file_info(file.name); auto dl = Net::Download::makeFile(file.url, path); if (!file.sha1.isEmpty()) { dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, file.sha1)); } jobPtr->addNetAction(dl); } jobPtr->setMaxConcurrent(1); // FTB blocks multiple requests at a time connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModDownloadSucceeded); connect(jobPtr.get(), &NetJob::failed, this, &PackInstallTask::onModDownloadFailed); connect(jobPtr.get(), &NetJob::aborted, this, &PackInstallTask::abort); connect(jobPtr.get(), &NetJob::progress, this, &PackInstallTask::setProgress); m_net_job = jobPtr; setAbortable(true); jobPtr->start(); } void PackInstallTask::onModDownloadSucceeded() { m_net_job.reset(); if (!m_blockedMods.isEmpty()) { copyBlockedMods(); } emitSucceeded(); } void PackInstallTask::onManifestDownloadFailed(QString reason) { m_net_job.reset(); emitFailed(reason); } void PackInstallTask::onResolveModsFailed(QString reason) { m_net_job.reset(); emitFailed(reason); } void PackInstallTask::onCreateInstanceFailed(QString reason) { emitFailed(reason); } void PackInstallTask::onModDownloadFailed(QString reason) { m_net_job.reset(); emitFailed(reason); } /// @brief copy the matched blocked mods to the instance staging area void PackInstallTask::copyBlockedMods() { setStatus(tr("Copying Blocked Mods...")); setAbortable(false); int i = 0; int total = m_blockedMods.length(); setProgress(i, total); for (const auto& mod : m_blockedMods) { if (!mod.matched) { qDebug() << mod.name << "was not matched to a local file, skipping copy"; continue; } auto dest_path = FS::PathCombine(m_stagingPath, ".minecraft", mod.targetFolder, mod.name); setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); qDebug() << "Will try to copy" << mod.localPath << "to" << dest_path; if (!FS::copy(mod.localPath, dest_path)()) { qDebug() << "Copy of" << mod.localPath << "to" << dest_path << "Failed"; } i++; setProgress(i, total); } setAbortable(true); } } // namespace FTB PrismLauncher-11.0.3/launcher/modplatform/modrinth/0000755000175100017510000000000015224505336021734 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/modplatform/modrinth/ModrinthCheckUpdate.h0000644000175100017510000000160715224505336025776 0ustar runnerrunner#pragma once #include "modplatform/CheckUpdateTask.h" class ModrinthCheckUpdate : public CheckUpdateTask { Q_OBJECT public: ModrinthCheckUpdate(QList& resources, std::vector& mcVersions, QList loadersList, ResourceFolderModel* resourceModel); public slots: bool abort() override; protected slots: void executeTask() override; void getUpdateModsForLoader(std::optional loader = {}, bool forceModLoaderCheck = false); void checkVersionsResponse(QByteArray* response, std::optional loader); void checkNextLoader(); private: Task::Ptr m_job = nullptr; QHash m_mappings; QString m_hashType; int m_loaderIdx = 0; qsizetype m_initialSize = 0; }; PrismLauncher-11.0.3/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h0000644000175100017510000000321415224505336027666 0ustar runnerrunner#pragma once #include #include #include #include #include #include #include #include "BaseInstance.h" #include "InstanceCreationTask.h" class ModrinthCreationTask final : public InstanceCreationTask { Q_OBJECT struct File { QString path; QCryptographicHash::Algorithm hashAlgorithm; QByteArray hash; QQueue downloads; bool required = true; }; public: ModrinthCreationTask(const QString& stagingPath, SettingsObject* globalSettings, QWidget* parent, QString id, QString versionId = {}, QString originalInstanceId = {}) : m_parent(parent), m_managed_id(std::move(id)), m_managed_version_id(std::move(versionId)) { setStagingPath(stagingPath); setParentSettings(globalSettings); m_original_instance_id = std::move(originalInstanceId); } bool abort() override; bool updateInstance() override; std::unique_ptr createInstance() override; private: bool parseManifest(const QString&, std::vector&, bool setInternalData = true, bool showOptionalDialog = true); private: QWidget* m_parent = nullptr; QString m_minecraft_version, m_fabric_version, m_quilt_version, m_forge_version, m_neoForge_version; QString m_managed_id, m_managed_version_id, m_managed_name; std::vector m_files; Task::Ptr m_task; std::optional m_instance; QString m_root_path = "minecraft"; }; PrismLauncher-11.0.3/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp0000644000175100017510000004735315224505336030235 0ustar runnerrunner#include "ModrinthInstanceCreationTask.h" #include "Application.h" #include "FileSystem.h" #include "InstanceList.h" #include "Json.h" #include "QObjectPtr.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/Mod.h" #include "modplatform/EnsureMetadataTask.h" #include "modplatform/helpers/OverrideUtils.h" #include "net/ChecksumValidator.h" #include "net/ApiDownload.h" #include "net/ApiHeaderProxy.h" #include "net/NetJob.h" #include "modplatform/ModIndex.h" #include "settings/INISettingsObject.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/pages/modplatform/OptionalModDialog.h" #include #include #include #include bool ModrinthCreationTask::abort() { if (!canAbort()) { return false; } if (m_task) { m_task->abort(); } return InstanceCreationTask::abort(); } bool ModrinthCreationTask::updateInstance() { auto* instanceList = APPLICATION->instances(); // FIXME: How to handle situations when there's more than one install already for a given modpack? BaseInstance* inst = nullptr; if (auto originalId = originalInstanceID(); !originalId.isEmpty()) { inst = instanceList->getInstanceById(originalId); Q_ASSERT(inst); } else { inst = instanceList->getInstanceByManagedName(originalName()); if (!inst) { inst = instanceList->getInstanceById(originalName()); if (!inst) { return false; } } } QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); if (!parseManifest(indexPath, m_files, true, false)) { return false; } auto versionName = inst->getManagedPackVersionName(); m_root_path = QFileInfo(inst->gameRoot()).fileName(); auto versionStr = !versionName.isEmpty() ? tr(" (version %1)").arg(versionName) : ""; if (shouldConfirmUpdate()) { auto shouldUpdate = askIfShouldUpdate(m_parent, versionStr); if (shouldUpdate == ShouldUpdate::SkipUpdating) { return false; } if (shouldUpdate == ShouldUpdate::Cancel) { m_abort = true; return false; } } // Remove repeated files, we don't need to download them! QDir oldInstDir(inst->instanceRoot()); QString oldIndexFolder(FS::PathCombine(oldInstDir.absolutePath(), "mrpack")); QString oldIndexPath(FS::PathCombine(oldIndexFolder, "modrinth.index.json")); QFileInfo oldIndexFile(oldIndexPath); if (oldIndexFile.exists()) { std::vector oldFiles; parseManifest(oldIndexPath, oldFiles, false, false); // Let's remove all duplicated, identical resources! auto filesIterator = m_files.begin(); begin: while (filesIterator != m_files.end()) { const auto& file = *filesIterator; auto oldFilesIterator = oldFiles.begin(); while (oldFilesIterator != oldFiles.end()) { const auto& oldFile = *oldFilesIterator; if (oldFile.hash == file.hash) { qDebug() << "Removed file at" << file.path << "from list of downloads"; filesIterator = m_files.erase(filesIterator); oldFilesIterator = oldFiles.erase(oldFilesIterator); goto begin; // Sorry :c } oldFilesIterator++; } filesIterator++; } QDir oldMinecraftDir(inst->gameRoot()); // Some files were removed from the old version, and some will be downloaded in an updated version, // so we're fine removing them! if (!oldFiles.empty()) { for (const auto& file : oldFiles) { scheduleToDelete(m_parent, oldMinecraftDir, file.path, true); } } // We will remove all the previous overrides, to prevent duplicate files! // TODO: Currently 'overrides' will always override the stuff on update. How do we preserve unchanged overrides? // FIXME: We may want to do something about disabled mods. auto oldOverrides = Override::readOverrides("overrides", oldIndexFolder); for (const auto& entry : oldOverrides) { scheduleToDelete(m_parent, oldMinecraftDir, entry); } auto oldClientOverrides = Override::readOverrides("client-overrides", oldIndexFolder); for (const auto& entry : oldClientOverrides) { scheduleToDelete(m_parent, oldMinecraftDir, entry); } } else { // We don't have an old index file, so we may duplicate stuff! auto* dialog = CustomMessageBox::selectable(m_parent, tr("No index file."), tr("We couldn't find a suitable index file for the older version. This may cause some " "of the files to be duplicated. Do you want to continue?"), QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel); if (dialog->exec() == QDialog::DialogCode::Rejected) { m_abort = true; return false; } } setOverride(true, inst->id()); qDebug() << "Will override instance!"; m_instance = inst; // We let it go through the createInstance() stage, just with a couple modifications for updating return false; } // https://docs.modrinth.com/docs/modpacks/format_definition/ std::unique_ptr ModrinthCreationTask::createInstance() { QEventLoop loop; QString parentFolder(FS::PathCombine(m_stagingPath, "mrpack")); QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); if (m_files.empty() && !parseManifest(indexPath, m_files, true, true)) { return nullptr; } // Keep index file in case we need it some other time (like when changing versions) QString newIndexPlace(FS::PathCombine(parentFolder, "modrinth.index.json")); FS::ensureFilePathExists(newIndexPlace); FS::move(indexPath, newIndexPlace); auto mcPath = FS::PathCombine(m_stagingPath, m_root_path); auto overridePath = FS::PathCombine(m_stagingPath, "overrides"); if (QFile::exists(overridePath)) { // Create a list of overrides in "overrides.txt" inside mrpack/ Override::createOverrides("overrides", parentFolder, overridePath); // Apply the overrides if (!FS::move(overridePath, mcPath)) { setError(tr("Could not rename the overrides folder:\n") + "overrides"); return nullptr; } } // Do client overrides auto clientOverridePath = FS::PathCombine(m_stagingPath, "client-overrides"); if (QFile::exists(clientOverridePath)) { // Create a list of overrides in "client-overrides.txt" inside mrpack/ Override::createOverrides("client-overrides", parentFolder, clientOverridePath); // Apply the overrides if (!FS::overrideFolder(mcPath, clientOverridePath)) { setError(tr("Could not rename the client overrides folder:\n") + "client overrides"); return nullptr; } } QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_unique(configPath); auto instance = std::make_unique(m_globalSettings, std::move(instanceSettings), m_stagingPath); auto* components = instance->getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_minecraft_version, true); QString loader; if (!m_fabric_version.isEmpty()) { components->setComponentVersion("net.fabricmc.fabric-loader", m_fabric_version); loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::Fabric); } if (!m_quilt_version.isEmpty()) { components->setComponentVersion("org.quiltmc.quilt-loader", m_quilt_version); loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::Quilt); } if (!m_forge_version.isEmpty()) { components->setComponentVersion("net.minecraftforge", m_forge_version); loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::Forge); } if (!m_neoForge_version.isEmpty()) { components->setComponentVersion("net.neoforged", m_neoForge_version); loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::NeoForge); } if (m_instIcon != "default") { instance->setIconKey(m_instIcon); } else if (!m_managed_id.isEmpty()) { instance->setIconKey("modrinth"); } // Don't add managed info to packs without an ID (most likely imported from ZIP) if (!m_managed_id.isEmpty()) { instance->setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version()); } else { instance->setManagedPack("modrinth", "", name(), "", ""); } instance->setName(name()); instance->saveNow(); auto downloadMods = makeShared(tr("Mod Download Modrinth"), APPLICATION->network()); auto rootModpackPath = FS::PathCombine(m_stagingPath, m_root_path); auto rootModpackUrl = QUrl::fromLocalFile(rootModpackPath); // TODO make this work with other sorts of resource QHash resources; for (auto& file : m_files) { auto fileName = file.path; fileName = FS::RemoveInvalidPathChars(fileName); auto filePath = FS::PathCombine(rootModpackPath, fileName); if (!rootModpackUrl.isParentOf(QUrl::fromLocalFile(filePath))) { // This means we somehow got out of the root folder, so abort here to prevent exploits setError(tr("One of the files has a path that leads to an arbitrary location (%1). This is a security risk and isn't allowed.") .arg(fileName)); return nullptr; } if (fileName.startsWith("mods/")) { auto* mod = new Mod(filePath); ModDetails d; d.mod_id = filePath; mod->setDetails(d); resources[file.hash.toHex()] = mod; } if (file.downloads.empty()) { setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName)); return nullptr; } qDebug() << "Will try to download" << file.downloads.front() << "to" << filePath; Net::ModrinthDownloadMeta meta{ .reason = m_instance.has_value() ? "update" : "modpack", .gameVersion = m_minecraft_version, .loader = loader, }; QUrl downloadUrl = file.downloads.dequeue(); auto dl = Net::ApiDownload::makeFile(downloadUrl, filePath, Net::Download::Option::NoOptions, meta); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); downloadMods->addNetAction(dl); if (!file.downloads.empty()) { // FIXME: This really needs to be put into a ConcurrentTask of // MultipleOptionsTask's , once those exist :) auto param = dl.toWeakRef(); connect(dl.get(), &Task::failed, [&file, filePath, param, downloadMods, meta] { QUrl fallbackUrl = file.downloads.dequeue(); auto ndl = Net::ApiDownload::makeFile(fallbackUrl, filePath, Net::Download::Option::NoOptions, meta); ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); downloadMods->addNetAction(ndl); if (auto shared = param.lock()) { shared->succeeded(); } }); } } bool endedWell = false; connect(downloadMods.get(), &NetJob::succeeded, this, [&endedWell]() { endedWell = true; }); connect(downloadMods.get(), &NetJob::failed, [this, &endedWell](const QString& reason) { endedWell = false; setError(reason); }); connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit); connect(downloadMods.get(), &NetJob::progress, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); connect(downloadMods.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); setStatus(tr("Downloading mods...")); downloadMods->start(); m_task = downloadMods; loop.exec(); if (!endedWell) { for (auto* resource : resources) { delete resource; } return nullptr; } QEventLoop ensureMetaLoop; QDir folder = FS::PathCombine(instance->modsRoot(), ".index"); auto ensureMetadataTask = makeShared(resources, folder, ModPlatform::ResourceProvider::MODRINTH); connect(ensureMetadataTask.get(), &Task::succeeded, this, [&endedWell]() { endedWell = true; }); connect(ensureMetadataTask.get(), &Task::finished, &ensureMetaLoop, &QEventLoop::quit); connect(ensureMetadataTask.get(), &Task::progress, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); connect(ensureMetadataTask.get(), &Task::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); ensureMetadataTask->start(); m_task = ensureMetadataTask; ensureMetaLoop.exec(); for (auto* resource : resources) { delete resource; } resources.clear(); // Update information of the already installed instance, if any. if (m_instance && endedWell) { setAbortable(false); auto* inst = m_instance.value(); // Only change the name if it didn't use a custom name, so that the previous custom name // is preserved, but if we're using the original one, we update the version string. // NOTE: This needs to come before the copyManagedPack call! if (inst->name().contains(inst->getManagedPackVersionName()) && inst->name() != instance->name()) { if (askForChangingInstanceName(m_parent, inst->name(), instance->name()) == InstanceNameChange::ShouldChange) { inst->setName(instance->name()); } } inst->copyManagedPack(*instance); } if (endedWell) { return instance; } return nullptr; } bool ModrinthCreationTask::parseManifest(const QString& indexPath, std::vector& files, bool setInternalData, bool showOptionalDialog) { try { auto doc = Json::requireDocument(indexPath); auto obj = Json::requireObject(doc, "modrinth.index.json"); int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); if (formatVersion == 1) { auto game = Json::requireString(obj, "game", "modrinth.index.json"); if (game != "minecraft") { throw JSONValidationError("Unknown game: " + game); } if (setInternalData) { if (m_managed_version_id.isEmpty()) { m_managed_version_id = obj["versionId"].toString(); } m_managed_name = obj["name"].toString(); } auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); std::vector optionalFiles; for (const auto& modInfo : jsonFiles) { File file; file.path = Json::requireString(modInfo, "path").replace("\\", "/"); auto env = modInfo["env"].toObject(); // 'env' field is optional if (!env.isEmpty()) { QString support = env["client"].toString("unsupported"); if (support == "unsupported") { continue; } if (support == "optional") { file.required = false; } } QJsonObject hashes = Json::requireObject(modInfo, "hashes"); file.hash = QByteArray::fromHex(Json::requireString(hashes, "sha512").toLatin1()); file.hashAlgorithm = QCryptographicHash::Sha512; // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode // (as Modrinth seems to incorrectly handle spaces) auto downloadArr = modInfo["downloads"].toArray(); for (auto download : downloadArr) { qWarning() << download.toString(); bool isLast = download.toString() == downloadArr.last().toString(); auto downloadUrl = QUrl(download.toString()); if (!downloadUrl.isValid()) { qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(downloadUrl.toString(), file.path); if (isLast && file.downloads.isEmpty()) { throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); } } else { file.downloads.push_back(downloadUrl); } } (file.required ? files : optionalFiles).push_back(file); } if (!optionalFiles.empty()) { if (showOptionalDialog) { QStringList oFiles; for (const auto& file : optionalFiles) { oFiles.push_back(file.path); } OptionalModDialog optionalModDialog(m_parent, oFiles); if (optionalModDialog.exec() == QDialog::Rejected) { emitAborted(); return false; } auto selectedMods = optionalModDialog.getResult(); for (auto file : optionalFiles) { if (selectedMods.contains(file.path)) { file.required = true; } else { file.path += ".disabled"; } files.push_back(file); } } else { for (auto file : optionalFiles) { file.path += ".disabled"; files.push_back(file); } } } if (setInternalData) { auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { QString name = it.key(); if (name == "minecraft") { m_minecraft_version = Json::requireString(*it, "Minecraft version"); } else if (name == "fabric-loader") { m_fabric_version = Json::requireString(*it, "Fabric Loader version"); } else if (name == "quilt-loader") { m_quilt_version = Json::requireString(*it, "Quilt Loader version"); } else if (name == "forge") { m_forge_version = Json::requireString(*it, "Forge version"); } else if (name == "neoforge") { m_neoForge_version = Json::requireString(*it, "NeoForge version"); } else { throw JSONValidationError("Unknown dependency type: " + name); } } } } else { throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion)); } } catch (const JSONValidationError& e) { setError(tr("Could not understand pack index:\n") + e.cause()); return false; } return true; } PrismLauncher-11.0.3/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp0000644000175100017510000002217115224505336026330 0ustar runnerrunner#include "ModrinthCheckUpdate.h" #include "Application.h" #include "ModrinthAPI.h" #include "ModrinthPackIndex.h" #include "Json.h" #include "QObjectPtr.h" #include "ResourceDownloadTask.h" #include "modplatform/ModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/ConcurrentTask.h" static const ModrinthAPI g_api; ModrinthCheckUpdate::ModrinthCheckUpdate(QList& resources, std::vector& mcVersions, QList loadersList, ResourceFolderModel* resourceModel) : CheckUpdateTask(resources, mcVersions, std::move(loadersList), resourceModel) , m_hashType(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()) { if (!m_loadersList.isEmpty()) { // this is for mods so append all the other posible loaders to the initial list m_initialSize = m_loadersList.length(); ModPlatform::ModLoaderTypes modLoaders; for (auto* m : resources) { modLoaders |= m->metadata()->loaders; } for (auto l : m_loadersList) { modLoaders &= ~static_cast(l); } m_loadersList.append(ModPlatform::modLoaderTypesToList(modLoaders)); } } bool ModrinthCheckUpdate::abort() { if (m_job) { return m_job->abort(); } return true; } /* Check for update: * - Get latest version available * - Compare hash of the latest version with the current hash * - If equal, no updates, else, there's updates, so add to the list * */ void ModrinthCheckUpdate::executeTask() { setStatus(tr("Preparing resources for Modrinth...")); setProgress(0, ((m_loadersList.isEmpty() ? 1 : m_loadersList.length()) * 2) + 1); auto hashingTask = makeShared("MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); bool startHasing = false; for (auto* resource : m_resources) { auto hash = resource->metadata()->hash; // Sadly the API can only handle one hash type per call, se we // need to generate a new hash if the current one is innadequate // (though it will rarely happen, if at all) if (resource->metadata()->hash_format != m_hashType) { auto hashTask = Hashing::createHasher(resource->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH); connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, resource](const QString& hash) { m_mappings.insert(hash, resource); }); connect(hashTask.get(), &Task::failed, [this] { failed("Failed to generate hash"); }); hashingTask->addTask(hashTask); startHasing = true; } else { m_mappings.insert(hash, resource); } } if (startHasing) { connect(hashingTask.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader); m_job = hashingTask; hashingTask->start(); } else { checkNextLoader(); } } void ModrinthCheckUpdate::getUpdateModsForLoader(std::optional loader, bool forceModLoaderCheck) { m_loaderIdx++; setStatus(tr("Waiting for the API response from Modrinth...")); setProgress(m_progress + 1, m_progressTotal); QStringList hashes; if (forceModLoaderCheck && loader.has_value()) { for (const auto& hash : m_mappings.keys()) { if ((m_mappings.value(hash)->metadata()->loaders & loader.value()) != 0) { hashes.append(hash); } } } else { hashes = m_mappings.keys(); } if (hashes.isEmpty()) { checkNextLoader(); return; } auto [job, response] = g_api.latestVersions(hashes, m_hashType, m_gameVersions, loader); connect(job.get(), &Task::succeeded, this, [this, response, loader] { checkVersionsResponse(response, loader); }); connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::checkNextLoader); m_job = job; job->start(); } void ModrinthCheckUpdate::checkVersionsResponse(QByteArray* response, std::optional loader) { setStatus(tr("Parsing the API response from Modrinth...")); setProgress(m_progress + 1, m_progressTotal); QJsonParseError parseError{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError); if (parseError.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at" << parseError.offset << "reason:" << parseError.errorString(); qWarning() << *response; emitFailed(parseError.errorString()); return; } try { auto iter = m_mappings.begin(); while (iter != m_mappings.end()) { const QString hash = iter.key(); Resource* resource = iter.value(); auto projectObj = doc[hash].toObject(); // If the returned project is empty, but we have Modrinth metadata, // it means this specific version is not available if (projectObj.isEmpty()) { qDebug() << "Mod" << m_mappings.find(hash).value()->name() << "got an empty response. Hash:" << hash; ++iter; continue; } // Sometimes a version may have multiple files, one with "forge" and one with "fabric", // so we may want to filter it QString loaderFilter; if (loader.has_value() && loader != 0) { auto modLoaders = ModPlatform::modLoaderTypesToList(*loader); if (!modLoaders.isEmpty()) { loaderFilter = ModPlatform::getModLoaderAsString(modLoaders.first()); } } // Currently, we rely on a couple heuristics to determine whether an update is actually available or not: // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the // loader_filter // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case) // Such is the pain of having arbitrary files for a given version .-. auto projectVer = Modrinth::loadIndexedPackVersion(projectObj, m_hashType, loaderFilter); if (projectVer.downloadUrl.isEmpty()) { qCritical() << "Modrinth mod without download url!" << projectVer.fileName; ++iter; continue; } // Fake pack with the necessary info to pass to the download task :) auto pack = std::make_shared(); pack->name = resource->name(); pack->slug = resource->metadata()->slug; pack->addonId = resource->metadata()->project_id; pack->provider = ModPlatform::ResourceProvider::MODRINTH; if ((projectVer.hash != hash && projectVer.is_preferred) || (resource->status() == ResourceStatus::NOT_INSTALLED)) { auto downloadTask = makeShared(pack, projectVer, m_resourceModel, true, "update"); QString oldVersion = resource->metadata()->version_number; if (oldVersion.isEmpty()) { if (resource->status() == ResourceStatus::NOT_INSTALLED) { oldVersion = tr("Not installed"); } else { oldVersion = tr("Unknown"); } } m_updates.emplace_back(pack->name, hash, oldVersion, projectVer.version_number, projectVer.version_type, projectVer.changelog, ModPlatform::ResourceProvider::MODRINTH, downloadTask, resource->enabled()); } m_deps.append(std::make_shared(pack, projectVer)); iter = m_mappings.erase(iter); } } catch (Json::JsonException& e) { emitFailed(e.cause() + ": " + e.what()); return; } checkNextLoader(); } void ModrinthCheckUpdate::checkNextLoader() { if (m_mappings.isEmpty()) { emitSucceeded(); return; } if (m_loaderIdx < m_loadersList.size()) { // this are mods so check with loades getUpdateModsForLoader(m_loadersList.at(m_loaderIdx), m_loaderIdx > m_initialSize); return; } if (m_loadersList.isEmpty() && m_loaderIdx == 0) { // this are other resources no need to check more than once with empty loader getUpdateModsForLoader(); return; } for (auto* resource : m_mappings) { QString reason; if (dynamic_cast(resource) != nullptr) { reason = tr("No valid version found for this resource. It's probably unavailable for the current game " "version / mod loader."); } else { reason = tr("No valid version found for this resource. It's probably unavailable for the current game version."); } emit checkFailed(resource, reason); } emitSucceeded(); } PrismLauncher-11.0.3/launcher/modplatform/modrinth/ModrinthPackExportTask.h0000644000175100017510000000441015224505336026514 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "BaseInstance.h" #include "MMCZip.h" #include "minecraft/MinecraftInstance.h" #include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "tasks/Task.h" class ModrinthPackExportTask : public Task { Q_OBJECT public: ModrinthPackExportTask(const QString& name, const QString& version, const QString& summary, bool optionalFiles, BaseInstance* instance, const QString& output, MMCZip::FilterFileFunction filter); protected: void executeTask() override; bool abort() override; private: struct ResolvedFile { QString sha1, sha512, url; qint64 size; ModPlatform::Side side; }; static const QStringList PREFIXES; static const QStringList FILE_EXTENSIONS; // inputs const QString name, version, summary; const bool optionalFiles; const BaseInstance* instance; MinecraftInstance* mcInstance; const QDir gameRoot; const QString output; const MMCZip::FilterFileFunction filter; ModrinthAPI api; QFileInfoList files; QMap pendingHashes; QMap resolvedFiles; Task::Ptr task; void collectFiles(); void collectHashes(); void makeApiRequest(); void parseApiResponse(QByteArray* response); void buildZip(); QByteArray generateIndex(); }; PrismLauncher-11.0.3/launcher/modplatform/modrinth/ModrinthPackIndex.h0000644000175100017510000000214415224505336025461 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "modplatform/ModIndex.h" #include "BaseInstance.h" namespace Modrinth { void loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj); void loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj); auto loadIndexedPackVersion(QJsonObject& obj, const QString& preferred_hash_type = "sha512", const QString& preferred_file_name = "") -> ModPlatform::IndexedVersion; } // namespace Modrinth PrismLauncher-11.0.3/launcher/modplatform/modrinth/ModrinthPackIndex.cpp0000644000175100017510000002116715224505336026022 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ModrinthPackIndex.h" #include "FileSystem.h" #include "ModrinthAPI.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/ModIndex.h" bool shouldDownloadOnSide(const QString& side) { return side == "required" || side == "optional"; } // https://docs.modrinth.com/api/operations/getproject/ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = obj["project_id"].toString(); if (pack.addonId.toString().isEmpty()) { pack.addonId = Json::requireString(obj, "id"); } pack.provider = ModPlatform::ResourceProvider::MODRINTH; pack.name = Json::requireString(obj, "title"); pack.slug = obj["slug"].toString(""); if (!pack.slug.isEmpty()) { pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug; } else { pack.websiteUrl = ""; } pack.description = obj["description"].toString(""); pack.logoUrl = obj["icon_url"].toString(""); pack.logoName = QString("%1.%2").arg(obj["slug"].toString(), QFileInfo(QUrl(pack.logoUrl).fileName()).suffix()); if (obj.contains("author")) { ModPlatform::ModpackAuthor modAuthor; modAuthor.name = obj["author"].toString(); modAuthor.url = ModrinthAPI::getAuthorURL(modAuthor.name); pack.authors = { modAuthor }; } auto client = shouldDownloadOnSide(obj["client_side"].toString()); auto server = shouldDownloadOnSide(obj["server_side"].toString()); if (server && client) { pack.side = ModPlatform::Side::UniversalSide; } else if (server) { pack.side = ModPlatform::Side::ServerSide; } else if (client) { pack.side = ModPlatform::Side::ClientSide; } // Modrinth can have more data than what's provided by the basic search :) pack.extraDataLoaded = false; } void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.extraData.issuesUrl = obj["issues_url"].toString(); if (pack.extraData.issuesUrl.endsWith('/')) pack.extraData.issuesUrl.chop(1); pack.extraData.sourceUrl = obj["source_url"].toString(); if (pack.extraData.sourceUrl.endsWith('/')) pack.extraData.sourceUrl.chop(1); pack.extraData.wikiUrl = obj["wiki_url"].toString(); if (pack.extraData.wikiUrl.endsWith('/')) pack.extraData.wikiUrl.chop(1); pack.extraData.discordUrl = obj["discord_url"].toString(); if (pack.extraData.discordUrl.endsWith('/')) { pack.extraData.discordUrl.chop(1); } auto donate_arr = obj["donation_urls"].toArray(); for (auto d : donate_arr) { auto d_obj = Json::requireObject(d); ModPlatform::DonationData donate; donate.id = d_obj["id"].toString(); donate.platform = d_obj["platform"].toString(); donate.url = d_obj["url"].toString(); pack.extraData.donate.append(donate); } pack.extraData.status = obj["status"].toString(); pack.extraData.body = obj["body"].toString().remove("
    "); pack.extraDataLoaded = true; } ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, const QString& preferred_hash_type, const QString& preferred_file_name) { ModPlatform::IndexedVersion file; file.addonId = Json::requireString(obj, "project_id"); file.fileId = Json::requireString(obj, "id"); file.date = Json::requireString(obj, "date_published"); auto versionArray = Json::requireArray(obj, "game_versions"); if (versionArray.empty()) { return {}; } for (auto mcVer : versionArray) { file.mcVersion.append({ ModrinthAPI::mapMCVersionFromModrinth(mcVer.toString()), mcVer.toString() }); // double this so we can check both strings when filtering } auto loaders = Json::requireArray(obj, "loaders"); for (auto loader : loaders) { if (loader == "neoforge") { file.loaders |= ModPlatform::NeoForge; } else if (loader == "forge") { file.loaders |= ModPlatform::Forge; } else if (loader == "cauldron") { file.loaders |= ModPlatform::Cauldron; } else if (loader == "liteloader") { file.loaders |= ModPlatform::LiteLoader; } else if (loader == "fabric") { file.loaders |= ModPlatform::Fabric; } else if (loader == "quilt") { file.loaders |= ModPlatform::Quilt; } } file.version = Json::requireString(obj, "name"); file.version_number = Json::requireString(obj, "version_number"); file.version_type = ModPlatform::IndexedVersionType::fromString(Json::requireString(obj, "version_type")); if (obj.contains("changelog")) { file.changelog = Json::requireString(obj, "changelog"); } auto dependencies = obj["dependencies"].toArray(); for (auto d : dependencies) { auto dep = d.toObject(); ModPlatform::Dependency dependency; dependency.addonId = dep["project_id"].toString(); dependency.version = dep["version_id"].toString(); auto depType = Json::requireString(dep, "dependency_type"); if (depType == "required") { dependency.type = ModPlatform::DependencyType::REQUIRED; } else if (depType == "optional") { dependency.type = ModPlatform::DependencyType::OPTIONAL; } else if (depType == "incompatible") { dependency.type = ModPlatform::DependencyType::INCOMPATIBLE; } else if (depType == "embedded") { dependency.type = ModPlatform::DependencyType::EMBEDDED; } else { dependency.type = ModPlatform::DependencyType::UNKNOWN; } file.dependencies.append(dependency); } auto files = Json::requireArray(obj, "files"); int i = 0; if (files.empty()) { // This should not happen normally, but check just in case qWarning() << "Modrinth returned an unexpected empty list of files:" << obj; return {}; } // Find correct file (needed in cases where one version may have multiple files) // Will default to the last one if there's no primary (though I think Modrinth requires that // at least one file is primary, idk) // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed while (i < files.count() - 1) { auto parent = files[i].toObject(); auto fileName = Json::requireString(parent, "filename"); if (!preferred_file_name.isEmpty() && fileName.contains(preferred_file_name)) { file.is_preferred = true; break; } // Grab the primary file, if available if (Json::requireBoolean(parent, "primary")) { break; } i++; } auto parent = files[i].toObject(); if (parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); file.fileName = Json::requireString(parent, "filename"); file.fileName = FS::RemoveInvalidPathChars(file.fileName); file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1); auto hash_list = Json::requireObject(parent, "hashes"); if (hash_list.contains(preferred_hash_type)) { file.hash = Json::requireString(hash_list, preferred_hash_type); file.hash_type = preferred_hash_type; } else { auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH); for (auto& hash_type : hash_types) { if (hash_list.contains(hash_type)) { file.hash = Json::requireString(hash_list, hash_type); file.hash_type = hash_type; break; } } } return file; } return {}; } PrismLauncher-11.0.3/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp0000644000175100017510000002666515224505336027067 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ModrinthPackExportTask.h" #include #include #include #include #include #include "Json.h" #include "MMCZip.h" #include "archive/ExportToZipTask.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/ModFolderModel.h" #include "modplatform/ModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/Task.h" const QStringList ModrinthPackExportTask::PREFIXES({ "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" }); const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "zip" }); ModrinthPackExportTask::ModrinthPackExportTask(const QString& name, const QString& version, const QString& summary, bool optionalFiles, BaseInstance* instance, const QString& output, MMCZip::FilterFileFunction filter) : name(name) , version(version) , summary(summary) , optionalFiles(optionalFiles) , instance(instance) , mcInstance(dynamic_cast(instance)) , gameRoot(instance->gameRoot()) , output(output) , filter(filter) {} void ModrinthPackExportTask::executeTask() { setStatus(tr("Searching for files...")); setProgress(0, 0); collectFiles(); } bool ModrinthPackExportTask::abort() { if (task) { task->abort(); return true; } return false; } void ModrinthPackExportTask::collectFiles() { setAbortable(false); QCoreApplication::processEvents(); files.clear(); if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) { emitFailed(tr("Could not search for files")); return; } pendingHashes.clear(); resolvedFiles.clear(); if (mcInstance) { mcInstance->loaderModList()->update(); connect(mcInstance->loaderModList(), &ModFolderModel::updateFinished, this, &ModrinthPackExportTask::collectHashes); } else collectHashes(); } void ModrinthPackExportTask::collectHashes() { setStatus(tr("Finding file hashes...")); for (const QFileInfo& file : files) { QCoreApplication::processEvents(); const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath()); // require sensible file types if (!std::any_of(PREFIXES.begin(), PREFIXES.end(), [&relative](const QString& prefix) { return relative.startsWith(prefix); })) continue; if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) { return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); })) continue; QFile openFile(file.absoluteFilePath()); if (!openFile.open(QFile::ReadOnly)) { qWarning() << "Could not open" << file << "for hashing:" << openFile.errorString(); continue; } const QByteArray data = openFile.readAll(); if (openFile.error() != QFileDevice::NoError) { qWarning() << "Could not read" << file << "error:" << openFile.errorString(); continue; } auto sha512 = Hashing::hash(data, Hashing::Algorithm::Sha512); auto allMods = mcInstance->loaderModList()->allMods(); if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; }); modIter != allMods.end()) { const Mod* mod = *modIter; if (mod->metadata() != nullptr) { const QUrl& url = mod->metadata()->url; // ensure the url is permitted on modrinth.com if (!url.isEmpty() && BuildConfig.MODRINTH_MRPACK_HOSTS.contains(url.host())) { qDebug() << "Resolving" << relative << "from index"; auto sha1 = Hashing::hash(data, Hashing::Algorithm::Sha1); ResolvedFile resolvedFile{ sha1, sha512, url.toEncoded(), openFile.size(), mod->metadata()->side }; resolvedFiles[relative] = resolvedFile; // nice! we've managed to resolve based on local metadata! // no need to enqueue it continue; } } } qDebug() << "Enqueueing" << relative << "for Modrinth query"; pendingHashes[relative] = sha512; } setAbortable(true); makeApiRequest(); } void ModrinthPackExportTask::makeApiRequest() { if (pendingHashes.isEmpty()) buildZip(); else { setStatus(tr("Finding versions for hashes...")); auto [versionsTask, response] = api.currentVersions(pendingHashes.values(), "sha512"); task = versionsTask; connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); }); connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed); connect(task.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted); task->start(); } } void ModrinthPackExportTask::parseApiResponse(QByteArray* response) { task = nullptr; try { const QJsonDocument doc = Json::requireDocument(*response); QMapIterator iterator(pendingHashes); while (iterator.hasNext()) { iterator.next(); const QJsonObject obj = doc[iterator.value()].toObject(); if (obj.isEmpty()) continue; const QJsonArray files_array = obj["files"].toArray(); if (auto fileIter = std::find_if(files_array.begin(), files_array.end(), [&iterator](const QJsonValue& file) { return file["hashes"]["sha512"] == iterator.value(); }); fileIter != files_array.end()) { // map the file to the url resolvedFiles[iterator.key()] = ResolvedFile{ fileIter->toObject()["hashes"].toObject()["sha1"].toString(), iterator.value(), fileIter->toObject()["url"].toString(), fileIter->toObject()["size"].toInt() }; } } } catch (const Json::JsonException& e) { emitFailed(tr("Failed to parse versions response: %1").arg(e.what())); return; } pendingHashes.clear(); buildZip(); } void ModrinthPackExportTask::buildZip() { setStatus(tr("Adding files...")); auto zipTask = makeShared(output, gameRoot, files, "overrides/", true); zipTask->addExtraFile("modrinth.index.json", generateIndex()); zipTask->setExcludeFiles(resolvedFiles.keys()); auto progressStep = std::make_shared(); connect(zipTask.get(), &Task::finished, this, [this, progressStep] { progressStep->state = TaskStepState::Succeeded; stepProgress(*progressStep); }); connect(zipTask.get(), &Task::succeeded, this, &ModrinthPackExportTask::emitSucceeded); connect(zipTask.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted); connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) { progressStep->state = TaskStepState::Failed; stepProgress(*progressStep); emitFailed(reason); }); connect(zipTask.get(), &Task::stepProgress, this, &ModrinthPackExportTask::propagateStepProgress); connect(zipTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { progressStep->update(current, total); stepProgress(*progressStep); }); connect(zipTask.get(), &Task::status, this, [this, progressStep](QString status) { progressStep->status = status; stepProgress(*progressStep); }); task.reset(zipTask); zipTask->start(); } QByteArray ModrinthPackExportTask::generateIndex() { QJsonObject out; out["formatVersion"] = 1; out["game"] = "minecraft"; out["name"] = name; out["versionId"] = version; if (!summary.isEmpty()) out["summary"] = summary; if (mcInstance) { auto profile = mcInstance->getPackProfile(); // collect all supported components const ComponentPtr minecraft = profile->getComponent("net.minecraft"); const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader"); const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader"); const ComponentPtr forge = profile->getComponent("net.minecraftforge"); const ComponentPtr neoForge = profile->getComponent("net.neoforged"); // convert all available components to mrpack dependencies QJsonObject dependencies; if (minecraft != nullptr) dependencies["minecraft"] = minecraft->m_version; if (quilt != nullptr) dependencies["quilt-loader"] = quilt->m_version; if (fabric != nullptr) dependencies["fabric-loader"] = fabric->m_version; if (forge != nullptr) dependencies["forge"] = forge->m_version; if (neoForge != nullptr) dependencies["neoforge"] = neoForge->m_version; out["dependencies"] = dependencies; } QJsonArray filesOut; for (auto iterator = resolvedFiles.constBegin(); iterator != resolvedFiles.constEnd(); iterator++) { QJsonObject fileOut; QString path = iterator.key(); const ResolvedFile& value = iterator.value(); QJsonObject env; // detect disabled mod const QFileInfo pathInfo(path); if (optionalFiles && pathInfo.suffix() == "disabled") { // rename it path = pathInfo.dir().filePath(pathInfo.completeBaseName()); env["client"] = "optional"; env["server"] = "optional"; } else { env["client"] = "required"; env["server"] = "required"; } // a server side mod does not imply that the mod does not work on the client // however, if a mrpack mod is marked as server-only it will not install on the client if (iterator->side == ModPlatform::Side::ClientSide) env["server"] = "unsupported"; fileOut["env"] = env; fileOut["path"] = path; fileOut["downloads"] = QJsonArray{ iterator->url }; QJsonObject hashes; hashes["sha1"] = value.sha1; hashes["sha512"] = value.sha512; fileOut["hashes"] = hashes; fileOut["fileSize"] = value.size; filesOut << fileOut; } out["files"] = filesOut; return QJsonDocument(out).toJson(QJsonDocument::Compact); } PrismLauncher-11.0.3/launcher/modplatform/modrinth/ModrinthAPI.cpp0000644000175100017510000001513715224505336024565 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "ModrinthAPI.h" #include "Application.h" #include "Json.h" #include "net/ApiDownload.h" #include "net/ApiUpload.h" #include "net/NetJob.h" std::pair ModrinthAPI::currentVersion(const QString& hash, const QString& hash_format) const { auto netJob = makeShared(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); auto [action, response] = Net::ApiDownload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format)); netJob->addNetAction(action); return { netJob, response }; } std::pair ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format) const { auto netJob = makeShared(QString("Modrinth::GetCurrentVersions"), APPLICATION->network()); QJsonObject body_obj; Json::writeStringList(body_obj, "hashes", hashes); Json::writeString(body_obj, "algorithm", hash_format); QJsonDocument body(body_obj); auto body_raw = body.toJson(); auto [action, response] = Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), body_raw); netJob->addNetAction(action); netJob->setAskRetry(false); return { netJob, response }; } std::pair ModrinthAPI::latestVersion(const QString& hash, const QString& hash_format, std::optional> mcVersions, std::optional loaders) const { auto netJob = makeShared(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); QJsonObject body_obj; if (loaders.has_value()) { Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value())); } if (mcVersions.has_value()) { QStringList game_versions; for (auto& ver : mcVersions.value()) { game_versions.append(mapMCVersionToModrinth(ver)); } Json::writeStringList(body_obj, "game_versions", game_versions); } QJsonDocument body(body_obj); auto body_raw = body.toJson(); auto [action, response] = Net::ApiUpload::makeByteArray( QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), body_raw); netJob->addNetAction(action); return { netJob, response }; } std::pair ModrinthAPI::latestVersions(const QStringList& hashes, const QString& hash_format, std::optional> mcVersions, std::optional loaders) const { auto netJob = makeShared(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); QJsonObject body_obj; Json::writeStringList(body_obj, "hashes", hashes); Json::writeString(body_obj, "algorithm", hash_format); if (loaders.has_value()) { Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value())); } if (mcVersions.has_value()) { QStringList game_versions; for (auto& ver : mcVersions.value()) { game_versions.append(mapMCVersionToModrinth(ver)); } Json::writeStringList(body_obj, "game_versions", game_versions); } QJsonDocument body(body_obj); auto body_raw = body.toJson(); auto [action, response] = Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), body_raw); netJob->addNetAction(action); return { netJob, response }; } std::pair ModrinthAPI::getProjects(QStringList addonIds) const { auto netJob = makeShared(QString("Modrinth::GetProjects"), APPLICATION->network()); auto searchUrl = getMultipleModInfoURL(addonIds); auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(searchUrl)); netJob->addNetAction(action); return { netJob, response }; } QList ModrinthAPI::getSortingMethods() const { // https://docs.modrinth.com/api-spec/#tag/projects/operation/searchProjects return { { .index = 1, .name = "relevance", .readable_name = QObject::tr("Sort by Relevance") }, { .index = 2, .name = "downloads", .readable_name = QObject::tr("Sort by Downloads") }, { .index = 3, .name = "follows", .readable_name = QObject::tr("Sort by Follows") }, { .index = 4, .name = "newest", .readable_name = QObject::tr("Sort by Newest") }, { .index = 5, .name = "updated", .readable_name = QObject::tr("Sort by Last Updated") } }; } std::pair ModrinthAPI::getModCategories() { auto netJob = makeShared(QString("Modrinth::GetCategories"), APPLICATION->network()); auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(BuildConfig.MODRINTH_PROD_URL + "/tag/category")); netJob->addNetAction(action); QObject::connect(netJob.get(), &Task::failed, [](const QString& msg) { qDebug() << "Modrinth failed to get categories:" << msg; }); return { netJob, response }; } QList ModrinthAPI::loadCategories(const QByteArray& response, const QString& projectType) { QList categories; QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from categories at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return categories; } try { auto arr = Json::requireArray(doc); for (auto val : arr) { auto cat = Json::requireObject(val); auto name = Json::requireString(cat, "name"); if (cat["project_type"].toString() == projectType) { categories.push_back({ .name = name, .id = name }); } } } catch (Json::JsonException& e) { qCritical() << "Failed to parse response from a version request."; qCritical() << e.what(); qDebug() << doc; } return categories; } QList ModrinthAPI::loadModCategories(const QByteArray& response) { return loadCategories(response, "mod"); }; PrismLauncher-11.0.3/launcher/modplatform/modrinth/ModrinthAPI.h0000644000175100017510000002365015224505336024231 0ustar runnerrunner// SPDX-FileCopyrightText: 2022-2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include "BuildConfig.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" #include #include class ModrinthAPI : public ResourceAPI { public: std::pair currentVersion(const QString& hash, const QString& hash_format) const; std::pair currentVersions(const QStringList& hashes, QString hash_format) const; std::pair latestVersion(const QString& hash, const QString& hash_format, std::optional> mcVersions, std::optional loaders) const; std::pair latestVersions(const QStringList& hashes, const QString& hash_format, std::optional> mcVersions, std::optional loaders) const; std::pair getProjects(QStringList addonIds) const override; static std::pair getModCategories(); static QList loadCategories(const QByteArray& response, const QString& projectType); static QList loadModCategories(const QByteArray& response); public: auto getSortingMethods() const -> QList override; static auto getAuthorURL(const QString& name) -> QString { return "https://modrinth.com/user/" + name; }; static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> QStringList { QStringList l; for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader, ModPlatform::DataPack, ModPlatform::Babric, ModPlatform::BTA, ModPlatform::LegacyFabric, ModPlatform::Ornithe, ModPlatform::Rift }) { if ((types & loader) != 0U) { l << getModLoaderAsString(loader); } } return l; } static auto getModLoaderFilters(ModPlatform::ModLoaderTypes types) -> QString { QStringList l; for (const auto& loader : getModLoaderStrings(types)) { l << QString("\"categories:%1\"").arg(loader); } return l.join(','); } static auto getCategoriesFilters(const QStringList& categories) -> QString { QStringList l; for (const auto& cat : categories) { l << QString("\"categories:%1\"").arg(cat); } return l.join(','); } static QString getSideFilters(ModPlatform::Side side) { switch (side) { case ModPlatform::Side::ClientSide: return { R"("client_side:required","client_side:optional"],["server_side:optional","server_side:unsupported")" }; case ModPlatform::Side::ServerSide: return { R"("server_side:required","server_side:optional"],["client_side:optional","client_side:unsupported")" }; case ModPlatform::Side::UniversalSide: return { R"("client_side:required"],["server_side:required")" }; case ModPlatform::Side::NoSide: // fallthrough default: return {}; } } static QString mapMCVersionFromModrinth(QString v) { static const QString s_preString = " Pre-Release "; bool pre = false; if (v.contains("-pre")) { pre = true; v.replace("-pre", s_preString); } v.replace("-", " "); if (pre) { v.replace(" Pre Release ", s_preString); } return v; } private: static QString resourceTypeParameter(ModPlatform::ResourceType type) { switch (type) { case ModPlatform::ResourceType::Mod: return "mod"; case ModPlatform::ResourceType::ResourcePack: return "resourcepack"; case ModPlatform::ResourceType::ShaderPack: return "shader"; case ModPlatform::ResourceType::DataPack: return "datapack"; case ModPlatform::ResourceType::Modpack: return "modpack"; default: qWarning() << "Invalid resource type for Modrinth API!"; break; } return ""; } QString createFacets(const SearchArgs& args) const { QStringList facets_list; if (args.loaders.has_value() && args.loaders.value() != 0) { facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value()))); } if (args.versions.has_value() && !args.versions.value().empty()) { facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value()))); } if (args.side.has_value()) { auto side = getSideFilters(args.side.value()); if (!side.isEmpty()) { facets_list.append(QString("[%1]").arg(side)); } } if (args.categoryIds.has_value() && !args.categoryIds->empty()) { facets_list.append(QString("[%1]").arg(getCategoriesFilters(args.categoryIds.value()))); } if (args.openSource) { facets_list.append("[\"open_source:true\"]"); } facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type))); return QString("[%1]").arg(facets_list.join(',')); } public: auto getSearchURL(const SearchArgs& args) const -> std::optional override { if (args.loaders.has_value() && args.loaders.value() != 0) { if (!validateModLoaders(args.loaders.value())) { qWarning() << "Modrinth - or our interface - does not support any the provided mod loaders!"; return {}; } } QStringList get_arguments; get_arguments.append(QString("offset=%1").arg(args.offset)); get_arguments.append(QString("limit=25")); if (args.search.has_value()) { get_arguments.append(QString("query=%1").arg(args.search.value())); } if (args.sorting.has_value()) { get_arguments.append(QString("index=%1").arg(args.sorting.value().name)); } get_arguments.append(QString("facets=%1").arg(createFacets(args))); return BuildConfig.MODRINTH_PROD_URL + "/search?" + get_arguments.join('&'); }; auto getInfoURL(const QString& id) const -> std::optional override { return BuildConfig.MODRINTH_PROD_URL + "/project/" + id; }; auto getMultipleModInfoURL(const QStringList& ids) const -> QString { return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\"")); }; auto getVersionsURL(const VersionSearchArgs& args) const -> std::optional override { QStringList get_arguments; if (args.mcVersions.has_value()) { get_arguments.append(QString("game_versions=[%1]").arg(getGameVersionsString(args.mcVersions.value()))); } if (args.loaders.has_value()) { get_arguments.append(QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\""))); } get_arguments.append(QString("include_changelog=%1").arg(args.includeChangelog ? "true" : "false")); return QString("%1/project/%2/version%3%4") .arg(BuildConfig.MODRINTH_PROD_URL, args.pack->addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&')); }; QString getGameVersionsArray(const std::vector& mcVersions) const { QString s; for (const auto& ver : mcVersions) { s += QString(R"("versions:%1",)").arg(mapMCVersionToModrinth(ver)); } s.remove(s.length() - 1, 1); // remove last comma return s.isEmpty() ? QString() : s; } static auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool { return (loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader | ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA | ModPlatform::LegacyFabric | ModPlatform::Ornithe | ModPlatform::Rift)) != 0; } std::optional getDependencyURL(const DependencySearchArgs& args) const override { return args.dependency.version.length() != 0 ? QString("%1/version/%2").arg(BuildConfig.MODRINTH_PROD_URL, args.dependency.version) : QString(R"(%1/project/%2/version?game_versions=["%3"]&loaders=["%4"]&include_changelog=%5)") .arg(BuildConfig.MODRINTH_PROD_URL) .arg(args.dependency.addonId.toString()) .arg(mapMCVersionToModrinth(args.mcVersion)) .arg(getModLoaderStrings(args.loader).join("\",\"")) .arg(args.includeChangelog ? "true" : "false"); }; QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.object().value("hits").toArray(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { Modrinth::loadIndexedPack(m, obj); } ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType /*unused*/) const override { return Modrinth::loadIndexedPackVersion(obj); }; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { Modrinth::loadExtraPackData(m, obj); } }; PrismLauncher-11.0.3/launcher/modplatform/ModIndex.h0000644000175100017510000001547115224505336022000 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include #include #include class QIODevice; namespace ModPlatform { enum class ModLoaderType : std::uint16_t { NeoForge = 1U << 0U, Forge = 1U << 1U, Cauldron = 1U << 2U, LiteLoader = 1U << 3U, Fabric = 1U << 4U, Quilt = 1U << 5U, DataPack = 1U << 6U, Babric = 1U << 7U, BTA = 1U << 8U, LegacyFabric = 1U << 9U, Ornithe = 1U << 10U, Rift = 1U << 11U }; ModLoaderType operator|(ModLoaderType lhs, ModLoaderType rhs); using enum ModLoaderType; Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) QList modLoaderTypesToList(ModLoaderTypes flags); enum class ResourceProvider : std::uint8_t { MODRINTH, FLAME }; enum class DependencyType : std::uint8_t { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; enum class Side : std::uint8_t { NoSide = 0, ClientSide = 1U << 0U, ServerSide = 1U << 1U, UniversalSide = ClientSide | ServerSide }; namespace SideUtils { QString toString(Side side); Side fromString(QString side); } // namespace SideUtils namespace DependencyTypeUtils { QString toString(DependencyType type); DependencyType fromString(const QString& str); } // namespace DependencyTypeUtils namespace ProviderCapabilities { const char* name(ResourceProvider); QString readableName(ResourceProvider); QStringList hashType(ResourceProvider); } // namespace ProviderCapabilities struct ModpackAuthor { QString name; QString url; }; struct DonationData { QString id; QString platform; QString url; }; struct IndexedVersionType { enum class Enum : std::uint8_t { Unknown = 0, Release = 1, Beta = 2, Alpha = 3 }; using enum Enum; constexpr IndexedVersionType(Enum e = Unknown) : m_type(e) {} // NOLINT(hicpp-explicit-conversions) static IndexedVersionType fromString(const QString& type); bool isValid() const { return m_type != Unknown; } std::strong_ordering operator<=>(const IndexedVersionType& other) const = default; std::strong_ordering operator<=>(const IndexedVersionType::Enum& other) const { return m_type <=> other; } QString toString() const; explicit operator int() const { return static_cast(m_type); } explicit operator IndexedVersionType::Enum() { return m_type; } private: Enum m_type; }; struct Dependency { QVariant addonId; DependencyType type; QString version; }; struct IndexedVersion { QVariant addonId; QVariant fileId; QString version; QString version_number; IndexedVersionType version_type; QStringList mcVersion; QString downloadUrl; QString date; QString fileName; ModLoaderTypes loaders; QString hash_type; QString hash; bool is_preferred = true; QString changelog; QList dependencies; Side side = Side::NoSide; // this is for flame API // For internal use, not provided by APIs bool is_currently_selected = false; QString getVersionDisplayString() const { auto release_type = version_type.isValid() ? QString(" [%1]").arg(version_type.toString()) : ""; auto versionStr = !version.contains(version_number) ? version_number : ""; QString gameVersion = ""; for (const auto& v : mcVersion) { if (version.contains(v)) { gameVersion = ""; break; } if (gameVersion.isEmpty()) { gameVersion = QObject::tr(" for %1").arg(v); } } return QString("%1%2 — %3%4").arg(version, gameVersion, versionStr, release_type); } }; struct ExtraPackData { QList donate; QString issuesUrl; QString sourceUrl; QString wikiUrl; QString discordUrl; QString status; QString body; }; struct IndexedPack { using Ptr = std::shared_ptr; QVariant addonId; ResourceProvider provider; QString name; QString slug; QString description; QList authors; QString logoName; QString logoUrl; QString websiteUrl; Side side = Side::NoSide; bool versionsLoaded = false; QList versions; // Don't load by default, since some modplatform don't have that info bool extraDataLoaded = true; ExtraPackData extraData; // For internal use, not provided by APIs bool isVersionSelected(int index) const { if (!versionsLoaded) { return false; } return versions.at(index).is_currently_selected; } bool isAnyVersionSelected() const { if (!versionsLoaded) { return false; } return std::any_of(versions.constBegin(), versions.constEnd(), [](const auto& v) { return v.is_currently_selected; }); } }; struct OverrideDep { QString quilt; QString fabric; QString slug; ModPlatform::ResourceProvider provider; }; inline auto getOverrideDeps() -> QList { return { { .quilt = "634179", .fabric = "306612", .slug = "API", .provider = ModPlatform::ResourceProvider::FLAME }, { .quilt = "720410", .fabric = "308769", .slug = "KotlinLibraries", .provider = ModPlatform::ResourceProvider::FLAME }, { .quilt = "qvIfYCYJ", .fabric = "P7dR8mSH", .slug = "API", .provider = ModPlatform::ResourceProvider::MODRINTH }, { .quilt = "lwVhp9o5", .fabric = "Ha28R6CL", .slug = "KotlinLibraries", .provider = ModPlatform::ResourceProvider::MODRINTH } }; } QString getMetaURL(ResourceProvider provider, QVariant projectID); auto getModLoaderAsString(ModLoaderType type) -> const QString; auto getModLoaderFromString(QString type) -> ModLoaderType; constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept { auto x = static_cast(l); return (x != 0U) && ((x & (x - 1U)) == 0U); } struct Category { QString name; QString id; }; } // namespace ModPlatform Q_DECLARE_METATYPE(ModPlatform::IndexedPack) Q_DECLARE_METATYPE(ModPlatform::IndexedPack::Ptr) Q_DECLARE_METATYPE(ModPlatform::ResourceProvider) PrismLauncher-11.0.3/launcher/modplatform/packwiz/0000755000175100017510000000000015224505336021560 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/modplatform/packwiz/Packwiz.h0000644000175100017510000000644315224505336023350 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "modplatform/ModIndex.h" #include #include #include class QDir; namespace Packwiz { class V1 { public: // can also represent other resources beside loader mods - but this is what packwiz calls it struct Mod { QString slug{}; QString name{}; QString filename{}; ModPlatform::Side side{ ModPlatform::Side::UniversalSide }; ModPlatform::ModLoaderTypes loaders; QStringList mcVersions; ModPlatform::IndexedVersionType releaseType; // [download] QString mode{}; QUrl url{}; QString hash_format{}; QString hash{}; // [update] ModPlatform::ResourceProvider provider{}; QVariant file_id{}; QVariant project_id{}; QString version_number{}; QList dependencies; public: // This is a totally heuristic, but should work for now. auto isValid() const -> bool { return !slug.isEmpty() && !project_id.isNull(); } // Different providers can use different names for the same thing // Modrinth-specific auto mod_id() -> QVariant& { return project_id; } auto version() -> QVariant& { return file_id; } }; /* Generates the object representing the information in a mod.pw.toml file via * its common representation in the launcher, when downloading mods. * */ static auto createModFormat(const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod; /* Updates the mod index for the provided mod. * This creates a new index if one does not exist already * TODO: Ask the user if they want to override, and delete the old mod's files, or keep the old one. * */ static void updateModIndex(const QDir& index_dir, Mod& mod); /* Deletes the metadata for the mod with the given slug. If the metadata doesn't exist, it does nothing. */ static void deleteModIndex(const QDir& index_dir, QString& mod_slug); /* Gets the metadata for a mod with a particular file name. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ static auto getIndexForMod(const QDir& index_dir, QString slug) -> Mod; /* Gets the metadata for a mod with a particular id. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ static auto getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod; }; } // namespace Packwiz PrismLauncher-11.0.3/launcher/modplatform/packwiz/Packwiz.cpp0000644000175100017510000003361715224505336023706 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "Packwiz.h" #include #include #include #include #include #include #include #include #include "FileSystem.h" #include "StringUtils.h" #include "Version.h" #include "modplatform/ModIndex.h" #include namespace Packwiz { namespace { auto getRealIndexName(const QDir& indexDir, const QString& normalizedFname, bool shouldFindMatch = false) -> QString { const QFile indexFile(indexDir.absoluteFilePath(normalizedFname)); QString realFname = normalizedFname; if (!indexFile.exists()) { // Tries to get similar entries for (auto& fileName : indexDir.entryList(QDir::Filter::Files)) { if (QString::compare(normalizedFname, fileName, Qt::CaseInsensitive) == 0) { realFname = fileName; break; } } if (shouldFindMatch && (QString::compare(normalizedFname, realFname, Qt::CaseSensitive) == 0)) { qCritical() << "Could not find a match for a valid metadata file!"; qCritical() << "File:" << normalizedFname; return {}; } } return realFname; } // Helpers auto indexFileName(const QString& modSlug) -> QString { if (modSlug.endsWith(".pw.toml")) { return modSlug; } return QString("%1.pw.toml").arg(modSlug); } // Helper functions for extracting data from the TOML file auto stringEntry(toml::table table, const QString& entryName) -> QString { auto* node = table.get(StringUtils::toStdString(entryName)); if (!node) { qDebug() << "Failed to read str property '" + entryName + "' in mod metadata."; return {}; } return node->value_or(""); } auto intEntry(toml::table table, const QString& entryName) -> int { auto* node = table.get(StringUtils::toStdString(entryName)); if (!node) { qDebug() << "Failed to read int property '" + entryName + "' in mod metadata."; return {}; } return node->value_or(0); } bool sortMCVersions(const QString& a, const QString& b) { auto cmp = Version(a) <=> Version(b); if (cmp == std::strong_ordering::equal) { return a < b; } return cmp == std::strong_ordering::less; } } // namespace auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod { Mod mod; mod.slug = mod_pack.slug; mod.name = mod_pack.name; mod.filename = mod_version.fileName; if (mod_pack.provider == ModPlatform::ResourceProvider::FLAME) { mod.mode = "metadata:curseforge"; } else { mod.mode = "url"; mod.url = mod_version.downloadUrl; } mod.hash_format = mod_version.hash_type; mod.hash = mod_version.hash; mod.provider = mod_pack.provider; mod.file_id = mod_version.fileId; mod.project_id = mod_pack.addonId; mod.side = mod_version.side == ModPlatform::Side::NoSide ? mod_pack.side : mod_version.side; mod.loaders = mod_version.loaders; mod.mcVersions = mod_version.mcVersion; mod.mcVersions.removeDuplicates(); std::ranges::sort(mod.mcVersions, sortMCVersions); mod.releaseType = mod_version.version_type; mod.version_number = mod_version.version_number; if (mod.version_number.isNull()) // on CurseForge, there is only a version name - not a version number mod.version_number = mod_version.version; mod.dependencies = mod_version.dependencies; return mod; } void V1::updateModIndex(const QDir& index_dir, Mod& mod) { if (!mod.isValid()) { qCritical() << QString("Tried to update metadata of an invalid mod!"); return; } // Ensure the corresponding mod's info exists, and create it if not auto normalized_fname = indexFileName(mod.slug); auto real_fname = getRealIndexName(index_dir, normalized_fname); QFile index_file(index_dir.absoluteFilePath(real_fname)); if (real_fname != normalized_fname) index_file.rename(normalized_fname); // There's already data on there! // TODO: We should do more stuff here, as the user is likely trying to // override a file. In this case, check versions and ask the user what // they want to do! if (index_file.exists()) { index_file.remove(); } else { FS::ensureFilePathExists(index_file.fileName()); } toml::table update; switch (mod.provider) { case (ModPlatform::ResourceProvider::FLAME): if (mod.file_id.toInt() == 0 || mod.project_id.toInt() == 0) { qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname); return; } update = toml::table{ { "file-id", mod.file_id.toInt() }, { "project-id", mod.project_id.toInt() }, }; break; case (ModPlatform::ResourceProvider::MODRINTH): if (mod.mod_id().toString().isEmpty() || mod.version().toString().isEmpty()) { qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname); return; } update = toml::table{ { "mod-id", mod.mod_id().toString().toStdString() }, { "version", mod.version().toString().toStdString() }, }; break; } toml::array loaders; for (auto loader : ModPlatform::modLoaderTypesToList(mod.loaders)) { loaders.push_back(getModLoaderAsString(loader).toStdString()); } toml::array mcVersions; for (auto version : mod.mcVersions) { mcVersions.push_back(version.toStdString()); } if (!index_file.open(QIODevice::ReadWrite)) { qCritical() << "Could not open file" << normalized_fname << "error:" << index_file.errorString(); return; } toml::array deps; for (auto dep : mod.dependencies) { auto tbl = toml::table{ { "addonId", dep.addonId.toString().toStdString() }, { "type", ModPlatform::DependencyTypeUtils::toString(dep.type).toStdString() } }; if (!dep.version.isEmpty()) { tbl.emplace("version", dep.version.toStdString()); } deps.push_back(tbl); } // Put TOML data into the file QTextStream in_stream(&index_file); { auto tbl = toml::table{ { "name", mod.name.toStdString() }, { "filename", mod.filename.toStdString() }, { "side", ModPlatform::SideUtils::toString(mod.side).toStdString() }, { "x-prismlauncher-loaders", loaders }, { "x-prismlauncher-mc-versions", mcVersions }, { "x-prismlauncher-release-type", mod.releaseType.toString().toStdString() }, { "x-prismlauncher-version-number", mod.version_number.toStdString() }, { "x-prismlauncher-dependencies", deps }, { "download", toml::table{ { "mode", mod.mode.toStdString() }, { "url", mod.url.toString().toStdString() }, { "hash-format", mod.hash_format.toStdString() }, { "hash", mod.hash.toStdString() }, } }, { "update", toml::table{ { ModPlatform::ProviderCapabilities::name(mod.provider), update } } } }; std::stringstream ss; ss << tbl; in_stream << QString::fromStdString(ss.str()); } index_file.flush(); index_file.close(); } void V1::deleteModIndex(const QDir& index_dir, QString& mod_slug) { auto normalized_fname = indexFileName(mod_slug); auto real_fname = getRealIndexName(index_dir, normalized_fname); if (real_fname.isEmpty()) return; QFile index_file(index_dir.absoluteFilePath(real_fname)); if (!index_file.exists()) { qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_slug); return; } if (!index_file.remove()) { qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_slug); } } auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod { Mod mod; auto normalized_fname = indexFileName(slug); auto real_fname = getRealIndexName(index_dir, normalized_fname, true); if (real_fname.isEmpty()) return {}; toml::table table; #if TOML_EXCEPTIONS try { table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname))); } catch (const toml::parse_error& err) { qWarning() << QString("Could not open file %1!").arg(normalized_fname); qWarning() << "Reason:" << QString(err.what()); return {}; } #else toml::parse_result result = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname))); if (!result) { qWarning() << QString("Could not open file %1!").arg(normalized_fname); qWarning() << "Reason:" << result.error().description(); return {}; } table = result.table(); #endif // index_file.close(); mod.slug = slug; { // Basic info mod.name = stringEntry(table, "name"); mod.filename = stringEntry(table, "filename"); mod.side = ModPlatform::SideUtils::fromString(stringEntry(table, "side")); mod.releaseType = ModPlatform::IndexedVersionType::fromString(table["x-prismlauncher-release-type"].value_or("")); if (auto loaders = table["x-prismlauncher-loaders"]; loaders && loaders.is_array()) { for (auto&& loader : *loaders.as_array()) { if (loader.is_string()) { mod.loaders |= ModPlatform::getModLoaderFromString(QString::fromStdString(loader.as_string()->value_or(""))); } } } if (auto versions = table["x-prismlauncher-mc-versions"]; versions && versions.is_array()) { for (auto&& version : *versions.as_array()) { if (version.is_string()) { auto ver = QString::fromStdString(version.as_string()->value_or("")); if (!ver.isEmpty()) { mod.mcVersions << ver; } } } mod.mcVersions.removeDuplicates(); std::ranges::sort(mod.mcVersions, sortMCVersions); } } mod.version_number = table["x-prismlauncher-version-number"].value_or(""); { // [download] info auto download_table = table["download"].as_table(); if (!download_table) { qCritical() << QString("No [download] section found on mod metadata!"); return {}; } mod.mode = stringEntry(*download_table, "mode"); mod.url = stringEntry(*download_table, "url"); mod.hash_format = stringEntry(*download_table, "hash-format"); mod.hash = stringEntry(*download_table, "hash"); } { // [update] info using Provider = ModPlatform::ResourceProvider; auto update_table = table["update"]; if (!update_table || !update_table.is_table()) { qCritical() << QString("No [update] section found on mod metadata!"); return {}; } toml::table* mod_provider_table = nullptr; if ((mod_provider_table = update_table[ModPlatform::ProviderCapabilities::name(Provider::FLAME)].as_table())) { mod.provider = Provider::FLAME; mod.file_id = intEntry(*mod_provider_table, "file-id"); mod.project_id = intEntry(*mod_provider_table, "project-id"); } else if ((mod_provider_table = update_table[ModPlatform::ProviderCapabilities::name(Provider::MODRINTH)].as_table())) { mod.provider = Provider::MODRINTH; mod.mod_id() = stringEntry(*mod_provider_table, "mod-id"); mod.version() = stringEntry(*mod_provider_table, "version"); } else { qCritical() << QString("No mod provider on mod metadata!"); return {}; } } { // dependencies auto deps = table["x-prismlauncher-dependencies"].as_array(); if (deps) { for (auto&& depNode : *deps) { auto dep = depNode.as_table(); if (dep) { ModPlatform::Dependency d; d.addonId = stringEntry(*dep, "addonId"); if (dep->contains("version")) { d.version = stringEntry(*dep, "version"); } d.type = ModPlatform::DependencyTypeUtils::fromString(stringEntry(*dep, "type")); mod.dependencies << d; } } } } return mod; } auto V1::getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod { for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { auto mod = getIndexForMod(index_dir, file_name); if (mod.mod_id() == mod_id) return mod; } return {}; } } // namespace Packwiz PrismLauncher-11.0.3/launcher/modplatform/EnsureMetadataTask.h0000644000175100017510000000366115224505336024014 0ustar runnerrunner#pragma once #include "ModIndex.h" #include "net/NetJob.h" #include "modplatform/helpers/HashUtils.h" #include "minecraft/mod/Resource.h" #include "tasks/ConcurrentTask.h" class Mod; class QDir; class EnsureMetadataTask : public Task { Q_OBJECT public: EnsureMetadataTask(Resource*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); EnsureMetadataTask(QList&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); EnsureMetadataTask(QHash&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); ~EnsureMetadataTask() = default; Task::Ptr getHashingTask() { return m_hashingTask; } public slots: bool abort() override; protected slots: void executeTask() override; private: // FIXME: Move to their own namespace Task::Ptr modrinthVersionsTask(); Task::Ptr modrinthProjectsTask(); Task::Ptr flameVersionsTask(); Task::Ptr flameProjectsTask(); // Helpers enum class RemoveFromList { Yes, No }; void emitReady(Resource*, QString key = {}, RemoveFromList = RemoveFromList::Yes); void emitFail(Resource*, QString key = {}, RemoveFromList = RemoveFromList::Yes); // Hashes and stuff Hashing::Hasher::Ptr createNewHash(Resource*); QString getExistingHash(Resource*); private slots: void updateMetadata(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource*); void updateMetadataCallback(ModPlatform::IndexedPack& pack, Resource* resource); signals: void metadataReady(Resource*); void metadataFailed(Resource*); private: QHash m_resources; QDir m_indexDir; ModPlatform::ResourceProvider m_provider; QHash m_tempVersions; Task::Ptr m_hashingTask; Task::Ptr m_currentTask; QHash m_updateMetadataTasks; }; PrismLauncher-11.0.3/launcher/modplatform/CheckUpdateTask.h0000644000175100017510000000446715224505336023277 0ustar runnerrunner#pragma once #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "tasks/Task.h" class ResourceDownloadTask; class ModFolderModel; class CheckUpdateTask : public Task { Q_OBJECT public: CheckUpdateTask(QList& resources, std::vector& mcVersions, QList loadersList, ResourceFolderModel* resourceModel) : m_resources(resources), m_gameVersions(mcVersions), m_loadersList(std::move(loadersList)), m_resourceModel(resourceModel) {} struct Update { QString name; QString oldHash; QString oldVersion; QString newVersion; std::optional newVersionType; QString changelog; ModPlatform::ResourceProvider provider; shared_qobject_ptr download; bool enabled = true; public: Update(QString name, QString oldH, QString oldV, QString newV, std::optional newVType, QString changelog, ModPlatform::ResourceProvider p, shared_qobject_ptr t, bool enabled = true) : name(std::move(name)) , oldHash(std::move(oldH)) , oldVersion(std::move(oldV)) , newVersion(std::move(newV)) , newVersionType(newVType) , changelog(std::move(changelog)) , provider(p) , download(std::move(t)) , enabled(enabled) {} }; auto getUpdates() -> std::vector&& { return std::move(m_updates); } auto getDependencies() -> QList>&& { return std::move(m_deps); } protected slots: void executeTask() override = 0; signals: void checkFailed(Resource* failed, QString reason, QUrl recoverUrl = {}); protected: QList& m_resources; std::vector& m_gameVersions; QList m_loadersList; ResourceFolderModel* m_resourceModel; std::vector m_updates; QList> m_deps; }; PrismLauncher-11.0.3/launcher/QObjectPtr.h0000644000175100017510000000315115224505336017752 0ustar runnerrunner#pragma once #include #include #include #include /** * A unique pointer class with unique pointer semantics intended for derivates of QObject * Calls deleteLater() instead of destroying the contained object immediately */ template using unique_qobject_ptr = QScopedPointer; /** * A shared pointer class with shared pointer semantics intended for derivates of QObject * Calls deleteLater() instead of destroying the contained object immediately */ template class shared_qobject_ptr : public QSharedPointer { public: constexpr explicit shared_qobject_ptr() : QSharedPointer() {} constexpr explicit shared_qobject_ptr(T* ptr) : QSharedPointer(ptr, &QObject::deleteLater) {} constexpr shared_qobject_ptr(std::nullptr_t null_ptr) : QSharedPointer(null_ptr, &QObject::deleteLater) {} template constexpr shared_qobject_ptr(const shared_qobject_ptr& other) : QSharedPointer(other) {} template constexpr shared_qobject_ptr(const QSharedPointer& other) : QSharedPointer(other) {} void reset() { QSharedPointer::reset(); } void reset(T* other) { shared_qobject_ptr t(other); this->swap(t); } void reset(const shared_qobject_ptr& other) { shared_qobject_ptr t(other); this->swap(t); } }; template shared_qobject_ptr makeShared(Args... args) { auto obj = new T(args...); return shared_qobject_ptr(obj); } PrismLauncher-11.0.3/launcher/net/0000755000175100017510000000000015224505336016352 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/net/Download.cpp0000644000175100017510000000617615224505336020637 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Download.h" #include #include #include #include #include "ByteArraySink.h" #include "ChecksumValidator.h" #include "MetaCacheSink.h" namespace Net { #if defined(LAUNCHER_APPLICATION) auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Download::Ptr { auto dl = makeShared(); dl->m_url = url; dl->setObjectName(QString("CACHE:") + url.toString()); dl->m_options = options; auto md5Node = new ChecksumValidator(QCryptographicHash::Md5); auto cachedNode = new MetaCacheSink(entry, md5Node, options.testFlag(Option::MakeEternal)); dl->m_sink.reset(cachedNode); return dl; } #endif auto Download::makeByteArray(QUrl url, Options options) -> std::pair { auto dl = makeShared(); dl->m_url = url; dl->setObjectName(QString("BYTES:") + url.toString()); dl->m_options = options; auto sink = std::make_unique(); QByteArray* response = sink->output(); dl->m_sink = std::move(sink); return { dl, response }; } auto Download::makeFile(QUrl url, QString path, Options options) -> Download::Ptr { auto dl = makeShared(); dl->m_url = url; dl->setObjectName(QString("FILE:") + url.toString()); dl->m_options = options; dl->m_sink.reset(new FileSink(path)); return dl; } QNetworkReply* Download::getReply(QNetworkRequest& request) { return m_network->get(request); } } // namespace Net PrismLauncher-11.0.3/launcher/net/NetRequest.cpp0000644000175100017510000003513715224505336021166 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "NetRequest.h" #include #include #include #include #include #include #include #if defined(LAUNCHER_APPLICATION) #include "Application.h" #include "settings/SettingsObject.h" #endif #include "BuildConfig.h" #include "MMCTime.h" #include "StringUtils.h" namespace Net { NetRequest::NetRequest() : Task() { connect(&m_retryTimer, &QTimer::timeout, this, &NetRequest::executeTask); } void NetRequest::addValidator(Validator* v) { m_sink->addValidator(v); } void NetRequest::executeTask() { setStatus(tr("Requesting %1").arg(StringUtils::truncateUrlHumanFriendly(m_url, 80))); if (getState() == Task::State::AbortedByUser) { qCWarning(logCat) << getUid().toString() << "Attempt to start an aborted Request:" << m_url.toString(); emit aborted(); emit finished(); return; } QNetworkRequest request(m_url); m_state = m_sink->init(request); switch (m_state) { case State::Succeeded: qCDebug(logCat) << getUid().toString() << "Request cache hit" << m_url.toString(); emit succeeded(); emit finished(); return; case State::Running: qCDebug(logCat) << getUid().toString() << "Running" << m_url.toString(); break; case State::Inactive: case State::Failed: m_failReason = m_sink->failReason(); emit failed(m_sink->failReason()); emit finished(); return; case State::AbortedByUser: emit aborted(); emit finished(); return; } #if defined(LAUNCHER_APPLICATION) auto user_agent = APPLICATION->getUserAgent(); #else auto user_agent = BuildConfig.USER_AGENT; #endif request.setHeader(QNetworkRequest::UserAgentHeader, user_agent.toUtf8()); for (auto& header_proxy : m_headerProxies) { header_proxy->writeHeaders(request); } #if defined(LAUNCHER_APPLICATION) request.setTransferTimeout(APPLICATION->settings()->get("RequestTimeout").toInt() * 1000); #else request.setTransferTimeout(); #endif m_last_progress_time = m_clock.now(); m_last_progress_bytes = 0; auto rep = getReply(request); if (rep == nullptr) // it failed return; m_reply.reset(rep); connect(rep, &QNetworkReply::uploadProgress, this, &NetRequest::onProgress); connect(rep, &QNetworkReply::downloadProgress, this, &NetRequest::onProgress); connect(rep, &QNetworkReply::finished, this, &NetRequest::downloadFinished); connect(rep, &QNetworkReply::errorOccurred, this, &NetRequest::downloadError); connect(rep, &QNetworkReply::sslErrors, this, &NetRequest::sslErrors); connect(rep, &QNetworkReply::readyRead, this, &NetRequest::downloadReadyRead); } void NetRequest::onProgress(qint64 bytesReceived, qint64 bytesTotal) { auto now = m_clock.now(); auto elapsed = now - m_last_progress_time; // use milliseconds for speed precision auto elapsed_ms = std::chrono::duration_cast(elapsed); auto bytes_received_since = bytesReceived - m_last_progress_bytes; auto dl_speed_bps = (double)bytes_received_since / elapsed_ms.count() * 1000; auto remaining_time_s = (bytesTotal - bytesReceived) / dl_speed_bps; //: Current amount of bytes downloaded, out of the total amount of bytes in the download QString dl_progress = tr("%1 / %2").arg(StringUtils::humanReadableFileSize(bytesReceived)).arg(StringUtils::humanReadableFileSize(bytesTotal)); QString dl_speed_str; if (elapsed_ms.count() > 0) { auto str_eta = bytesTotal > 0 ? Time::humanReadableDuration(remaining_time_s) : tr("unknown"); //: Download speed, in bytes per second (remaining download time in parenthesis) dl_speed_str = tr("%1 /s (%2)").arg(StringUtils::humanReadableFileSize(dl_speed_bps)).arg(str_eta); } else { //: Download speed at 0 bytes per second dl_speed_str = tr("0 B/s"); } setDetails(dl_progress + "\n" + dl_speed_str); setProgress(bytesReceived, bytesTotal); } void NetRequest::downloadError(QNetworkReply::NetworkError error) { if (error == QNetworkReply::OperationCanceledError) { qCCritical(logCat) << getUid().toString() << "Aborted" << m_url.toString(); m_state = State::Failed; } else if (replyStatusCode() == 429 /* HTTP Too Many Requests*/ && m_options & Option::AutoRetry) { qCDebug(logCat) << getUid().toString() << "Rate Limited!"; int64_t delay = 10 * std::pow(2, m_retryCount); if (m_reply->hasRawHeader("Retry-After")) { auto retryAfter = m_reply->rawHeader("Retry-After"); if (retryAfter.trimmed().endsWith("GMT")) /* HTTP Date format */ { auto afterTimestamp = QDateTime::fromString(QString::fromUtf8(retryAfter.trimmed()), "ddd, dd MMM yyyy HH:mm:ss 'GMT'"); auto now = QDateTime::currentDateTime(); delay = now.secsTo(afterTimestamp); } else { delay = retryAfter.toLong(); } } handleAutoRetry(delay); } else { if (m_options & Option::AcceptLocalFiles) { if (m_sink->hasLocalData()) { m_state = State::Succeeded; return; } } // error happened during download. qCCritical(logCat) << getUid().toString() << "Failed" << m_url.toString() << "with error" << error; if (m_reply) qCCritical(logCat) << getUid().toString() << "HTTP status:" << replyStatusCode() << errorString(); if (m_errorResponse.size() > 0) qCCritical(logCat) << getUid().toString() << "Response from server:" << m_errorResponse; m_state = State::Failed; } } void NetRequest::sslErrors(const QList& errors) { int i = 1; for (auto error : errors) { qCCritical(logCat).nospace() << getUid().toString() << " Request " << m_url.toString() << " SSL Error #" << i << ": " << error.errorString(); auto cert = error.certificate(); qCCritical(logCat) << getUid().toString() << "Certificate in question:\n" << cert.toText(); i++; } } auto NetRequest::handleRedirect() -> bool { QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); if (!redirect.isValid()) { if (!m_reply->hasRawHeader("Location")) { // no redirect -> it's fine to continue return false; } // there is a Location header, but it's not correct. we need to apply some workarounds... QByteArray redirectBA = m_reply->rawHeader("Location"); if (redirectBA.size() == 0) { // empty, yet present redirect header? WTF? return false; } QString redirectStr = QString::fromUtf8(redirectBA); if (redirectStr.startsWith("//")) { /* * IF the URL begins with //, we need to insert the URL scheme. * See: https://bugreports.qt.io/browse/QTBUG-41061 * See: http://tools.ietf.org/html/rfc3986#section-4.2 */ redirectStr = m_reply->url().scheme() + ":" + redirectStr; } else if (redirectStr.startsWith("/")) { /* * IF the URL begins with /, we need to process it as a relative URL */ auto url = m_reply->url(); url.setPath(redirectStr, QUrl::TolerantMode); redirectStr = url.toString(); } /* * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues. * FIXME: report Qt bug for this */ redirect = QUrl(redirectStr, QUrl::TolerantMode); if (!redirect.isValid()) { qCWarning(logCat) << getUid().toString() << "Failed to parse redirect URL:" << redirectStr; downloadError(QNetworkReply::ProtocolFailure); return false; } qCDebug(logCat) << getUid().toString() << "Fixed location header:" << redirect; } else { qCDebug(logCat) << getUid().toString() << "Location header:" << redirect; } m_url = QUrl(redirect.toString()); qCDebug(logCat) << getUid().toString() << "Following redirect to" << m_url.toString(); executeTask(); return true; } void NetRequest::handleAutoRetry(int64_t delay) { m_retryCount++; if (delay > 60 || m_retryCount > 4) { /* 1 minute is too long to wait for retry, fail for now */ m_state = State::Failed; auto retryAfter = QDateTime::currentDateTime().addSecs(delay); emitFailed(tr("Request Rate Limited for %n second(s): Retry After %1", "seconds", delay) .arg(retryAfter.toLocalTime().toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)))); return; } else { qCDebug(logCat) << getUid().toString() << "Retyring Request in" << delay << "seconds"; setStatus(tr("Rate Limited: Waiting %n second(s)", "seconds", delay)); m_retryTimer.setTimerType(Qt::VeryCoarseTimer); m_retryTimer.setSingleShot(true); m_retryTimer.setInterval(delay * 1000); m_retryTimer.start(); } } void NetRequest::downloadFinished() { // currently waiting for retry if (m_retryTimer.isActive()) { return; } // handle HTTP redirection first if (handleRedirect()) { qCDebug(logCat) << getUid().toString() << "Request redirected:" << m_url.toString(); return; } // if the download failed before this point ... if (m_state == State::Succeeded) // pretend to succeed so we continue processing :) { qCDebug(logCat) << getUid().toString() << "Request failed but we are allowed to proceed:" << m_url.toString(); m_sink->abort(); emit succeeded(); emit finished(); return; } else if (m_state == State::Failed) { qCDebug(logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString(); m_sink->abort(); m_failReason = m_reply->errorString(); emit failed(m_reply->errorString()); emit finished(); return; } else if (m_state == State::AbortedByUser) { qCDebug(logCat) << getUid().toString() << "Request aborted in previous step:" << m_url.toString(); m_sink->abort(); emit aborted(); emit finished(); return; } // make sure we got all the remaining data, if any auto data = m_reply->readAll(); if (data.size()) { qCDebug(logCat) << getUid().toString() << "Writing extra" << data.size() << "bytes"; m_state = m_sink->write(data); if (m_state != State::Succeeded) { qCDebug(logCat) << getUid().toString() << "Request failed to write:" << m_url.toString(); m_sink->abort(); m_failReason = m_sink->failReason(); emit failed(m_sink->failReason()); emit finished(); return; } } // otherwise, finalize the whole graph m_state = m_sink->finalize(*m_reply.get()); if (m_state != State::Succeeded) { qCDebug(logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString(); m_sink->abort(); m_failReason = m_sink->failReason(); emit failed(m_sink->failReason()); emit finished(); return; } qCDebug(logCat) << getUid().toString() << "Request succeeded:" << m_url.toString(); emit succeeded(); emit finished(); } void NetRequest::downloadReadyRead() { if (m_state == State::Running) { auto data = m_reply->readAll(); m_state = m_sink->write(data); if (replyStatusCode() >= 400) { m_errorResponse.append(data); } if (m_state == State::Failed) { qCCritical(logCat) << getUid().toString() << "Failed to process response chunk:" << m_sink->failReason(); } // qDebug() << "Request" << m_url.toString() << "gained" << data.size() << "bytes"; } else { qCCritical(logCat) << getUid().toString() << "Cannot write download data! illegal status" << m_status; } } auto NetRequest::abort() -> bool { m_state = State::AbortedByUser; if (m_reply) { disconnect(m_reply.get(), &QNetworkReply::errorOccurred, nullptr, nullptr); m_reply->abort(); } return true; } int NetRequest::replyStatusCode() const { return m_reply ? m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() : -1; } QNetworkReply::NetworkError NetRequest::error() const { return m_reply ? m_reply->error() : QNetworkReply::NoError; } QUrl NetRequest::url() const { return m_url; } QString NetRequest::errorString() const { return m_reply ? m_reply->errorString() : ""; } void NetRequest::enableAutoRetry(bool enable) { if (enable) { m_options |= Option::AutoRetry; } else { m_options &= ~static_cast(Option::AutoRetry); } } } // namespace Net PrismLauncher-11.0.3/launcher/net/Validator.h0000644000175100017510000000342315224505336020452 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 namespace Net { class Validator { public: /* con/des */ Validator() {} virtual ~Validator() {} public: /* methods */ virtual bool init(QNetworkRequest& request) = 0; virtual bool write(QByteArray& data) = 0; virtual bool abort() = 0; virtual bool validate(QNetworkReply& reply) = 0; }; } // namespace Net PrismLauncher-11.0.3/launcher/net/MetaCacheSink.h0000644000175100017510000000406515224505336021167 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ChecksumValidator.h" #include "FileSink.h" #include "net/HttpMetaCache.h" namespace Net { class MetaCacheSink : public FileSink { public: MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum, bool is_eternal = false); virtual ~MetaCacheSink() = default; auto hasLocalData() -> bool override; protected: auto initCache(QNetworkRequest& request) -> Task::State override; auto finalizeCache(QNetworkReply& reply) -> Task::State override; private: MetaEntryPtr m_entry; ChecksumValidator* m_md5Node; bool m_is_eternal; }; } // namespace Net PrismLauncher-11.0.3/launcher/net/ApiDownload.cpp0000644000175100017510000000323515224505336021262 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "net/ApiDownload.h" #include #include "net/ApiHeaderProxy.h" namespace Net { Download::Ptr ApiDownload::makeCached(QUrl url, MetaEntryPtr entry, Download::Options options) { auto dl = Download::makeCached(std::move(url), std::move(entry), options); dl->addHeaderProxy(std::make_unique()); return dl; } std::pair ApiDownload::makeByteArray(QUrl url, Download::Options options) { auto [dl, response] = Download::makeByteArray(std::move(url), options); dl->addHeaderProxy(std::make_unique()); return { dl, response }; } Download::Ptr ApiDownload::makeFile(QUrl url, QString path, Download::Options options, ModrinthDownloadMeta meta) { auto dl = Download::makeFile(std::move(url), std::move(path), options); dl->addHeaderProxy(std::make_unique(std::move(meta))); return dl; } } // namespace Net PrismLauncher-11.0.3/launcher/net/HttpMetaCache.cpp0000644000175100017510000002537115224505336021540 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "HttpMetaCache.h" #include "FileSystem.h" #include "Json.h" #include #include #include #include #include #include "net/Logging.h" auto MetaEntry::getFullPath() -> QString { // FIXME: make local? return FS::PathCombine(m_basePath, m_relativePath); } HttpMetaCache::HttpMetaCache(QString path) : QObject(), m_index_file(path) { saveBatchingTimer.setSingleShot(true); saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer); connect(&saveBatchingTimer, &QTimer::timeout, this, &HttpMetaCache::SaveNow); } HttpMetaCache::~HttpMetaCache() { saveBatchingTimer.stop(); SaveNow(); } auto HttpMetaCache::getEntry(QString base, QString resource_path) -> MetaEntryPtr { // no base. no base path. can't store if (!m_entries.contains(base)) { // TODO: log problem return {}; } EntryMap& map = m_entries[base]; if (map.entry_list.contains(resource_path)) { return map.entry_list[resource_path]; } return {}; } auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr { resource_path = FS::RemoveInvalidPathChars(resource_path); auto entry = getEntry(base, resource_path); // it's not present? generate a default stale entry if (!entry) { return staleEntry(base, resource_path); } auto& selected_base = m_entries[base]; QString real_path = FS::PathCombine(selected_base.base_path, resource_path); QFileInfo finfo(real_path); // is the file really there? if not -> stale if (!finfo.isFile() || !finfo.isReadable()) { // if the file doesn't exist, we disown the entry selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } if (!expected_etag.isEmpty() && expected_etag != entry->m_etag) { // if the etag doesn't match expected, we disown the entry selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } // if the file changed, check md5sum qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch(); if (file_last_changed != entry->m_local_changed_timestamp) { QFile input(real_path); if (!input.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file" << input.fileName() << "for reading:" << input.errorString(); return staleEntry(base, resource_path); } QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData(); if (entry->m_md5sum != md5sum) { selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } // md5sums matched... keep entry and save the new state to file entry->m_local_changed_timestamp = file_last_changed; SaveEventually(); } // Get rid of old entries, to prevent cache problems auto current_time = QDateTime::currentSecsSinceEpoch(); if (entry->isExpired(current_time - (file_last_changed / 1000))) { qCWarning(taskNetLogC) << "[HttpMetaCache]" << "Removing cache entry because of old age!"; selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } // entry passed all the checks we cared about. entry->m_basePath = getBasePath(base); return entry; } auto HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) -> bool { if (!m_entries.contains(stale_entry->m_baseId)) { qCCritical(taskHttpMetaCacheLogC) << "Cannot add entry with unknown base:" << stale_entry->m_baseId.toLocal8Bit(); return false; } if (stale_entry->m_stale) { qCCritical(taskHttpMetaCacheLogC) << "Cannot add stale entry:" << stale_entry->getFullPath().toLocal8Bit(); return false; } m_entries[stale_entry->m_baseId].entry_list[stale_entry->m_relativePath] = stale_entry; SaveEventually(); return true; } auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool { if (!entry) return false; entry->m_stale = true; SaveEventually(); return true; } // returns true on success, false otherwise auto HttpMetaCache::evictAll() -> bool { bool ret = true; for (QString& base : m_entries.keys()) { EntryMap& map = m_entries[base]; qCDebug(taskHttpMetaCacheLogC) << "Evicting base" << base; for (MetaEntryPtr entry : map.entry_list) { if (!evictEntry(entry)) qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath; } map.entry_list.clear(); // AND all return codes together so the result is true iff all runs of deletePath() are true ret &= FS::deleteContents(map.base_path); } return ret; } auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr { auto foo = new MetaEntry(); foo->m_baseId = base; foo->m_basePath = getBasePath(base); foo->m_relativePath = resource_path; foo->m_stale = true; return MetaEntryPtr(foo); } void HttpMetaCache::addBase(QString base, QString base_root) { // TODO: report error if (m_entries.contains(base)) return; // TODO: check if the base path is valid EntryMap foo; foo.base_path = base_root; m_entries[base] = foo; } auto HttpMetaCache::getBasePath(QString base) -> QString { if (m_entries.contains(base)) { return m_entries[base].base_path; } return {}; } void HttpMetaCache::Load() { if (m_index_file.isNull()) return; QFile index(m_index_file); if (!index.open(QIODevice::ReadOnly)) return; QJsonParseError parseError; QJsonDocument json = QJsonDocument::fromJson(index.readAll(), &parseError); // Fail if the JSON is invalid. if (parseError.error != QJsonParseError::NoError) { qCritical() << QString("Failed to parse HttpMetaCache file: %1 at offset %2") .arg(parseError.errorString(), QString::number(parseError.offset)) .toUtf8(); return; } // Make sure the root is an object. if (!json.isObject()) { qCritical() << "HttpMetaCache root should be an object."; return; } auto root = json.object(); // check file version first auto version_val = root["version"].toString(); if (version_val != "1") return; // read the entry array auto array = root["entries"].toArray(); for (auto element : array) { auto element_obj = element.toObject(); auto base = element_obj["base"].toString(); if (!m_entries.contains(base)) continue; auto& entrymap = m_entries[base]; auto foo = new MetaEntry(); foo->m_baseId = base; foo->m_relativePath = element_obj["path"].toString(); foo->m_md5sum = element_obj["md5sum"].toString(); foo->m_etag = element_obj["etag"].toString(); foo->m_local_changed_timestamp = element_obj["last_changed_timestamp"].toDouble(); foo->m_remote_changed_timestamp = element_obj["remote_changed_timestamp"].toString(); foo->makeEternal(element_obj[QStringLiteral("eternal")].toBool()); if (!foo->isEternal()) { foo->m_current_age = element_obj["current_age"].toDouble(); foo->m_max_age = element_obj["max_age"].toDouble(); } // presumed innocent until closer examination foo->m_stale = false; entrymap.entry_list[foo->m_relativePath] = MetaEntryPtr(foo); } } void HttpMetaCache::SaveEventually() { // reset the save timer saveBatchingTimer.stop(); saveBatchingTimer.start(30000); } void HttpMetaCache::SaveNow() { if (m_index_file.isNull()) return; qCDebug(taskHttpMetaCacheLogC) << "Saving metacache with" << m_entries.size() << "entries"; QJsonObject toplevel; Json::writeString(toplevel, "version", "1"); QJsonArray entriesArr; for (auto group : m_entries) { for (auto entry : group.entry_list) { // do not save stale entries. they are dead. if (entry->m_stale) { continue; } QJsonObject entryObj; Json::writeString(entryObj, "base", entry->m_baseId); Json::writeString(entryObj, "path", entry->m_relativePath); Json::writeString(entryObj, "md5sum", entry->m_md5sum); Json::writeString(entryObj, "etag", entry->m_etag); entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->m_local_changed_timestamp))); if (!entry->m_remote_changed_timestamp.isEmpty()) entryObj.insert("remote_changed_timestamp", QJsonValue(entry->m_remote_changed_timestamp)); if (entry->isEternal()) { entryObj.insert("eternal", true); } else { entryObj.insert("current_age", QJsonValue(double(entry->m_current_age))); entryObj.insert("max_age", QJsonValue(double(entry->m_max_age))); } entriesArr.append(entryObj); } } toplevel.insert("entries", entriesArr); try { Json::write(toplevel, m_index_file); } catch (const Exception& e) { qCWarning(taskHttpMetaCacheLogC) << "Error writing cache:" << e.what(); } } PrismLauncher-11.0.3/launcher/net/MetaCacheSink.cpp0000644000175100017510000001150115224505336021513 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "MetaCacheSink.h" #include #include #include #include "Application.h" #include "net/Logging.h" namespace Net { /** Maximum time to hold a cache entry * = 1 week in seconds */ #define MAX_TIME_TO_EXPIRE 1 * 7 * 24 * 60 * 60 MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum, bool is_eternal) : Net::FileSink(entry->getFullPath()), m_entry(entry), m_md5Node(md5sum), m_is_eternal(is_eternal) { addValidator(md5sum); } Task::State MetaCacheSink::initCache(QNetworkRequest& request) { if (!m_entry->isStale()) { return Task::State::Succeeded; } // check if file exists, if it does, use its information for the request QFile current(m_filename); if (current.exists() && current.size() != 0) { if (m_entry->getRemoteChangedTimestamp().size()) { request.setRawHeader(QString("If-Modified-Since").toLatin1(), m_entry->getRemoteChangedTimestamp().toLatin1()); } if (m_entry->getETag().size()) { request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->getETag().toLatin1()); } } return Task::State::Running; } Task::State MetaCacheSink::finalizeCache(QNetworkReply& reply) { QFileInfo output_file_info(m_filename); if (m_wroteAnyData) { m_entry->setMD5Sum(m_md5Node->hash().toHex().constData()); } m_entry->setETag(reply.rawHeader("ETag").constData()); if (reply.hasRawHeader("Last-Modified")) { m_entry->setRemoteChangedTimestamp(reply.rawHeader("Last-Modified").constData()); } m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch()); { // Cache lifetime if (m_is_eternal) { qCDebug(taskMetaCacheLogC) << "Adding eternal cache entry:" << m_entry->getFullPath(); m_entry->makeEternal(true); } else if (reply.hasRawHeader("Cache-Control")) { auto cache_control_header = reply.rawHeader("Cache-Control"); qCDebug(taskMetaCacheLogC) << "Parsing 'Cache-Control' header with" << cache_control_header; static const QRegularExpression s_maxAgeExpr("max-age=([0-9]+)"); qint64 max_age = s_maxAgeExpr.match(cache_control_header).captured(1).toLongLong(); m_entry->setMaximumAge(max_age); } else if (reply.hasRawHeader("Expires")) { auto expires_header = reply.rawHeader("Expires"); qCDebug(taskMetaCacheLogC) << "Parsing 'Expires' header with" << expires_header; qint64 max_age = QDateTime::fromString(expires_header).toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch(); m_entry->setMaximumAge(max_age); } else { m_entry->setMaximumAge(MAX_TIME_TO_EXPIRE); } if (reply.hasRawHeader("Age")) { auto age_header = reply.rawHeader("Age"); qCDebug(taskMetaCacheLogC) << "Parsing 'Age' header with" << age_header; qint64 current_age = age_header.toLongLong(); m_entry->setCurrentAge(current_age); } else { m_entry->setCurrentAge(0); } } m_entry->setStale(false); APPLICATION->metacache()->updateEntry(m_entry); return Task::State::Succeeded; } bool MetaCacheSink::hasLocalData() { QFileInfo info(m_filename); return info.exists() && info.size() != 0; } } // namespace Net PrismLauncher-11.0.3/launcher/net/Upload.h0000644000175100017510000000430515224505336017751 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include "net/NetRequest.h" namespace Net { class Upload : public NetRequest { Q_OBJECT public: using Ptr = shared_qobject_ptr; explicit Upload() : NetRequest() { logCat = taskUploadLogC; }; /** * Creates a request downloading to the returned QByteArray,. * The QByteArray will live as long as the Upload object. */ static std::pair makeByteArray(QUrl url, QByteArray m_post_data); protected: virtual QNetworkReply* getReply(QNetworkRequest&) override; QByteArray m_post_data; }; } // namespace Net PrismLauncher-11.0.3/launcher/net/Sink.h0000644000175100017510000000607015224505336017432 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Validator.h" #include "tasks/Task.h" namespace Net { class Sink { public: Sink() = default; virtual ~Sink() = default; public: virtual auto init(QNetworkRequest& request) -> Task::State = 0; virtual auto write(QByteArray& data) -> Task::State = 0; virtual auto abort() -> Task::State = 0; virtual auto finalize(QNetworkReply& reply) -> Task::State = 0; virtual auto hasLocalData() -> bool = 0; QString failReason() const { return m_fail_reason; } void addValidator(Validator* validator) { if (validator) { validators.push_back(std::shared_ptr(validator)); } } protected: bool initAllValidators(QNetworkRequest& request) { for (auto& validator : validators) { if (!validator->init(request)) return false; } return true; } bool finalizeAllValidators(QNetworkReply& reply) { for (auto& validator : validators) { if (!validator->validate(reply)) return false; } return true; } bool failAllValidators() { bool success = true; for (auto& validator : validators) { success &= validator->abort(); } return success; } bool writeAllValidators(QByteArray& data) { for (auto& validator : validators) { if (!validator->write(data)) return false; } return true; } protected: std::vector> validators; QString m_fail_reason; }; } // namespace Net PrismLauncher-11.0.3/launcher/net/NetUtils.h0000644000175100017510000000626015224505336020276 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include namespace Net { inline bool isApplicationError(QNetworkReply::NetworkError x) { // Mainly taken from https://github.com/qt/qtbase/blob/dev/src/network/access/qhttpthreaddelegate.cpp static QSet errors = { QNetworkReply::ProtocolInvalidOperationError, QNetworkReply::AuthenticationRequiredError, QNetworkReply::ContentAccessDenied, QNetworkReply::ContentNotFoundError, QNetworkReply::ContentOperationNotPermittedError, QNetworkReply::ProxyAuthenticationRequiredError, QNetworkReply::ContentConflictError, QNetworkReply::ContentGoneError, QNetworkReply::InternalServerError, QNetworkReply::OperationNotImplementedError, QNetworkReply::ServiceUnavailableError, QNetworkReply::UnknownServerError, QNetworkReply::UnknownContentError }; return errors.contains(x); } // 500 class errors, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/500 // microsoft may send these error codes when services (auth) are down. // We treat this as a reason to launch in offline mode. inline bool isServerError(QNetworkReply::NetworkError x) { static QSet errors = { QNetworkReply::InternalServerError, QNetworkReply::OperationNotImplementedError, QNetworkReply::ServiceUnavailableError, // 503 | seen in logs in 2026 //QNetworkReply::GatewayTimeoutError, // 504 | seen in logs in 2024 // Qt doesn't have it mapped. Unknown covers it QNetworkReply::UnknownServerError }; return errors.contains(x); } } // namespace Net PrismLauncher-11.0.3/launcher/net/Logging.h0000644000175100017510000000206215224505336020111 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include Q_DECLARE_LOGGING_CATEGORY(taskNetLogC) Q_DECLARE_LOGGING_CATEGORY(taskDownloadLogC) Q_DECLARE_LOGGING_CATEGORY(taskUploadLogC) Q_DECLARE_LOGGING_CATEGORY(taskMCSkinsLogC) Q_DECLARE_LOGGING_CATEGORY(taskMetaCacheLogC) Q_DECLARE_LOGGING_CATEGORY(taskHttpMetaCacheLogC) PrismLauncher-11.0.3/launcher/net/DummySink.h0000644000175100017510000000246115224505336020446 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once namespace Net { class DummySink : public Sink { public: explicit DummySink() {} ~DummySink() override {} auto init(QNetworkRequest& request) -> Task::State override { return Task::State::Running; } auto write(QByteArray& data) -> Task::State override { return Task::State::Succeeded; } auto abort() -> Task::State override { return Task::State::AbortedByUser; } auto finalize(QNetworkReply& reply) -> Task::State override { return Task::State::Succeeded; } auto hasLocalData() -> bool override { return false; } }; } // namespace Net PrismLauncher-11.0.3/launcher/net/ApiUpload.cpp0000644000175100017510000000213415224505336020734 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "net/ApiUpload.h" #include "net/ApiHeaderProxy.h" namespace Net { std::pair ApiUpload::makeByteArray(QUrl url, QByteArray m_post_data) { auto [up, response] = Upload::makeByteArray(url, m_post_data); up->addHeaderProxy(std::make_unique()); return { up, response }; } } // namespace Net PrismLauncher-11.0.3/launcher/net/NetJob.cpp0000644000175100017510000001414215224505336020241 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "NetJob.h" #include #include "net/NetRequest.h" #include "tasks/ConcurrentTask.h" #if defined(LAUNCHER_APPLICATION) #include "Application.h" #include "settings/SettingsObject.h" #include "ui/dialogs/NetworkJobFailedDialog.h" #endif NetJob::NetJob(QString job_name, QNetworkAccessManager* network, int max_concurrent) : ConcurrentTask(job_name), m_network(network) { #if defined(LAUNCHER_APPLICATION) if (APPLICATION_DYN && max_concurrent < 0) max_concurrent = APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt(); #endif if (max_concurrent > 0) setMaxConcurrent(max_concurrent); } auto NetJob::addNetAction(Net::NetRequest::Ptr action) -> bool { action->setNetwork(m_network); addTask(action); return true; } void NetJob::executeNextSubTask() { // We're finished, check for failures and retry if we can (up to 3 times) if (isRunning() && m_queue.isEmpty() && m_doing.isEmpty() && !m_failed.isEmpty() && m_try < 3) { m_try += 1; m_failed.removeIf([this](QHash::iterator task) { // there is no point in retying on 404 Not Found if (static_cast(task->get())->replyStatusCode() == 404) { return false; } m_done.remove(task->get()); m_queue.enqueue(*task); return true; }); } ConcurrentTask::executeNextSubTask(); } auto NetJob::size() const -> int { return m_queue.size() + m_doing.size() + m_done.size(); } auto NetJob::canAbort() const -> bool { bool canFullyAbort = true; // can abort the downloads on the queue? for (auto part : m_queue) canFullyAbort &= part->canAbort(); // can abort the active downloads? for (auto part : m_doing) canFullyAbort &= part->canAbort(); return canFullyAbort; } auto NetJob::abort() -> bool { // fail all downloads on the queue for (auto task : m_queue) m_failed.insert(task.get(), task); m_queue.clear(); if (m_doing.isEmpty()) { // no downloads to abort, NetJob is not running return true; } bool fullyAborted = true; // abort active downloads auto toKill = m_doing.values(); for (auto part : toKill) { fullyAborted &= part->abort(); } if (fullyAborted) emitAborted(); else emitFailed(tr("Failed to abort all tasks in the NetJob!")); return fullyAborted; } auto NetJob::getFailedActions() -> QList { QList failed; for (auto index : m_failed) { failed.push_back(dynamic_cast(index.get())); } return failed; } auto NetJob::getFailedFiles() -> QList { QList failed; for (auto index : m_failed) { failed.append(static_cast(index.get())->url().toString()); } return failed; } void NetJob::updateState() { emit progress(m_done.count(), totalSize()); setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)") .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize()))); } bool NetJob::isOnline() { // check some errors that are ussually associated with the lack of internet for (auto job : getFailedActions()) { auto err = job->error(); if (err != QNetworkReply::HostNotFoundError && err != QNetworkReply::NetworkSessionFailedError) { return true; } } return false; }; void NetJob::emitFailed(QString reason) { #if defined(LAUNCHER_APPLICATION) if (APPLICATION_DYN && m_ask_retry && m_manual_try < APPLICATION->settings()->get("NumberOfManualRetries").toInt() && isOnline()) { m_manual_try++; auto failed = getFailedActions(); auto dialog = new NetworkJobFailedDialog(objectName(), m_try, m_done.size(), failed.size(), nullptr); dialog->setAttribute(Qt::WA_DeleteOnClose); for (const auto& request : failed) { dialog->addFailedRequest(request->url(), request->errorString()); } dialog->open(); connect(dialog, &QDialog::finished, this, [this, reason = std::move(reason)](int result) { if (result == QDialog::Accepted) { m_try = 0; executeNextSubTask(); } else { ConcurrentTask::emitFailed(reason); } }); return; } #endif ConcurrentTask::emitFailed(reason); } void NetJob::setAskRetry(bool askRetry) { m_ask_retry = askRetry; } PrismLauncher-11.0.3/launcher/net/HttpMetaCache.h0000644000175100017510000001107715224505336021203 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include class HttpMetaCache; class MetaEntry { friend class HttpMetaCache; protected: MetaEntry() = default; public: auto isStale() -> bool { return m_stale; } void setStale(bool stale) { m_stale = stale; } auto getFullPath() -> QString; auto getRemoteChangedTimestamp() -> QString { return m_remote_changed_timestamp; } void setRemoteChangedTimestamp(QString remote_changed_timestamp) { m_remote_changed_timestamp = remote_changed_timestamp; } void setLocalChangedTimestamp(qint64 timestamp) { m_local_changed_timestamp = timestamp; } auto getETag() -> QString { return m_etag; } void setETag(QString etag) { m_etag = etag; } auto getMD5Sum() -> QString { return m_md5sum; } void setMD5Sum(QString md5sum) { m_md5sum = md5sum; } /* Whether the entry expires after some time (false) or not (true). */ void makeEternal(bool eternal) { m_is_eternal = eternal; } bool isEternal() const { return m_is_eternal; } auto getCurrentAge() -> qint64 { return m_current_age; } void setCurrentAge(qint64 age) { m_current_age = age; } auto getMaximumAge() -> qint64 { return m_max_age; } void setMaximumAge(qint64 age) { m_max_age = age; } bool isExpired(qint64 offset) { return !m_is_eternal && (m_current_age >= m_max_age - offset); } protected: QString m_baseId; QString m_basePath; QString m_relativePath; QString m_md5sum; QString m_etag; qint64 m_local_changed_timestamp = 0; QString m_remote_changed_timestamp; // QString for now, RFC 2822 encoded time qint64 m_current_age = 0; qint64 m_max_age = 0; bool m_is_eternal = false; bool m_stale = true; }; using MetaEntryPtr = std::shared_ptr; class HttpMetaCache : public QObject { Q_OBJECT public: // supply path to the cache index file HttpMetaCache(QString path = QString()); ~HttpMetaCache() override; // get the entry solely from the cache // you probably don't want this, unless you have some specific caching needs. auto getEntry(QString base, QString resource_path) -> MetaEntryPtr; // get the entry from cache and verify that it isn't stale (within reason) auto resolveEntry(QString base, QString resource_path, QString expected_etag = QString()) -> MetaEntryPtr; // add a previously resolved stale entry auto updateEntry(MetaEntryPtr stale_entry) -> bool; // evict selected entry from cache auto evictEntry(MetaEntryPtr entry) -> bool; bool evictAll(); void addBase(QString base, QString base_root); // (re)start a timer that calls SaveNow later. void SaveEventually(); void Load(); auto getBasePath(QString base) -> QString; public slots: void SaveNow(); private: // create a new stale entry, given the parameters auto staleEntry(QString base, QString resource_path) -> MetaEntryPtr; struct EntryMap { QString base_path; QMap entry_list; }; QMap m_entries; QString m_index_file; QTimer saveBatchingTimer; }; PrismLauncher-11.0.3/launcher/net/Logging.cpp0000644000175100017510000000224415224505336020446 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "net/Logging.h" Q_LOGGING_CATEGORY(taskNetLogC, "launcher.task.net") Q_LOGGING_CATEGORY(taskDownloadLogC, "launcher.task.net.download") Q_LOGGING_CATEGORY(taskUploadLogC, "launcher.task.net.upload") Q_LOGGING_CATEGORY(taskMCSkinsLogC, "launcher.task.minecraft.skins") Q_LOGGING_CATEGORY(taskMetaCacheLogC, "launcher.task.net.metacache") Q_LOGGING_CATEGORY(taskHttpMetaCacheLogC, "launcher.task.net.metacache.http") PrismLauncher-11.0.3/launcher/net/PasteUpload.h0000644000175100017510000000556115224505336020753 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Lenny McLennington * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "net/ByteArraySink.h" #include "net/NetRequest.h" #include "tasks/Task.h" #include #include #include #include #include #include class PasteUpload : public Net::NetRequest { public: enum PasteType : int { // 0x0.st NullPointer, // hastebin.com Hastebin, // paste.gg PasteGG, // mclo.gs Mclogs, // Helpful to get the range of valid values on the enum for input sanitisation: First = NullPointer, Last = Mclogs }; struct PasteTypeInfo { const QString name; const QString defaultBase; const QString endpointPath; }; static const std::array PasteTypes; class Sink : public Net::ByteArraySink { public: Sink(PasteUpload* p) : m_d(p) {}; virtual ~Sink() = default; public: auto finalize(QNetworkReply& reply) -> Task::State override; private: PasteUpload* m_d; }; friend Sink; PasteUpload(const QString& log, QString url, PasteType pasteType); virtual ~PasteUpload() = default; QString pasteLink() { return m_pasteLink; } private: virtual QNetworkReply* getReply(QNetworkRequest&) override; QString m_log; QString m_pasteLink; QString m_baseUrl; const PasteType m_paste_type; }; PrismLauncher-11.0.3/launcher/net/ByteArraySink.h0000644000175100017510000000553215224505336021257 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Sink.h" namespace Net { /* * Sink object for downloads that uses an owned QByteArray as a target. */ class ByteArraySink : public Sink { public: virtual ~ByteArraySink() = default; public: auto init(QNetworkRequest& request) -> Task::State override { m_output.clear(); if (initAllValidators(request)) return Task::State::Running; m_fail_reason = "Failed to initialize validators"; return Task::State::Failed; }; auto write(QByteArray& data) -> Task::State override { m_output.append(data); if (writeAllValidators(data)) return Task::State::Running; m_fail_reason = "Failed to write validators"; return Task::State::Failed; } auto abort() -> Task::State override { failAllValidators(); m_fail_reason = "Aborted"; return Task::State::Failed; } auto finalize(QNetworkReply& reply) -> Task::State override { if (finalizeAllValidators(reply)) return Task::State::Succeeded; m_fail_reason = "Failed to finalize validators"; return Task::State::Failed; } auto hasLocalData() -> bool override { return false; } QByteArray* output() { return &m_output; } protected: QByteArray m_output; }; } // namespace Net PrismLauncher-11.0.3/launcher/net/NetJob.h0000644000175100017510000000530515224505336017707 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include "net/NetRequest.h" #include "tasks/ConcurrentTask.h" // Those are included so that they are also included by anyone using NetJob #include "net/Download.h" #include "net/HttpMetaCache.h" class NetJob : public ConcurrentTask { Q_OBJECT public: // TODO: delete using Ptr = shared_qobject_ptr; explicit NetJob(QString job_name, QNetworkAccessManager* network, int max_concurrent = -1); ~NetJob() override = default; auto size() const -> int; auto canAbort() const -> bool override; auto addNetAction(Net::NetRequest::Ptr action) -> bool; auto getFailedActions() -> QList; auto getFailedFiles() -> QList; void setAskRetry(bool askRetry); public slots: // Qt can't handle auto at the start for some reason? bool abort() override; void emitFailed(QString reason) override; protected slots: void executeNextSubTask() override; protected: void updateState() override; bool isOnline(); private: QNetworkAccessManager* m_network; int m_try = 1; bool m_ask_retry = true; int m_manual_try = 0; }; PrismLauncher-11.0.3/launcher/net/ChecksumValidator.h0000644000175100017510000000551715224505336022143 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Validator.h" #include namespace Net { class ChecksumValidator : public Validator { public: ChecksumValidator(QCryptographicHash::Algorithm algorithm, QString expectedHex) : Net::ChecksumValidator(algorithm, QByteArray::fromHex(expectedHex.toLatin1())) {} ChecksumValidator(QCryptographicHash::Algorithm algorithm, QByteArray expected = QByteArray()) : m_checksum(algorithm), m_expected(expected) {}; virtual ~ChecksumValidator() = default; public: auto init(QNetworkRequest&) -> bool override { m_checksum.reset(); return true; } auto write(QByteArray& data) -> bool override { m_checksum.addData(data); return true; } auto abort() -> bool override { m_checksum.reset(); return true; } auto validate(QNetworkReply& reply) -> bool override { if (!m_expected.isEmpty() && m_expected != hash()) { qWarning() << "Checksum mismatch for URL:" << reply.url().toString() << "expected:" << m_expected << "got:" << hash(); return false; } return true; } auto hash() -> QByteArray { return m_checksum.result(); } void setExpected(QByteArray expected) { m_expected = expected; } private: QCryptographicHash m_checksum; QByteArray m_expected; }; } // namespace Net PrismLauncher-11.0.3/launcher/net/FileSink.cpp0000644000175100017510000001174215224505336020567 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "FileSink.h" #include "FileSystem.h" #include "net/Logging.h" namespace Net { Task::State FileSink::init(QNetworkRequest& request) { auto result = initCache(request); if (result != Task::State::Running) { return result; } // create a new save file and open it for writing if (!FS::ensureFilePathExists(m_filename)) { qCCritical(taskNetLogC) << "Could not create folder for " + m_filename; m_fail_reason = "Could not create folder"; return Task::State::Failed; } m_wroteAnyData = false; m_output_file.reset(new PSaveFile(m_filename)); if (!m_output_file->open(QIODevice::WriteOnly)) { const auto error = QString("Could not open %1 for writing: %2").arg(m_filename).arg(m_output_file->errorString()); qCCritical(taskNetLogC) << error; m_fail_reason = error; return Task::State::Failed; } if (initAllValidators(request)) return Task::State::Running; m_fail_reason = "Failed to initialize validators"; return Task::State::Failed; } Task::State FileSink::write(QByteArray& data) { if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) { QString error = QString("Failed writing into %1: %2").arg(m_filename); if (m_output_file->error() == QFileDevice::NoError) { error = error.arg("Validators failed"); } else { error = error.arg(m_output_file->errorString()); } qCCritical(taskNetLogC) << error; m_fail_reason = error; m_output_file->cancelWriting(); m_output_file.reset(); m_wroteAnyData = false; return Task::State::Failed; } m_wroteAnyData = true; return Task::State::Running; } Task::State FileSink::abort() { if (m_output_file) { m_output_file->cancelWriting(); } failAllValidators(); return Task::State::Failed; } Task::State FileSink::finalize(QNetworkReply& reply) { bool gotFile = false; QVariant statusCodeV = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute); bool validStatus = false; int statusCode = statusCodeV.toInt(&validStatus); if (validStatus) { // this leaves out 304 Not Modified gotFile = statusCode == 200 || statusCode == 203; } // 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 || 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)) { m_fail_reason = "Failed to finalize validators"; return Task::State::Failed; } // nothing went wrong... if (!m_output_file->commit()) { const auto error = QString("Failed to commit changes to %1: %2").arg(m_filename).arg(m_output_file->errorString()); qCCritical(taskNetLogC) << error; m_fail_reason = error; m_output_file->cancelWriting(); return Task::State::Failed; } } // then get rid of the save file m_output_file.reset(); return finalizeCache(reply); } Task::State FileSink::initCache(QNetworkRequest&) { return Task::State::Running; } Task::State FileSink::finalizeCache(QNetworkReply&) { return Task::State::Succeeded; } bool FileSink::hasLocalData() { QFileInfo info(m_filename); return info.exists() && info.size() != 0; } } // namespace Net PrismLauncher-11.0.3/launcher/net/HeaderProxy.h0000644000175100017510000000245015224505336020756 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include namespace Net { struct HeaderPair { QByteArray headerName; QByteArray headerValue; }; class HeaderProxy { public: HeaderProxy() {} virtual ~HeaderProxy() {} public: virtual QList headers(const QNetworkRequest& request) const = 0; public: void writeHeaders(QNetworkRequest& request) { for (auto header : headers(request)) { request.setRawHeader(header.headerName, header.headerValue); } } }; } // namespace Net PrismLauncher-11.0.3/launcher/net/NetRequest.h0000644000175100017510000000765715224505336020641 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include "HeaderProxy.h" #include "Sink.h" #include "Validator.h" #include "QObjectPtr.h" #include "net/Logging.h" #include "tasks/Task.h" namespace Net { class NetRequest : public Task { Q_OBJECT protected: explicit NetRequest(); public: using Ptr = shared_qobject_ptr; enum class Option { NoOptions = 0, AcceptLocalFiles = 1, MakeEternal = 2, AutoRetry = 4 }; Q_DECLARE_FLAGS(Options, Option) public: ~NetRequest() override = default; void addValidator(Validator* v); auto abort() -> bool override; auto canAbort() const -> bool override { return true; } void setNetwork(QNetworkAccessManager* network) { m_network = network; } void addHeaderProxy(std::unique_ptr proxy) { m_headerProxies.push_back(std::move(proxy)); } // automatically handle HTTP 429 Too Many Requests errors and retry void enableAutoRetry(bool enable); QUrl url() const; void setUrl(QUrl url) { m_url = url; } int replyStatusCode() const; QNetworkReply::NetworkError error() const; QString errorString() const; private: auto handleRedirect() -> bool; void handleAutoRetry(int64_t delay); virtual QNetworkReply* getReply(QNetworkRequest&) = 0; protected slots: void onProgress(qint64 bytesReceived, qint64 bytesTotal); void downloadError(QNetworkReply::NetworkError error); void sslErrors(const QList& errors); void downloadFinished(); void downloadReadyRead(); void executeTask() override; protected: std::unique_ptr m_sink; Options m_options; using logCatFunc = const QLoggingCategory& (*)(); logCatFunc logCat = taskUploadLogC; std::chrono::steady_clock m_clock; std::chrono::time_point m_last_progress_time; qint64 m_last_progress_bytes; QNetworkAccessManager* m_network; /// the network reply std::unique_ptr m_reply; QByteArray m_errorResponse; /// source URL QUrl m_url; std::vector> m_headerProxies; int m_retryCount = 0; QTimer m_retryTimer; }; } // namespace Net Q_DECLARE_OPERATORS_FOR_FLAGS(Net::NetRequest::Options) PrismLauncher-11.0.3/launcher/net/ApiDownload.h0000644000175100017510000000254615224505336020733 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include "Download.h" #include "net/ApiHeaderProxy.h" namespace Net { namespace ApiDownload { Download::Ptr makeCached(QUrl url, MetaEntryPtr entry, Download::Options options = Download::Option::NoOptions); std::pair makeByteArray(QUrl url, Download::Options options = Download::Option::NoOptions); Download::Ptr makeFile(QUrl url, QString path, Download::Options options = Download::Option::NoOptions, ModrinthDownloadMeta meta = ModrinthDownloadMeta()); }; // namespace ApiDownload } // namespace Net PrismLauncher-11.0.3/launcher/net/Mode.h0000644000175100017510000000010515224505336017403 0ustar runnerrunner#pragma once namespace Net { enum class Mode { Offline, Online }; } PrismLauncher-11.0.3/launcher/net/PasteUpload.cpp0000644000175100017510000002350615224505336021305 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Lenny McLennington * Copyright (C) 2022 Swirl * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "PasteUpload.h" #include #include #include #include #include #include #include "logs/AnonymizeLog.h" const std::array PasteUpload::PasteTypes = { { { "0x0.st", "https://0x0.st", "" }, { "hastebin", "https://hst.sh", "/documents" }, { "paste.gg", "https://paste.gg", "/api/v1/pastes" }, { "mclo.gs", "https://api.mclo.gs", "/1/log" } } }; QNetworkReply* PasteUpload::getReply(QNetworkRequest& request) { switch (m_paste_type) { case PasteUpload::NullPointer: { QHttpMultiPart* multiPart = new QHttpMultiPart{ QHttpMultiPart::FormDataType, this }; QHttpPart filePart; filePart.setBody(m_log.toUtf8()); filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"file\"; filename=\"log.txt\""); multiPart->append(filePart); return m_network->post(request, multiPart); } case PasteUpload::Hastebin: { return m_network->post(request, m_log.toUtf8()); } case PasteUpload::Mclogs: { QUrlQuery postData; postData.addQueryItem("content", m_log); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); return m_network->post(request, postData.toString().toUtf8()); } case PasteUpload::PasteGG: { QJsonObject obj; QJsonDocument doc; request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); obj.insert("expires", QDateTime::currentDateTimeUtc().addDays(100).toString(Qt::DateFormat::ISODate)); QJsonArray files; QJsonObject logFileInfo; QJsonObject logFileContentInfo; logFileContentInfo.insert("format", "text"); logFileContentInfo.insert("value", m_log); logFileInfo.insert("name", "log.txt"); logFileInfo.insert("content", logFileContentInfo); files.append(logFileInfo); obj.insert("files", files); doc.setObject(obj); return m_network->post(request, doc.toJson()); } } return nullptr; }; auto PasteUpload::Sink::finalize(QNetworkReply& reply) -> Task::State { if (!finalizeAllValidators(reply)) { m_fail_reason = "Failed to finalize validators"; return Task::State::Failed; } int statusCode = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply.error() != QNetworkReply::NetworkError::NoError) { m_fail_reason = QObject::tr("Network error: %1").arg(reply.errorString()); return Task::State::Failed; } else if (statusCode != 200 && statusCode != 201) { QString reasonPhrase = reply.attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); m_fail_reason = QObject::tr("Error: %1 returned unexpected status code %2 %3").arg(m_d->url().toString()).arg(statusCode).arg(reasonPhrase); return Task::State::Failed; } switch (m_d->m_paste_type) { case PasteUpload::NullPointer: m_d->m_pasteLink = QString::fromUtf8(*output()).trimmed(); break; case PasteUpload::Hastebin: { QJsonParseError jsonError; auto doc = QJsonDocument::fromJson(*output(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "hastebin server did not reply with JSON" << jsonError.errorString(); m_fail_reason = QObject::tr("Failed to parse response from hastebin server: expected JSON but got an invalid response. Error: %1") .arg(jsonError.errorString()); return Task::State::Failed; } auto obj = doc.object(); if (obj.contains("key") && obj["key"].isString()) { QString key = doc.object()["key"].toString(); m_d->m_pasteLink = m_d->m_baseUrl + "/" + key; } else { qDebug() << "Log upload failed:" << doc.toJson(); m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); return Task::State::Failed; } break; } case PasteUpload::Mclogs: { QJsonParseError jsonError; auto doc = QJsonDocument::fromJson(*output(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "mclogs server did not reply with JSON" << jsonError.errorString(); m_fail_reason = QObject::tr("Failed to parse response from mclogs server: expected JSON but got an invalid response. Error: %1") .arg(jsonError.errorString()); return Task::State::Failed; } auto obj = doc.object(); if (obj.contains("success") && obj["success"].isBool()) { bool success = obj["success"].toBool(); if (success) { m_d->m_pasteLink = obj["url"].toString(); } else { QString error = obj["error"].toString(); m_fail_reason = QObject::tr("Error: %1 returned an error: %2").arg(m_d->url().toString(), error); return Task::State::Failed; } } else { qDebug() << "Log upload failed:" << doc.toJson(); m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); return Task::State::Failed; } break; } case PasteUpload::PasteGG: QJsonParseError jsonError; auto doc = QJsonDocument::fromJson(*output(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "pastegg server did not reply with JSON" << jsonError.errorString(); m_fail_reason = QObject::tr("Failed to parse response from pasteGG server: expected JSON but got an invalid response. Error: %1") .arg(jsonError.errorString()); return Task::State::Failed; } auto obj = doc.object(); if (obj.contains("status") && obj["status"].isString()) { QString status = obj["status"].toString(); if (status == "success") { m_d->m_pasteLink = m_d->m_baseUrl + "/p/anonymous/" + obj["result"].toObject()["id"].toString(); } else { QString error = obj["error"].toString(); QString message = (obj.contains("message") && obj["message"].isString()) ? obj["message"].toString() : "none"; m_fail_reason = QObject::tr("Error: %1 returned an error code: %2\nError message: %3").arg(m_d->url().toString(), error, message); return Task::State::Failed; } } else { qDebug() << "Log upload failed:" << doc.toJson(); m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); return Task::State::Failed; } break; } return Task::State::Succeeded; } PasteUpload::PasteUpload(const QString& log, QString url, PasteType pasteType) : m_log(log), m_baseUrl(url), m_paste_type(pasteType) { anonymizeLog(m_log); auto base = PasteUpload::PasteTypes.at(pasteType); if (m_baseUrl.isEmpty()) m_baseUrl = base.defaultBase; // HACK: Paste's docs say the standard API path is at /api/ but the official instance paste.gg doesn't follow that?? if (pasteType == PasteUpload::PasteGG && m_baseUrl == base.defaultBase) m_url = "https://api.paste.gg/v1/pastes"; else m_url = m_baseUrl + base.endpointPath; m_sink.reset(new Sink(this)); } PrismLauncher-11.0.3/launcher/net/Download.h0000644000175100017510000000503215224505336020272 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include "HttpMetaCache.h" #include "QObjectPtr.h" #include "net/NetRequest.h" namespace Net { class ByteArraySink; class Download : public NetRequest { Q_OBJECT public: using Ptr = shared_qobject_ptr; explicit Download() : NetRequest() { logCat = taskDownloadLogC; } #if defined(LAUNCHER_APPLICATION) static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr; #endif /** * Creates a request downloading to the returned QByteArray,. * The QByteArray will live as long as the Download object. */ static auto makeByteArray(QUrl url, Options options = Option::NoOptions) -> std::pair; static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr; protected: virtual QNetworkReply* getReply(QNetworkRequest&) override; }; } // namespace Net PrismLauncher-11.0.3/launcher/net/RawHeaderProxy.h0000644000175100017510000000332015224505336021425 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include "net/HeaderProxy.h" namespace Net { class RawHeaderProxy : public HeaderProxy { public: RawHeaderProxy(QList headers = {}) : HeaderProxy(), m_headers(std::move(headers)) {}; virtual ~RawHeaderProxy() = default; public: virtual QList headers(const QNetworkRequest&) const override { return m_headers; }; void addHeader(const HeaderPair& header) { m_headers.append(header); } void addHeader(const QByteArray& headerName, const QByteArray& headerValue) { m_headers.append({ headerName, headerValue }); } void addHeaders(const QList& headers) { m_headers.append(headers); } void setHeaders(QList headers) { m_headers = headers; }; private: QList m_headers; }; } // namespace Net PrismLauncher-11.0.3/launcher/net/ApiHeaderProxy.h0000644000175100017510000000533515224505336021415 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include "Application.h" #include "BuildConfig.h" #include "net/HeaderProxy.h" #include #include namespace Net { struct ModrinthDownloadMeta { QString reason; QString gameVersion; QString loader; bool isEmpty() const { return reason.isEmpty(); } QByteArray toJson() const { QJsonObject obj; if (!reason.isEmpty()) { obj["reason"] = reason; } if (!gameVersion.isEmpty()) { obj["game_version"] = gameVersion; } if (!loader.isEmpty()) { obj["loader"] = loader; } return QJsonDocument(obj).toJson(QJsonDocument::Compact); } }; class ApiHeaderProxy : public HeaderProxy { public: ApiHeaderProxy() = default; explicit ApiHeaderProxy(ModrinthDownloadMeta meta) : m_meta(std::move(meta)) {} ~ApiHeaderProxy() override = default; public: QList headers(const QNetworkRequest& request) const override { QList hdrs; const auto host = request.url().host(); if (APPLICATION->capabilities() & Application::SupportsFlame && (host == QUrl(BuildConfig.FLAME_BASE_URL).host() || host == BuildConfig.FLAME_DOWNLOAD_HOST)) { hdrs.append({ .headerName = "x-api-key", .headerValue = APPLICATION->getFlameAPIKey().toUtf8() }); } else if (host == QUrl(BuildConfig.MODRINTH_PROD_URL).host() || host == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) { QString token = APPLICATION->getModrinthAPIToken(); if (!token.isNull()) { hdrs.append({ .headerName = "Authorization", .headerValue = token.toUtf8() }); } } if (host == BuildConfig.MODRINTH_DOWNLOAD_HOST && !m_meta.isEmpty()) { hdrs.append({ .headerName = "modrinth-download-meta", .headerValue = m_meta.toJson() }); } return hdrs; }; private: ModrinthDownloadMeta m_meta; }; } // namespace Net PrismLauncher-11.0.3/launcher/net/Upload.cpp0000644000175100017510000000456115224505336020310 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Upload.h" #include #include #include "ByteArraySink.h" namespace Net { QNetworkReply* Upload::getReply(QNetworkRequest& request) { if (!request.hasRawHeader("Content-Type")) request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); return m_network->post(request, m_post_data); } std::pair Upload::makeByteArray(QUrl url, QByteArray m_post_data) { auto up = makeShared(); up->m_url = std::move(url); auto sink = std::make_unique(); QByteArray* response = sink->output(); up->m_sink = std::move(sink); up->m_post_data = std::move(m_post_data); return { up, response }; } } // namespace Net PrismLauncher-11.0.3/launcher/net/ApiUpload.h0000644000175100017510000000166215224505336020406 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include "Upload.h" namespace Net { namespace ApiUpload { std::pair makeByteArray(QUrl url, QByteArray m_post_data); }; } // namespace Net PrismLauncher-11.0.3/launcher/net/FileSink.h0000644000175100017510000000433115224505336020230 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "PSaveFile.h" #include "Sink.h" namespace Net { class FileSink : public Sink { public: FileSink(QString filename) : m_filename(filename) {}; virtual ~FileSink() = default; public: auto init(QNetworkRequest& request) -> Task::State override; auto write(QByteArray& data) -> Task::State override; auto abort() -> Task::State override; auto finalize(QNetworkReply& reply) -> Task::State override; auto hasLocalData() -> bool override; protected: virtual auto initCache(QNetworkRequest&) -> Task::State; virtual auto finalizeCache(QNetworkReply& reply) -> Task::State; protected: QString m_filename; bool m_wroteAnyData = false; std::unique_ptr m_output_file; }; } // namespace Net PrismLauncher-11.0.3/launcher/MMCTime.h0000644000175100017510000000221515224505336017170 0ustar runnerrunner/* * Copyright 2021 Jamie Mansfield * * 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 namespace Time { QString prettifyDuration(int64_t duration, bool noDays = false); /** * @brief Returns a string with short form time duration ie. `2days 1h3m4s56.0ms`. * miliseconds are only included if `precision` is greater than 0. * * @param duration a number of seconds as floating point * @param precision number of decmial points to display on fractons of a second, defualts to 0. * @return QString */ QString humanReadableDuration(double duration, int precision = 0); } // namespace Time PrismLauncher-11.0.3/launcher/Markdown.cpp0000644000175100017510000000206015224505336020050 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Joshua Goins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "Markdown.h" QString markdownToHTML(const QString& markdown) { const QByteArray markdownData = markdown.toUtf8(); char* buffer = cmark_markdown_to_html(markdownData.constData(), markdownData.length(), CMARK_OPT_NOBREAKS | CMARK_OPT_UNSAFE); QString htmlStr(buffer); free(buffer); return htmlStr; } PrismLauncher-11.0.3/launcher/archive/0000755000175100017510000000000015224505336017205 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/archive/ArchiveReader.cpp0000644000175100017510000002237315224505336022424 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only AND LicenseRef-PublicDomain /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Additional note: Portions of this file are released into the public domain * under LicenseRef-PublicDomain. */ #include "ArchiveReader.h" #include #include #include #include #include #include #include #include namespace MMCZip { QStringList ArchiveReader::getFiles() { return m_fileNames; } bool ArchiveReader::collectFiles(bool onlyFiles) { return parse([this, onlyFiles](File* f) { if (!onlyFiles || f->isFile()) { m_fileNames << f->filename(); } return f->skip(); }); } using getPathFunc = std::function; static QString decodeLibArchivePath(archive_entry* entry, const getPathFunc& getUtf8Path, const getPathFunc& getPath) { auto fileName = QString::fromUtf8(getUtf8Path(entry)); if (fileName.isEmpty()) { fileName = QString::fromLocal8Bit(getPath(entry)); } return fileName; } QString ArchiveReader::File::filename() { return decodeLibArchivePath(m_entry, archive_entry_pathname_utf8, archive_entry_pathname); } QByteArray ArchiveReader::File::readAll(int* outStatus) { QByteArray data; const void* buff = nullptr; size_t size = 0; la_int64_t offset = 0; int status = 0; while ((status = archive_read_data_block(m_archive.get(), &buff, &size, &offset)) == ARCHIVE_OK) { data.append(static_cast(buff), static_cast(size)); } if (status != ARCHIVE_EOF && status != ARCHIVE_OK) { qWarning() << "libarchive read error:" << archive_error_string(m_archive.get()); } if (outStatus) { *outStatus = status; } return data; } QDateTime ArchiveReader::File::dateTime() { auto mtime = archive_entry_mtime(m_entry); auto mtime_nsec = archive_entry_mtime_nsec(m_entry); auto dt = QDateTime::fromSecsSinceEpoch(mtime); return dt.addMSecs(mtime_nsec / 1e6); } int ArchiveReader::File::readNextHeader() { return archive_read_next_header(m_archive.get(), &m_entry); } auto ArchiveReader::goToFile(const QString& filename) -> std::unique_ptr { auto f = std::make_unique(); auto* a = f->m_archive.get(); archive_read_support_format_all(a); archive_read_support_filter_all(a); auto fileName = m_archivePath.toStdWString(); if (archive_read_open_filename_w(a, fileName.data(), m_blockSize) != ARCHIVE_OK) { qCritical() << "Failed to open archive file:" << m_archivePath << "-" << archive_error_string(a); return nullptr; } while (f->readNextHeader() == ARCHIVE_OK) { if (f->filename() == filename) { return f; } f->skip(); } archive_read_close(a); return nullptr; } static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = false) { int r = 0; const void* buff = nullptr; size_t size = 0; la_int64_t offset = 0; for (;;) { r = archive_read_data_block(ar, &buff, &size, &offset); if (r == ARCHIVE_EOF) { return ARCHIVE_OK; } if (r < ARCHIVE_OK) { qCritical() << "Failed reading data block:" << archive_error_string(ar); return (r); } if (notBlock) { r = archive_write_data(aw, buff, size); } else { r = archive_write_data_block(aw, buff, size, offset); } if (r < ARCHIVE_OK) { qCritical() << "Failed writing data block:" << archive_error_string(aw); return (r); } } } static bool willEscapeRoot(const QDir& root, archive_entry* entry) { auto entryPath = decodeLibArchivePath(entry, archive_entry_pathname_utf8, archive_entry_pathname); auto linkTarget = decodeLibArchivePath(entry, archive_entry_symlink_utf8, archive_entry_symlink); auto hardLink = decodeLibArchivePath(entry, archive_entry_hardlink_utf8, archive_entry_hardlink); if (entryPath.isEmpty() || (linkTarget.isEmpty() && hardLink.isEmpty())) { return false; } bool isHardLink = false; if (isHardLink = linkTarget.isEmpty(); isHardLink) { linkTarget = hardLink; } QString linkFullPath = root.filePath(entryPath); auto rootDir = QUrl::fromLocalFile(root.absolutePath()); if (!rootDir.isParentOf(QUrl::fromLocalFile(linkFullPath))) { return true; } QDir linkDir = QFileInfo(linkFullPath).dir(); if (!QDir::isAbsolutePath(linkTarget)) { linkTarget = (!isHardLink ? linkDir : root).filePath(linkTarget); } return !rootDir.isParentOf(QUrl::fromLocalFile(QDir::cleanPath(linkTarget))); } bool ArchiveReader::File::writeFile(archive* out, const QString& targetFileName, bool notBlock) { return writeFile(out, targetFileName, {}, notBlock); }; bool ArchiveReader::File::writeFile(archive* out, const QString& targetFileName, std::optional root, bool notBlock) { auto* entry = m_entry; std::unique_ptr entryClone(nullptr, &archive_entry_free); if (!targetFileName.isEmpty()) { entryClone.reset(archive_entry_clone(m_entry)); entry = entryClone.get(); auto nameUtf8 = targetFileName.toUtf8(); archive_entry_set_pathname_utf8(entry, nameUtf8.constData()); } if (root.has_value() && willEscapeRoot(root.value(), entry)) { qCritical() << "Failed to write header to entry:" << filename() << "-" << "file outside root"; return false; } if (archive_write_header(out, entry) < ARCHIVE_OK) { qCritical() << "Failed to write header to entry:" << filename() << "-" << archive_error_string(out) << targetFileName; return false; } if (archive_entry_size(m_entry) > 0) { auto r = copy_data(m_archive.get(), out, notBlock); if (r < ARCHIVE_OK) { qCritical() << "Failed reading data block:" << archive_error_string(out); } if (r < ARCHIVE_WARN) { return false; } } auto r = archive_write_finish_entry(out); if (r < ARCHIVE_OK) { qCritical() << "Failed to finish writing entry:" << archive_error_string(out); } return (r >= ARCHIVE_WARN); } bool ArchiveReader::parse(const std::function& doStuff) { auto f = std::make_unique(); auto* a = f->m_archive.get(); archive_read_support_format_all(a); archive_read_support_filter_all(a); auto fileName = m_archivePath.toStdWString(); if (archive_read_open_filename_w(a, fileName.data(), m_blockSize) != ARCHIVE_OK) { qCritical() << "Failed to open archive file:" << m_archivePath << "-" << f->error(); return false; } bool breakControl = false; while (f->readNextHeader() == ARCHIVE_OK) { if (f && !doStuff(f.get(), breakControl)) { qCritical() << "Failed to parse file:" << f->filename() << "-" << f->error(); return false; } if (breakControl) { break; } } archive_read_close(a); return true; } bool ArchiveReader::parse(const std::function& doStuff) { return parse([doStuff](File* f, bool&) { return doStuff(f); }); } bool ArchiveReader::File::isFile() { return (archive_entry_filetype(m_entry) & AE_IFMT) == AE_IFREG; } bool ArchiveReader::File::skip() { return archive_read_data_skip(m_archive.get()) == ARCHIVE_OK; } const char* ArchiveReader::File::error() { return archive_error_string(m_archive.get()); } QString ArchiveReader::getZipName() { return m_archivePath; } bool ArchiveReader::exists(const QString& filePath) const { if (filePath == QLatin1String("/") || filePath.isEmpty()) { return true; } // Normalize input path (remove trailing slash, if any) QString normalizedPath = QDir::cleanPath(filePath); if (normalizedPath.startsWith('/')) { normalizedPath.remove(0, 1); } if (normalizedPath == QLatin1String(".")) { return true; } if (normalizedPath == QLatin1String("..")) { return false; // root only } // Check for exact file match if (m_fileNames.contains(normalizedPath, Qt::CaseInsensitive)) { return true; } // Check for directory existence by seeing if any file starts with that path QString dirPath = normalizedPath + QLatin1Char('/'); for (const QString& f : m_fileNames) { if (f.startsWith(dirPath, Qt::CaseInsensitive)) { return true; } } return false; } ArchiveReader::File::File() : m_archive(ArchivePtr(archive_read_new(), archive_read_free)) {} } // namespace MMCZip PrismLauncher-11.0.3/launcher/archive/ExportToZipTask.cpp0000644000175100017510000000624715224505336023014 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ExportToZipTask.h" #include #include "FileSystem.h" namespace MMCZip { void ExportToZipTask::executeTask() { setStatus("Adding files..."); setProgress(0, m_files.length()); m_buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); }); connect(&m_buildZipWatcher, &QFutureWatcher::finished, this, &ExportToZipTask::finish); m_buildZipWatcher.setFuture(m_buildZipFuture); } auto ExportToZipTask::exportZip() -> ZipResult { if (!m_dir.exists()) { return ZipResult(tr("Folder doesn't exist")); } if (!m_output.open()) { return ZipResult(tr("Could not create file")); } for (auto fileName : m_extraFiles.keys()) { if (m_buildZipFuture.isCanceled()) return ZipResult(); if (!m_output.addFile(fileName, m_extraFiles[fileName])) { return ZipResult(tr("Could not add:") + fileName); } } for (const QFileInfo& file : m_files) { if (m_buildZipFuture.isCanceled()) return ZipResult(); auto absolute = file.absoluteFilePath(); auto relative = m_dir.relativeFilePath(absolute); setStatus("Compressing: " + relative); setProgress(m_progress + 1, m_progressTotal); if (m_followSymlinks) { if (file.isSymLink()) absolute = file.symLinkTarget(); else absolute = file.canonicalFilePath(); } if (!m_excludeFiles.contains(relative) && !m_output.addFile(absolute, m_destinationPrefix + relative)) { return ZipResult(tr("Could not read and compress %1").arg(relative)); } } if (!m_output.close()) { return ZipResult(tr("A zip error occurred")); } return ZipResult(); } void ExportToZipTask::finish() { if (m_buildZipFuture.isCanceled()) { FS::deletePath(m_outputPath); emitAborted(); } else if (auto result = m_buildZipFuture.result(); result.has_value()) { FS::deletePath(m_outputPath); emitFailed(result.value()); } else { emitSucceeded(); } } bool ExportToZipTask::abort() { if (m_buildZipFuture.isRunning()) { m_buildZipFuture.cancel(); // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur // immediately. return true; } return false; } } // namespace MMCZip PrismLauncher-11.0.3/launcher/archive/ExtractZipTask.cpp0000644000175100017510000001201115224505336022624 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ExtractZipTask.h" #include #include "FileSystem.h" #include "archive/ArchiveReader.h" #include "archive/ArchiveWriter.h" namespace MMCZip { void ExtractZipTask::executeTask() { m_zipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); }); connect(&m_zipWatcher, &QFutureWatcher::finished, this, &ExtractZipTask::finish); m_zipWatcher.setFuture(m_zipFuture); } auto ExtractZipTask::extractZip() -> ZipResult { auto target = m_outputDir.absolutePath(); auto target_top_dir = QUrl::fromLocalFile(target); QStringList extracted; qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input.getZipName() << "to" << target; if (!m_input.collectFiles()) { return ZipResult(tr("Failed to enumerate files in archive")); } if (m_input.getFiles().isEmpty()) { logWarning(tr("Extracting empty archives seems odd...")); return ZipResult(); } auto extPtr = ArchiveWriter::createDiskWriter(); auto ext = extPtr.get(); setStatus("Extracting files..."); setProgress(0, m_input.getFiles().count()); ZipResult result; auto fileName = m_input.getZipName(); if (!m_input.parse([this, &result, &target, &target_top_dir, ext, &extracted](ArchiveReader::File* f) { if (m_zipFuture.isCanceled()) return false; setProgress(m_progress + 1, m_progressTotal); QString file_name = f->filename(); if (!file_name.startsWith(m_subdirectory)) { f->skip(); return true; } auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size())); auto original_name = relative_file_name; setStatus("Unpacking: " + relative_file_name); // Fix subdirs/files ending with a / getting transformed into absolute paths if (relative_file_name.startsWith('/')) relative_file_name = relative_file_name.mid(1); // Fix weird "folders with a single file get squashed" thing QString sub_path; if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { sub_path = relative_file_name.section('/', 0, -2) + '/'; FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); relative_file_name = relative_file_name.split('/').last(); } QString target_file_path; if (relative_file_name.isEmpty()) { target_file_path = target + '/'; } else { target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) target_file_path += '/'; } if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { result = ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2") .arg(relative_file_name, target)); return false; } if (!f->writeFile(ext, target_file_path, target)) { result = ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path)); return false; } extracted.append(target_file_path); qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; return true; })) { FS::removeFiles(extracted); return result.has_value() ? result : ZipResult(tr("Failed to parse file %1").arg(fileName)); } return ZipResult(); } void ExtractZipTask::finish() { if (m_zipFuture.isCanceled()) { emitAborted(); } else if (auto result = m_zipFuture.result(); result.has_value()) { emitFailed(result.value()); } else { emitSucceeded(); } } bool ExtractZipTask::abort() { if (m_zipFuture.isRunning()) { m_zipFuture.cancel(); // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur // immediately. return true; } return false; } } // namespace MMCZip PrismLauncher-11.0.3/launcher/archive/ArchiveWriter.h0000644000175100017510000000260315224505336022135 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "archive/ArchiveReader.h" struct archive; namespace MMCZip { class ArchiveWriter { public: ArchiveWriter(const QString& archiveName); virtual ~ArchiveWriter(); bool open(); bool close(); bool addFile(const QString& fileName, const QString& fileDest); bool addFile(const QString& fileDest, const QByteArray& data); bool addFile(ArchiveReader::File* f); static std::unique_ptr createDiskWriter(); private: struct archive* m_archive = nullptr; QString m_filename; QString m_format = "zip"; }; } // namespace MMCZip PrismLauncher-11.0.3/launcher/archive/ArchiveWriter.cpp0000644000175100017510000002027615224505336022476 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ArchiveWriter.h" #include #include #include #include #include #include #if defined Q_OS_WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif // clang-format off #include #include // clang-format on #endif namespace MMCZip { ArchiveWriter::ArchiveWriter(const QString& archiveName) : m_filename(archiveName) {} ArchiveWriter::~ArchiveWriter() { close(); } bool ArchiveWriter::open() { if (m_filename.isEmpty()) { qCritical() << "Archive m_filename not set."; return false; } m_archive = archive_write_new(); if (!m_archive) { qCritical() << "Archive not initialized."; return false; } auto format = m_format.toUtf8(); archive_write_set_format_by_name(m_archive, format.constData()); if (archive_write_set_options(m_archive, "hdrcharset=UTF-8") != ARCHIVE_OK) { qCritical() << "Failed to open archive file:" << m_filename << "-" << archive_error_string(m_archive); return false; } auto archiveNameW = m_filename.toStdWString(); if (archive_write_open_filename_w(m_archive, archiveNameW.data()) != ARCHIVE_OK) { qCritical() << "Failed to open archive file:" << m_filename << "-" << archive_error_string(m_archive); return false; } return true; } bool ArchiveWriter::close() { bool success = true; if (m_archive) { if (archive_write_close(m_archive) != ARCHIVE_OK) { qCritical() << "Failed to close archive" << m_filename << "-" << archive_error_string(m_archive); success = false; } if (archive_write_free(m_archive) != ARCHIVE_OK) { qCritical() << "Failed to free archive" << m_filename << "-" << archive_error_string(m_archive); success = false; } m_archive = nullptr; } return success; } bool ArchiveWriter::addFile(const QString& fileName, const QString& fileDest) { QFileInfo fileInfo(fileName); if (!fileInfo.exists()) { qCritical() << "File does not exist:" << fileInfo.filePath(); return false; } std::unique_ptr entry_ptr(archive_entry_new(), archive_entry_free); auto entry = entry_ptr.get(); if (!entry) { qCritical() << "Failed to create archive entry"; return false; } auto fileDestUtf8 = fileDest.toUtf8(); archive_entry_set_pathname_utf8(entry, fileDestUtf8.constData()); #if defined Q_OS_WIN32 { // Windows needs to use this method, thanks I hate it. auto widePath = fileInfo.absoluteFilePath().toStdWString(); HANDLE file_handle = CreateFileW(widePath.data(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (file_handle == INVALID_HANDLE_VALUE) { qCritical() << "Failed to stat file:" << fileInfo.filePath(); return false; } BY_HANDLE_FILE_INFORMATION file_info; if (!GetFileInformationByHandle(file_handle, &file_info)) { qCritical() << "Failed to stat file:" << fileInfo.filePath(); CloseHandle(file_handle); return false; } archive_entry_copy_bhfi(entry, &file_info); CloseHandle(file_handle); } #else { // this only works for multibyte encoded filenames if the local is properly set, // a wide character version doesn't seem to exist: here's hoping... QByteArray utf8 = fileInfo.absoluteFilePath().toUtf8(); const char* cpath = utf8.constData(); struct stat st; if (stat(cpath, &st) != 0) { qCritical() << "Failed to stat file:" << fileInfo.filePath(); return false; } // This should handle the copying of most attributes archive_entry_copy_stat(entry, &st); } #endif // However: // "The [filetype] constants used by stat(2) may have different numeric values from the corresponding [libarchive constants]." // - `archive_entry_stat(3)` if (fileInfo.isSymLink()) { archive_entry_set_filetype(entry, AE_IFLNK); // We also need to manually copy some attributes from the link itself, as `stat` above operates on its target auto target = fileInfo.symLinkTarget().toUtf8(); archive_entry_set_symlink_utf8(entry, target.constData()); archive_entry_set_size(entry, 0); archive_entry_set_perm(entry, fileInfo.permissions()); } else if (fileInfo.isFile()) { archive_entry_set_filetype(entry, AE_IFREG); } else { qCritical() << "Unsupported file type:" << fileInfo.filePath(); return false; } if (archive_write_header(m_archive, entry) != ARCHIVE_OK) { qCritical() << "Failed to write header for:" << fileDest << "-" << archive_error_string(m_archive); return false; } if (fileInfo.isFile() && !fileInfo.isSymLink()) { QFile file(fileInfo.absoluteFilePath()); if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file:" << fileInfo.filePath() << "error:" << file.errorString(); return false; } constexpr qint64 chunkSize = 8192; QByteArray buffer; buffer.resize(chunkSize); while (!file.atEnd()) { auto bytesRead = file.read(buffer.data(), chunkSize); if (bytesRead < 0) { qCritical() << "Read error in file:" << fileInfo.filePath(); return false; } if (archive_write_data(m_archive, buffer.constData(), bytesRead) < 0) { qCritical() << "Write error in archive for:" << fileDest; return false; } } } return true; } bool ArchiveWriter::addFile(const QString& fileDest, const QByteArray& data) { std::unique_ptr entry_ptr(archive_entry_new(), archive_entry_free); auto entry = entry_ptr.get(); if (!entry) { qCritical() << "Failed to create archive entry"; return false; } auto fileDestUtf8 = fileDest.toUtf8(); archive_entry_set_pathname_utf8(entry, fileDestUtf8.constData()); archive_entry_set_perm(entry, 0644); archive_entry_set_filetype(entry, AE_IFREG); archive_entry_set_size(entry, data.size()); if (archive_write_header(m_archive, entry) != ARCHIVE_OK) { qCritical() << "Failed to write header for:" << fileDest << "-" << archive_error_string(m_archive); return false; } if (archive_write_data(m_archive, data.constData(), data.size()) < 0) { qCritical() << "Write error in archive for:" << fileDest << "-" << archive_error_string(m_archive); return false; } return true; } bool ArchiveWriter::addFile(ArchiveReader::File* f) { return f->writeFile(m_archive, "", true); } std::unique_ptr ArchiveWriter::createDiskWriter() { int flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS | ARCHIVE_EXTRACT_SECURE_NODOTDOT | ARCHIVE_EXTRACT_SECURE_SYMLINKS; std::unique_ptr extPtr(archive_write_disk_new(), [](archive* a) { if (a) { archive_write_close(a); archive_write_free(a); } }); archive* ext = extPtr.get(); archive_write_disk_set_options(ext, flags); archive_write_disk_set_standard_lookup(ext); return extPtr; } } // namespace MMCZip PrismLauncher-11.0.3/launcher/archive/ExtractZipTask.h0000644000175100017510000000300415224505336022273 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include "archive/ArchiveReader.h" #include "tasks/Task.h" namespace MMCZip { class ExtractZipTask : public Task { Q_OBJECT public: ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "") : m_input(input), m_outputDir(outputDir), m_subdirectory(subdirectory) {} virtual ~ExtractZipTask() = default; using ZipResult = std::optional; protected: virtual void executeTask() override; bool abort() override; ZipResult extractZip(); void finish(); private: ArchiveReader m_input; QDir m_outputDir; QString m_subdirectory; QFuture m_zipFuture; QFutureWatcher m_zipWatcher; }; } // namespace MMCZip PrismLauncher-11.0.3/launcher/archive/ArchiveReader.h0000644000175100017510000000436515224505336022072 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include #include #include struct archive; struct archive_entry; namespace MMCZip { class ArchiveReader { public: using ArchivePtr = std::unique_ptr; explicit ArchiveReader(QString fileName) : m_archivePath(std::move(fileName)) {} virtual ~ArchiveReader() = default; QStringList getFiles(); QString getZipName(); bool collectFiles(bool onlyFiles = true); bool exists(const QString& filePath) const; class File { public: File(); virtual ~File() = default; QString filename(); bool isFile(); QDateTime dateTime(); const char* error(); QByteArray readAll(int* outStatus = nullptr); bool skip(); bool writeFile(archive* out, const QString& targetFileName = "", bool notBlock = false); bool writeFile(archive* out, const QString& targetFileName, std::optional root, bool notBlock = false); private: int readNextHeader(); private: friend ArchiveReader; ArchivePtr m_archive; archive_entry* m_entry; }; std::unique_ptr goToFile(const QString& filename); bool parse(const std::function&); bool parse(const std::function&); private: QString m_archivePath; size_t m_blockSize = 10240; QStringList m_fileNames; }; } // namespace MMCZip PrismLauncher-11.0.3/launcher/archive/ExportToZipTask.h0000644000175100017510000000447315224505336022460 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include "archive/ArchiveWriter.h" #include "tasks/Task.h" namespace MMCZip { class ExportToZipTask : public Task { Q_OBJECT public: ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) : m_outputPath(outputPath) , m_output(outputPath) , m_dir(dir) , m_files(files) , m_destinationPrefix(destinationPrefix) , m_followSymlinks(followSymlinks) { setAbortable(true); }; ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks) {}; virtual ~ExportToZipTask() = default; void setExcludeFiles(QStringList excludeFiles) { m_excludeFiles = excludeFiles; } void addExtraFile(QString fileName, QByteArray data) { m_extraFiles.insert(fileName, data); } using ZipResult = std::optional; protected: virtual void executeTask() override; bool abort() override; ZipResult exportZip(); void finish(); private: QString m_outputPath; ArchiveWriter m_output; QDir m_dir; QFileInfoList m_files; QString m_destinationPrefix; bool m_followSymlinks; QStringList m_excludeFiles; QHash m_extraFiles; QFuture m_buildZipFuture; QFutureWatcher m_buildZipWatcher; }; } // namespace MMCZip PrismLauncher-11.0.3/launcher/Version.h0000644000175100017510000000375415224505336017373 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2026 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include // this implements the FlexVer // https://git.sleeping.town/exa/FlexVer class Version { public: Version(QString str) : m_string(std::move(str)) { parse(); } // NOLINT(hicpp-explicit-conversions) Version() = default; private: struct Section { enum class Type : std::uint8_t { Null, Textual, Numeric, PreRelease }; explicit Section(Type t = Type::Null, QString value = "") : t(t), value(std::move(value)) {} Type t; QString value; bool operator==(const Section& other) const = default; std::strong_ordering operator<=>(const Section& other) const; }; private: void parse(); public: QString toString() const { return m_string; } bool isEmpty() const { return m_string.isEmpty(); } friend QDebug operator<<(QDebug debug, const Version& v); bool operator==(const Version& other) const { return (*this <=> other) == std::strong_ordering::equal; } std::strong_ordering operator<=>(const Version& other) const; private: QString m_string; QList
    m_sections; };PrismLauncher-11.0.3/launcher/InstanceCopyPrefs.cpp0000644000175100017510000000740715224505336021677 0ustar runnerrunner// // Created by marcelohdez on 10/22/22. // #include "InstanceCopyPrefs.h" bool InstanceCopyPrefs::allTrue() const { return copySaves && keepPlaytime && copyGameOptions && copyResourcePacks && copyShaderPacks && copyServers && copyMods && copyScreenshots; } // Returns a single RegEx string of the selected folders/files to filter out (ex: ".minecraft/saves|.minecraft/server.dat") QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const { return getSelectedFiltersAsRegex({}); } QString InstanceCopyPrefs::getSelectedFiltersAsRegex(const QStringList& additionalFilters) const { QStringList filters; if (!copySaves) filters << "saves"; if (!copyGameOptions) filters << "options.txt"; if (!copyResourcePacks) filters << "resourcepacks" << "texturepacks"; if (!copyShaderPacks) filters << "shaderpacks"; if (!copyServers) filters << "servers.dat" << "servers.dat_old" << "server-resource-packs"; if (!copyMods) filters << "coremods" << "mods" << "config"; if (!copyScreenshots) filters << "screenshots"; for (auto filter : additionalFilters) { filters << filter; } // If we have any filters to add, join them as a single regex string to return: if (!filters.isEmpty()) { const QString MC_ROOT = "[.]?minecraft/"; // Ensure first filter starts with root, then join other filters with OR regex before root (ex: ".minecraft/saves|.minecraft/mods"): return MC_ROOT + filters.join("|" + MC_ROOT); } return {}; } // ======= Getters ======= bool InstanceCopyPrefs::isCopySavesEnabled() const { return copySaves; } bool InstanceCopyPrefs::isKeepPlaytimeEnabled() const { return keepPlaytime; } bool InstanceCopyPrefs::isCopyGameOptionsEnabled() const { return copyGameOptions; } bool InstanceCopyPrefs::isCopyResourcePacksEnabled() const { return copyResourcePacks; } bool InstanceCopyPrefs::isCopyShaderPacksEnabled() const { return copyShaderPacks; } bool InstanceCopyPrefs::isCopyServersEnabled() const { return copyServers; } bool InstanceCopyPrefs::isCopyModsEnabled() const { return copyMods; } bool InstanceCopyPrefs::isCopyScreenshotsEnabled() const { return copyScreenshots; } bool InstanceCopyPrefs::isUseSymLinksEnabled() const { return useSymLinks; } bool InstanceCopyPrefs::isUseHardLinksEnabled() const { return useHardLinks; } bool InstanceCopyPrefs::isLinkRecursivelyEnabled() const { return linkRecursively; } bool InstanceCopyPrefs::isDontLinkSavesEnabled() const { return dontLinkSaves; } bool InstanceCopyPrefs::isUseCloneEnabled() const { return useClone; } // ======= Setters ======= void InstanceCopyPrefs::enableCopySaves(bool b) { copySaves = b; } void InstanceCopyPrefs::enableKeepPlaytime(bool b) { keepPlaytime = b; } void InstanceCopyPrefs::enableCopyGameOptions(bool b) { copyGameOptions = b; } void InstanceCopyPrefs::enableCopyResourcePacks(bool b) { copyResourcePacks = b; } void InstanceCopyPrefs::enableCopyShaderPacks(bool b) { copyShaderPacks = b; } void InstanceCopyPrefs::enableCopyServers(bool b) { copyServers = b; } void InstanceCopyPrefs::enableCopyMods(bool b) { copyMods = b; } void InstanceCopyPrefs::enableCopyScreenshots(bool b) { copyScreenshots = b; } void InstanceCopyPrefs::enableUseSymLinks(bool b) { useSymLinks = b; } void InstanceCopyPrefs::enableLinkRecursively(bool b) { linkRecursively = b; } void InstanceCopyPrefs::enableUseHardLinks(bool b) { useHardLinks = b; } void InstanceCopyPrefs::enableDontLinkSaves(bool b) { dontLinkSaves = b; } void InstanceCopyPrefs::enableUseClone(bool b) { useClone = b; } PrismLauncher-11.0.3/launcher/AssertHelpers.h0000644000175100017510000000160315224505336020521 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #if defined(ASSERT_NEVER) #error ASSERT_NEVER already defined #else #define ASSERT_NEVER(cond) (Q_ASSERT((cond) == false), (cond)) #endif PrismLauncher-11.0.3/launcher/HardwareInfo.h0000644000175100017510000000227515224505336020314 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2026 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include namespace HardwareInfo { QString cpuInfo(); uint64_t totalRamMiB(); uint64_t availableRamMiB(); QStringList gpuInfo(); } // namespace HardwareInfo #ifdef Q_OS_MACOS namespace MacOSHardwareInfo { enum class MemoryPressureLevel : uint8_t { Normal = 1, Warning = 2, Critical = 4, }; MemoryPressureLevel memoryPressureLevel(); QString memoryPressureLevelName(); } // namespace MacOSHardwareInfo #endifPrismLauncher-11.0.3/launcher/InstanceCreationTask.h0000644000175100017510000000304715224505336022015 0ustar runnerrunner#pragma once #include "BaseVersion.h" #include "InstanceTask.h" #include "minecraft/MinecraftInstance.h" class InstanceCreationTask : public InstanceTask { Q_OBJECT public: InstanceCreationTask() = default; virtual ~InstanceCreationTask() = default; bool abort() override; protected: void executeTask() final override; /** * Tries to update an already existing instance. * * This can be implemented by subclasses to provide a way of updating an already existing * instance, according to that implementation's concept of 'identity' (i.e. instances that * are updates / downgrades of one another). * * If this returns true, createInstance() will not run, so you should do all update steps in here. * Otherwise, createInstance() is run as normal. */ virtual bool updateInstance() { return false; }; /** * Creates a new instance. * * Returns the instance if it was created or nullptr otherwise. */ virtual std::unique_ptr createInstance() { return nullptr; } QString getError() const { return m_error_message; } protected: void setError(const QString& message) { m_error_message = message; }; void scheduleToDelete(QWidget* parent, QDir dir, QString path, bool checkDisabled = false); protected: bool m_abort = false; QStringList m_filesToRemove; ShouldDeleteSaves m_shouldDeleteSaves; private: QString m_error_message; std::unique_ptr m_instance; Task::Ptr m_gameFilesTask; }; PrismLauncher-11.0.3/launcher/macsandbox/0000755000175100017510000000000015224505336017703 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/macsandbox/SecurityBookmarkFileAccess.h0000644000175100017510000001062115224505336025273 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef FILEACCESS_H #define FILEACCESS_H #include #include Q_FORWARD_DECLARE_OBJC_CLASS(NSData); Q_FORWARD_DECLARE_OBJC_CLASS(NSURL); Q_FORWARD_DECLARE_OBJC_CLASS(NSString); Q_FORWARD_DECLARE_OBJC_CLASS(NSAutoreleasePool); Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableDictionary); Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableSet); class QString; class QByteArray; class QUrl; class SecurityBookmarkFileAccess { /// The keys are bookmarks and the values are URLs. NSMutableDictionary* m_bookmarks; /// The keys are paths and the values are bookmarks. NSMutableDictionary* m_paths; /// Contains URLs that are currently being accessed. NSMutableSet* m_activeURLs; bool m_readOnly; NSURL* securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale); public: /// \param readOnly A boolean indicating whether the bookmark should be read-only. SecurityBookmarkFileAccess(bool readOnly = false); ~SecurityBookmarkFileAccess(); /// Get a security scoped bookmark from a URL. /// /// The URL must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling /// this function. Note that this is called implicitly if the user selects the directory from a file picker. /// \param url The URL to get the security scoped bookmark from. /// \return A QByteArray containing the security scoped bookmark. QByteArray urlToSecurityScopedBookmark(const QUrl& url); /// Get a security scoped bookmark from a path. /// /// The path must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling /// this function. Note that this is called implicitly if the user selects the directory from a file picker. /// \param path The path to get the security scoped bookmark from. /// \return A QByteArray containing the security scoped bookmark. QByteArray pathToSecurityScopedBookmark(const QString& path); /// Get a QUrl from a security scoped bookmark. If the bookmark is stale, isStale will be set to true and the bookmark will be updated. /// /// You must check whether the URL is valid before using it. /// \param bookmark The security scoped bookmark to get the URL from. /// \param isStale A boolean that will be set to true if the bookmark is stale. /// \return The URL from the security scoped bookmark. QUrl securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale); /// Makes the file or directory at the path pointed to by the bookmark accessible. Unlike `startAccessingSecurityScopedResource()`, this /// class ensures that only one "access" is active at a time. Calling this function again after the security-scoped resource has /// already been used will do nothing, and a single call to `stopUsingSecurityScopedBookmark()` will release the resource provided that /// this is the only `SecurityBookmarkFileAccess` accessing the resource. /// /// If the bookmark is stale, `isStale` will be set to true and the bookmark will be updated. Stored copies of the bookmark need to be /// updated. /// \param bookmark The security scoped bookmark to start accessing. /// \param isStale A boolean that will be set to true if the bookmark is stale. /// \return A boolean indicating whether the bookmark was successfully accessed. bool startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale); void stopUsingSecurityScopedBookmark(QByteArray& bookmark); /// Returns true if access to the `path` is currently being maintained by this object. bool isAccessingPath(const QString& path); }; #endif // FILEACCESS_H PrismLauncher-11.0.3/launcher/macsandbox/SecurityBookmarkFileAccess.mm0000644000175100017510000001433515224505336025463 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "SecurityBookmarkFileAccess.h" #include #include #include QByteArray SecurityBookmarkFileAccess::urlToSecurityScopedBookmark(const QUrl& url) { if (!url.isLocalFile()) return {}; NSError* error = nil; NSURL* nsurl = [url.toNSURL() absoluteURL]; NSData* bookmark; if ([m_paths objectForKey:[nsurl path]]) { bookmark = m_paths[[nsurl path]]; } else { bookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope | (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; } if (error) { return {}; } // remove/reapply access to ensure that write access is immediately cut off for read-only bookmarks // sometimes you need to call this twice to actually stop access (extra calls aren't harmful) [nsurl stopAccessingSecurityScopedResource]; [nsurl stopAccessingSecurityScopedResource]; nsurl = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope | (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) relativeToURL:nil bookmarkDataIsStale:nil error:&error]; m_paths[[nsurl path]] = bookmark; m_bookmarks[bookmark] = nsurl; QByteArray qBookmark = QByteArray::fromNSData(bookmark); bool isStale = false; startUsingSecurityScopedBookmark(qBookmark, isStale); return qBookmark; } SecurityBookmarkFileAccess::SecurityBookmarkFileAccess(bool readOnly) : m_readOnly(readOnly) { m_bookmarks = [NSMutableDictionary new]; m_paths = [NSMutableDictionary new]; m_activeURLs = [NSMutableSet new]; } SecurityBookmarkFileAccess::~SecurityBookmarkFileAccess() { for (NSURL* url : m_activeURLs) { [url stopAccessingSecurityScopedResource]; } } QByteArray SecurityBookmarkFileAccess::pathToSecurityScopedBookmark(const QString& path) { return urlToSecurityScopedBookmark(QUrl::fromLocalFile(path)); } NSURL* SecurityBookmarkFileAccess::securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale) { NSError* error = nil; BOOL localStale = NO; NSURL* nsurl = [NSURL URLByResolvingBookmarkData:bookmark.toNSData() options:NSURLBookmarkResolutionWithSecurityScope | (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) relativeToURL:nil bookmarkDataIsStale:&localStale error:&error]; if (error) { return nil; } isStale = localStale; if (isStale) { NSData* nsBookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope | (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; if (error) { return nil; } bookmark = QByteArray::fromNSData(nsBookmark); } NSData* nsBookmark = bookmark.toNSData(); m_paths[[nsurl path]] = nsBookmark; m_bookmarks[nsBookmark] = nsurl; return nsurl; } QUrl SecurityBookmarkFileAccess::securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale) { if (bookmark.isEmpty()) return {}; NSURL* url = securityScopedBookmarkToNSURL(bookmark, isStale); if (!url) return {}; return QUrl::fromNSURL(url); } bool SecurityBookmarkFileAccess::startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale) { NSURL* url = [m_bookmarks objectForKey:bookmark.toNSData()] ? m_bookmarks[bookmark.toNSData()] : securityScopedBookmarkToNSURL(bookmark, isStale); if ([m_activeURLs containsObject:url]) return false; [url stopAccessingSecurityScopedResource]; if ([url startAccessingSecurityScopedResource]) { [m_activeURLs addObject:url]; return true; } return false; } void SecurityBookmarkFileAccess::stopUsingSecurityScopedBookmark(QByteArray& bookmark) { if (![m_bookmarks objectForKey:bookmark.toNSData()]) return; NSURL* url = m_bookmarks[bookmark.toNSData()]; if ([m_activeURLs containsObject:url]) { [url stopAccessingSecurityScopedResource]; [url stopAccessingSecurityScopedResource]; [m_activeURLs removeObject:url]; [m_paths removeObjectForKey:[url path]]; [m_bookmarks removeObjectForKey:bookmark.toNSData()]; } } bool SecurityBookmarkFileAccess::isAccessingPath(const QString& path) { NSData* bookmark = [m_paths objectForKey:path.toNSString()]; if (!bookmark && path.endsWith('/')) { bookmark = [m_paths objectForKey:path.left(path.length() - 1).toNSString()]; } if (!bookmark) { return false; } NSURL* url = [m_bookmarks objectForKey:bookmark]; return [m_activeURLs containsObject:url]; } PrismLauncher-11.0.3/launcher/LoggedProcess.cpp0000644000175100017510000001354615224505336021041 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022,2023 Sefa Eyeoglu * Copyright (c) 2023 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "LoggedProcess.h" #include #include #include "MessageLevel.h" LoggedProcess::LoggedProcess(const QStringConverter::Encoding output_codec, QObject* parent) : QProcess(parent), m_err_decoder(output_codec), m_out_decoder(output_codec) { // QProcess has a strange interface... let's map a lot of those into a few. connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut); connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr); connect(this, &QProcess::finished, this, &LoggedProcess::on_exit); connect(this, &QProcess::errorOccurred, this, &LoggedProcess::on_error); connect(this, &QProcess::stateChanged, this, &LoggedProcess::on_stateChange); } LoggedProcess::~LoggedProcess() { if (m_is_detachable) { setProcessState(QProcess::NotRunning); } } QStringList LoggedProcess::reprocess(const QByteArray& data, QStringDecoder& decoder) { QString str = decoder(data); if (!m_leftover_line.isEmpty()) { str.prepend(m_leftover_line); m_leftover_line = ""; } auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed); m_leftover_line = lines.takeLast(); return lines; } void LoggedProcess::on_stdErr() { auto lines = reprocess(readAllStandardError(), m_err_decoder); emit log(lines, MessageLevel::StdErr); } void LoggedProcess::on_stdOut() { auto lines = reprocess(readAllStandardOutput(), m_out_decoder); emit log(lines, MessageLevel::StdOut); } void LoggedProcess::on_exit(int exit_code, QProcess::ExitStatus status) { // save the exit code m_exit_code = exit_code; // based on state, send signals if (!m_is_aborting) { if (status == QProcess::NormalExit) { //: Message displayed on instance exit emit log({ tr("Process exited with code %1.").arg(exit_code) }, MessageLevel::Launcher); changeState(LoggedProcess::Finished); } else { //: Message displayed on instance crashed if (exit_code == -1) emit log({ tr("Process crashed.") }, MessageLevel::Launcher); else emit log({ tr("Process crashed with exitcode %1.").arg(exit_code) }, MessageLevel::Launcher); changeState(LoggedProcess::Crashed); } } else { //: Message displayed after the instance exits due to kill request emit log({ tr("Process was killed by user.") }, MessageLevel::Error); changeState(LoggedProcess::Aborted); } } void LoggedProcess::on_error(QProcess::ProcessError error) { switch (error) { case QProcess::FailedToStart: { emit log({ tr("The process failed to start: %1").arg(errorString()) }, MessageLevel::Fatal); changeState(LoggedProcess::FailedToStart); break; } // we'll just ignore those... never needed them case QProcess::Crashed: case QProcess::ReadError: case QProcess::Timedout: case QProcess::UnknownError: case QProcess::WriteError: break; } } void LoggedProcess::kill() { m_is_aborting = true; QProcess::kill(); } int LoggedProcess::exitCode() const { return m_exit_code; } void LoggedProcess::changeState(LoggedProcess::State state) { if (state == m_state) return; m_state = state; emit stateChanged(m_state); } LoggedProcess::State LoggedProcess::state() const { return m_state; } void LoggedProcess::on_stateChange(QProcess::ProcessState state) { switch (state) { case QProcess::NotRunning: break; // let's not - there are too many that handle this already. case QProcess::Starting: { if (m_state != LoggedProcess::NotRunning) { qWarning() << "Wrong state change for process from state" << m_state << "to" << (int)LoggedProcess::Starting; } changeState(LoggedProcess::Starting); return; } case QProcess::Running: { if (m_state != LoggedProcess::Starting) { qWarning() << "Wrong state change for process from state" << m_state << "to" << (int)LoggedProcess::Running; } changeState(LoggedProcess::Running); return; } } } void LoggedProcess::setDetachable(bool detachable) { m_is_detachable = detachable; } PrismLauncher-11.0.3/launcher/KonamiCode.cpp0000644000175100017510000000147315224505336020306 0ustar runnerrunner#include "KonamiCode.h" #include #include namespace { const std::array konamiCode = { { Qt::Key_Up, Qt::Key_Up, Qt::Key_Down, Qt::Key_Down, Qt::Key_Left, Qt::Key_Right, Qt::Key_Left, Qt::Key_Right, Qt::Key_B, Qt::Key_A } }; } KonamiCode::KonamiCode(QObject* parent) : QObject(parent) {} void KonamiCode::input(QEvent* event) { if (event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); auto key = Qt::Key(keyEvent->key()); if (key == konamiCode[m_progress]) { m_progress++; } else { m_progress = 0; } if (m_progress == static_cast(konamiCode.size())) { m_progress = 0; emit triggered(); } } } PrismLauncher-11.0.3/launcher/LoggedProcess.h0000644000175100017510000000561715224505336020506 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022,2023 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include "MessageLevel.h" /* * This is a basic process. * It has line-based logging support and hides some of the nasty bits. */ class LoggedProcess : public QProcess { Q_OBJECT public: enum State { NotRunning, Starting, FailedToStart, Running, Finished, Crashed, Aborted }; public: explicit LoggedProcess(QStringConverter::Encoding outputEncoding = QStringConverter::System, QObject* parent = nullptr); virtual ~LoggedProcess(); State state() const; int exitCode() const; void setDetachable(bool detachable); signals: void log(QStringList lines, MessageLevel level); void stateChanged(LoggedProcess::State state); public slots: /** * @brief kill the process - equivalent to kill -9 */ void kill(); private slots: void on_stdErr(); void on_stdOut(); void on_exit(int exit_code, QProcess::ExitStatus status); void on_error(QProcess::ProcessError error); void on_stateChange(QProcess::ProcessState); private: void changeState(LoggedProcess::State state); QStringList reprocess(const QByteArray& data, QStringDecoder& decoder); private: QStringDecoder m_err_decoder; QStringDecoder m_out_decoder; QString m_leftover_line; bool m_killed = false; State m_state = NotRunning; int m_exit_code = 0; bool m_is_aborting = false; bool m_is_detachable = false; }; PrismLauncher-11.0.3/launcher/settings/0000755000175100017510000000000015224505336017424 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/settings/SettingsObject.h0000644000175100017510000002125615224505336022532 0ustar runnerrunner/* 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 #include #include #include #include #include #include #ifdef Q_OS_MACOS #include "macsandbox/SecurityBookmarkFileAccess.h" #endif class Setting; class SettingsObject; /*! * \brief The SettingsObject handles communicating settings between the application and a *settings file. * The class keeps a list of Setting objects. Each Setting object represents one * of the application's settings. These Setting objects are registered with * a SettingsObject and can be managed similarly to the way a list works. * * \author Andrew Okin * \date 2/22/2013 * * \sa Setting */ class SettingsObject : public QObject { Q_OBJECT public: class Lock { public: Lock(SettingsObject* locked) : m_locked(locked) { m_locked->suspendSave(); } ~Lock() { m_locked->resumeSave(); } private: SettingsObject* m_locked; }; public: explicit SettingsObject(QObject* parent = 0); virtual ~SettingsObject(); /*! * Registers an override setting for the given original setting in this settings object * gate decides if the passthrough (true) or the original (false) is used for value * * This will fail if there is already a setting with the same ID as * the one that is being registered. * \return A valid Setting shared pointer if successful. */ std::shared_ptr registerOverride(std::shared_ptr original, std::shared_ptr gate); /*! * Registers a passthorugh setting for the given original setting in this settings object * gate decides if the passthrough (true) or the original (false) is used for value * * This will fail if there is already a setting with the same ID as * the one that is being registered. * \return A valid Setting shared pointer if successful. */ std::shared_ptr registerPassthrough(std::shared_ptr original, std::shared_ptr gate); /*! * Registers the given setting with this SettingsObject and connects the necessary signals. * * This will fail if there is already a setting with the same ID as * the one that is being registered. * \return A valid Setting shared pointer if successful. */ std::shared_ptr registerSetting(QStringList synonyms, QVariant defVal = QVariant()); /*! * Registers the given setting with this SettingsObject and connects the necessary signals. * * This will fail if there is already a setting with the same ID as * the one that is being registered. * \return A valid Setting shared pointer if successful. */ std::shared_ptr registerSetting(QString id, QVariant defVal = QVariant()) { return registerSetting(QStringList(id), defVal); } /*! * \brief Gets the setting with the given ID. * \param id The ID of the setting to get. * \return A pointer to the setting with the given ID. * Returns null if there is no setting with the given ID. * \sa operator []() */ std::shared_ptr getSetting(const QString& id) const; /*! * \brief Gets the setting with the given ID. * \brief if is not registered yet it does that * \param id The ID of the setting to get. * \return A pointer to the setting with the given ID. * Returns null if there is no setting with the given ID. * \sa operator []() */ std::shared_ptr getOrRegisterSetting(const QString& id, QVariant defVal = QVariant()); /*! * \brief Gets the value of the setting with the given ID. * \param id The ID of the setting to get. * \return The setting's value as a QVariant. * If no setting with the given ID exists, returns an invalid QVariant. */ QVariant get(const QString& id); #ifdef Q_OS_MACOS /*! * \brief Get the path to the file or directory represented by the bookmark stored in the associated setting. * \param id The setting ID of the relevant directory - this should not include "Bookmark" at the end. * \return A path to the file or directory represented by the bookmark. * If a bookmark is not valid or stored, use default logic (directly return the stored path). * This can attempt to create a bookmark if the path is accessible and the bookmark is not valid. */ QString getPathFromBookmark(const QString& id); /*! * \brief Set a security-scoped bookmark to the provided path for the associated setting. * \param id The setting ID of the relevant directory - this should not include "Bookmark" at the end. * \param path The new desired path. * \return A boolean indicating whether a bookmark was successfully set. * The path needs to be accessible to the launcher before calling this function. For example, * it could come from a user selection in an open panel. */ bool setPathWithBookmark(const QString& id, const QString& path); #endif /*! * \brief Sets the value of the setting with the given ID. * If no setting with the given ID exists, returns false * \param id The ID of the setting to change. * \param value The new value of the setting. * \return True if successful, false if it failed. */ bool set(const QString& id, QVariant value); /*! * \brief Reverts the setting with the given ID to default. * \param id The ID of the setting to reset. */ void reset(const QString& id) const; /*! * \brief Checks if this SettingsObject contains a setting with the given ID. * \param id The ID to check for. * \return True if the SettingsObject has a setting with the given ID. */ bool contains(const QString& id); /*! * \brief Reloads the settings and emit signals for changed settings * \return True if reloading was successful */ virtual bool reload(); virtual void suspendSave() = 0; virtual void resumeSave() = 0; signals: /*! * \brief Signal emitted when one of this SettingsObject object's settings changes. * This is usually just connected directly to each Setting object's * SettingChanged() signals. * \param setting A reference to the Setting object that changed. * \param value The Setting object's new value. */ void SettingChanged(const Setting& setting, QVariant value); /*! * \brief Signal emitted when one of this SettingsObject object's settings resets. * This is usually just connected directly to each Setting object's * settingReset() signals. * \param setting A reference to the Setting object that changed. */ void settingReset(const Setting& setting); protected slots: /*! * \brief Changes a setting. * This slot is usually connected to each Setting object's * SettingChanged() signal. The signal is emitted, causing this slot * to update the setting's value in the config file. * \param setting A reference to the Setting object that changed. * \param value The setting's new value. */ virtual void changeSetting(const Setting& setting, QVariant value) = 0; /*! * \brief Resets a setting. * This slot is usually connected to each Setting object's * settingReset() signal. The signal is emitted, causing this slot * to update the setting's value in the config file. * \param setting A reference to the Setting object that changed. */ virtual void resetSetting(const Setting& setting) = 0; protected: /*! * \brief Connects the necessary signals to the given Setting. * \param setting The setting to connect. */ void connectSignals(const Setting& setting); /*! * \brief Function used by Setting objects to get their values from the SettingsObject. * \param setting The * \return */ virtual QVariant retrieveValue(const Setting& setting) = 0; friend class Setting; private: QMap> m_settings; #ifdef Q_OS_MACOS SecurityBookmarkFileAccess m_sandboxedFileAccess; #endif protected: bool m_suspendSave = false; bool m_doSave = false; }; PrismLauncher-11.0.3/launcher/settings/Setting.h0000644000175100017510000000676515224505336021230 0ustar runnerrunner/* 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 #include #include #include class SettingsObject; /*! * */ class Setting : public QObject { Q_OBJECT public: /** * Construct a Setting * * Synonyms are all the possible names used in the settings object, in order of preference. * First synonym is the ID, which identifies the setting in Prism Launcher. * * defVal is the default value that will be returned when the settings object * doesn't have any value for this setting. */ explicit Setting(QStringList synonyms, QVariant defVal = QVariant()); /*! * \brief Gets this setting's ID. * This is used to refer to the setting within the application. * \warning Changing the ID while the setting is registered with a SettingsObject results in * undefined behavior. * \return The ID of the setting. */ virtual QString id() const { return m_synonyms.first(); } /*! * \brief Gets this setting's config file key. * This is used to store the setting's value in the config file. It is usually * the same as the setting's ID, but it can be different. * \return The setting's config file key. */ virtual QStringList configKeys() const { return m_synonyms; } /*! * \brief Gets this setting's value as a QVariant. * This is done by calling the SettingsObject's retrieveValue() function. * If this Setting doesn't have a SettingsObject, this returns an invalid QVariant. * \return QVariant containing this setting's value. * \sa value() */ virtual QVariant get() const; /*! * \brief Gets this setting's default value. * \return The default value of this setting. */ virtual QVariant defValue() const; signals: /*! * \brief Signal emitted when this Setting object's value changes. * \param setting A reference to the Setting that changed. * \param value This Setting object's new value. */ void SettingChanged(const Setting& setting, QVariant value); /*! * \brief Signal emitted when this Setting object's value resets to default. * \param setting A reference to the Setting that changed. */ void settingReset(const Setting& setting); public slots: /*! * \brief Changes the setting's value. * This is done by emitting the SettingChanged() signal which will then be * handled by the SettingsObject object and cause the setting to change. * \param value The new value. */ virtual void set(QVariant value); /*! * \brief Reset the setting to default * This is done by emitting the settingReset() signal which will then be * handled by the SettingsObject object and cause the setting to change. */ virtual void reset(); protected: friend class SettingsObject; SettingsObject* m_storage; QStringList m_synonyms; QVariant m_defVal; }; PrismLauncher-11.0.3/launcher/settings/OverrideSetting.cpp0000644000175100017510000000240115224505336023242 0ustar runnerrunner/* 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 "OverrideSetting.h" OverrideSetting::OverrideSetting(std::shared_ptr other, std::shared_ptr gate) : Setting(other->configKeys(), QVariant()) { Q_ASSERT(other); Q_ASSERT(gate); m_other = other; m_gate = gate; } bool OverrideSetting::isOverriding() const { return m_gate->get().toBool(); } QVariant OverrideSetting::defValue() const { return m_other->get(); } QVariant OverrideSetting::get() const { if (isOverriding()) { return Setting::get(); } return m_other->get(); } void OverrideSetting::reset() { Setting::reset(); } void OverrideSetting::set(QVariant value) { Setting::set(value); } PrismLauncher-11.0.3/launcher/settings/PassthroughSetting.cpp0000644000175100017510000000276315224505336024005 0ustar runnerrunner/* 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 "PassthroughSetting.h" PassthroughSetting::PassthroughSetting(std::shared_ptr other, std::shared_ptr gate) : Setting(other->configKeys(), QVariant()) { Q_ASSERT(other); m_other = other; m_gate = gate; } bool PassthroughSetting::isOverriding() const { if (!m_gate) { return false; } return m_gate->get().toBool(); } QVariant PassthroughSetting::defValue() const { if (isOverriding()) { return m_other->get(); } return m_other->defValue(); } QVariant PassthroughSetting::get() const { if (isOverriding()) { return Setting::get(); } return m_other->get(); } void PassthroughSetting::reset() { if (isOverriding()) { Setting::reset(); } m_other->reset(); } void PassthroughSetting::set(QVariant value) { if (isOverriding()) { Setting::set(value); } m_other->set(value); } PrismLauncher-11.0.3/launcher/settings/OverrideSetting.h0000644000175100017510000000251615224505336022716 0ustar runnerrunner/* 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 #include #include "Setting.h" /*! * \brief A setting that 'overrides another.' * This means that the setting's default value will be the value of another setting. * The other setting can be (and usually is) a part of a different SettingsObject * than this one. */ class OverrideSetting : public Setting { Q_OBJECT public: explicit OverrideSetting(std::shared_ptr overridden, std::shared_ptr gate); virtual QVariant defValue() const; virtual QVariant get() const; virtual void set(QVariant value); virtual void reset(); private: bool isOverriding() const; protected: std::shared_ptr m_other; std::shared_ptr m_gate; }; PrismLauncher-11.0.3/launcher/settings/Setting.cpp0000644000175100017510000000233515224505336021550 0ustar runnerrunner/* 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 "Setting.h" #include "settings/SettingsObject.h" Setting::Setting(QStringList synonyms, QVariant defVal) : QObject(), m_synonyms(synonyms), m_defVal(defVal) {} QVariant Setting::get() const { SettingsObject* sbase = m_storage; if (!sbase) { return defValue(); } else { QVariant test = sbase->retrieveValue(*this); if (!test.isValid()) return defValue(); return test; } } QVariant Setting::defValue() const { return m_defVal; } void Setting::set(QVariant value) { emit SettingChanged(*this, value); } void Setting::reset() { emit settingReset(*this); } PrismLauncher-11.0.3/launcher/settings/INIFile.h0000644000175100017510000000373715224505336021026 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include // Sectionless INI parser (for instance config files) class INIFile : public QMap { public: explicit INIFile(); bool loadFile(QString fileName); bool loadFile(QByteArray data); bool saveFile(QString fileName); QVariant get(QString key, QVariant def) const; void set(QString key, QVariant val); }; PrismLauncher-11.0.3/launcher/settings/INISettingsObject.cpp0000644000175100017510000000611315224505336023420 0ustar runnerrunner/* 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 "INISettingsObject.h" #include "Setting.h" #include #include INISettingsObject::INISettingsObject(QStringList paths, QObject* parent) : SettingsObject(parent) { auto first_path = paths.constFirst(); for (auto path : paths) { if (!QFile::exists(path)) continue; if (path != first_path && QFile::exists(path)) { // Copy the fallback to the preferred path. QFile::copy(path, first_path); qDebug() << "Copied settings from" << path << "to" << first_path; break; } } m_filePath = first_path; m_ini.loadFile(first_path); } INISettingsObject::INISettingsObject(QString path, QObject* parent) : SettingsObject(parent) { m_filePath = path; m_ini.loadFile(path); } void INISettingsObject::setFilePath(const QString& filePath) { m_filePath = filePath; } bool INISettingsObject::reload() { return m_ini.loadFile(m_filePath) && SettingsObject::reload(); } void INISettingsObject::suspendSave() { m_suspendSave = true; } void INISettingsObject::resumeSave() { m_suspendSave = false; if (m_doSave) { m_ini.saveFile(m_filePath); } } void INISettingsObject::changeSetting(const Setting& setting, QVariant value) { if (contains(setting.id())) { // valid value -> set the main config, remove all the sysnonyms if (value.isValid()) { auto list = setting.configKeys(); m_ini.set(list.takeFirst(), value); for (auto iter : list) m_ini.remove(iter); } // invalid -> remove all (just like resetSetting) else { for (auto iter : setting.configKeys()) m_ini.remove(iter); } doSave(); } } void INISettingsObject::doSave() { if (m_suspendSave) { m_doSave = true; } else { m_ini.saveFile(m_filePath); } } void INISettingsObject::resetSetting(const Setting& setting) { // if we have the setting, remove all the synonyms. ALL OF THEM if (contains(setting.id())) { for (auto iter : setting.configKeys()) m_ini.remove(iter); doSave(); } } QVariant INISettingsObject::retrieveValue(const Setting& setting) { // if we have the setting, return value of the first matching synonym if (contains(setting.id())) { for (auto iter : setting.configKeys()) { if (m_ini.contains(iter)) return m_ini[iter]; } } return QVariant(); } PrismLauncher-11.0.3/launcher/settings/INISettingsObject.h0000644000175100017510000000350615224505336023070 0ustar runnerrunner/* 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 #include "settings/INIFile.h" #include "settings/SettingsObject.h" /*! * \brief A settings object that stores its settings in an INIFile. */ class INISettingsObject : public SettingsObject { Q_OBJECT public: /** 'paths' is a list of INI files to try, in order, for fallback support. */ explicit INISettingsObject(QStringList paths, QObject* parent = nullptr); explicit INISettingsObject(QString path, QObject* parent = nullptr); /*! * \brief Gets the path to the INI file. * \return The path to the INI file. */ virtual QString filePath() const { return m_filePath; } /*! * \brief Sets the path to the INI file and reloads it. * \param filePath The INI file's new path. */ virtual void setFilePath(const QString& filePath); bool reload() override; void suspendSave() override; void resumeSave() override; protected slots: virtual void changeSetting(const Setting& setting, QVariant value) override; virtual void resetSetting(const Setting& setting) override; protected: virtual QVariant retrieveValue(const Setting& setting) override; void doSave(); protected: INIFile m_ini; QString m_filePath; }; PrismLauncher-11.0.3/launcher/settings/INIFile.cpp0000644000175100017510000002025015224505336021346 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "settings/INIFile.h" #include #include #include #include #include #include #include #include #include "Json.h" INIFile::INIFile() {} bool INIFile::saveFile(QString fileName) { if (!contains("ConfigVersion")) insert("ConfigVersion", "1.3"); QSettings _settings_obj{ fileName, QSettings::Format::IniFormat }; _settings_obj.setFallbacksEnabled(false); _settings_obj.clear(); for (Iterator iter = begin(); iter != end(); iter++) _settings_obj.setValue(iter.key(), iter.value()); _settings_obj.sync(); if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) { if (status == QSettings::Status::AccessError) qCritical() << "An access error occurred while saving INI file" << fileName << "(is the file read-only?)"; if (ASSERT_NEVER(status == QSettings::Status::FormatError)) qCritical() << "A format error occurred while saving INI file" << fileName << "(this shouldn't be possible!)"; return false; } return true; } QString unescape(QString orig) { QString out; QChar prev = QChar::Null; for (auto c : orig) { if (prev == '\\') { if (c == 'n') out += '\n'; else if (c == 't') out += '\t'; else if (c == '#') out += '#'; else out += c; prev = QChar::Null; } else { if (c == '\\') { prev = c; continue; } out += c; prev = QChar::Null; } } return out; } QString unquote(QString str) { if ((str.contains(QChar(';')) || str.contains(QChar('=')) || str.contains(QChar(','))) && str.endsWith("\"") && str.startsWith("\"")) { #if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) str = str.remove(0, 1); str = str.remove(str.size() - 1, 1); #else str = str.removeFirst().removeLast(); #endif } return str; } bool parseOldFileFormat(QIODevice& device, QSettings::SettingsMap& map) { QTextStream in(device.readAll()); #if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0) in.setCodec("UTF-8"); #endif QStringList lines = in.readAll().split('\n'); for (int i = 0; i < lines.count(); i++) { QString& lineRaw = lines[i]; // Ignore comments. int commentIndex = 0; QString line = lineRaw; // Search for comments until no more escaped # are available while ((commentIndex = line.indexOf('#', commentIndex + 1)) != -1) { if (commentIndex > 0 && line.at(commentIndex - 1) == '\\') { continue; } line = line.left(lineRaw.indexOf('#')).trimmed(); } int eqPos = line.indexOf('='); if (eqPos == -1) continue; QString key = line.left(eqPos).trimmed(); QString valueStr = line.right(line.length() - eqPos - 1).trimmed(); valueStr = unquote(unescape(valueStr)); QVariant value(valueStr); map.insert(key, value); } return true; } QVariant migrateQByteArrayToBase64(QString key, QVariant value) { static const QStringList otherByteArrays = { "MainWindowState", "MainWindowGeometry", "ConsoleWindowState", "ConsoleWindowGeometry", "PagedGeometry", "NewInstanceGeometry", "ModDownloadGeometry", "RPDownloadGeometry", "TPDownloadGeometry", "ShaderDownloadGeometry" }; if (key.startsWith("WideBarVisibility_") || (key.startsWith("UI/") && key.endsWith("_Page/Columns"))) { return QString::fromUtf8(value.toByteArray().toBase64()); } if (otherByteArrays.contains(key)) { return QString::fromUtf8(value.toByteArray()); } if (key == "linkedInstances") { return Json::fromStringList(value.toStringList()); } if (key == "Env") { return Json::fromMap(value.toMap()); } return value; } bool INIFile::loadFile(QString fileName) { QSettings _settings_obj{ fileName, QSettings::Format::IniFormat }; _settings_obj.setFallbacksEnabled(false); if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) { if (status == QSettings::Status::AccessError) qCritical() << "An access error occurred while loading INI file" << fileName; if (status == QSettings::Status::FormatError) qCritical() << "A format error occurred while loading INI file" << fileName << "(is the file malformed or corrupted?)"; return false; } if (!_settings_obj.value("ConfigVersion").isValid()) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) return false; QSettings::SettingsMap map; parseOldFileFormat(file, map); file.close(); for (auto&& key : map.keys()) { auto value = migrateQByteArrayToBase64(key, map.value(key)); insert(key, value); } insert("ConfigVersion", "1.3"); } else if (_settings_obj.value("ConfigVersion").toString() == "1.1") { for (auto&& key : _settings_obj.allKeys()) { auto value = migrateQByteArrayToBase64(key, _settings_obj.value(key)); if (auto valueStr = value.toString(); (valueStr.contains(QChar(';')) || valueStr.contains(QChar('=')) || valueStr.contains(QChar(','))) && valueStr.endsWith("\"") && valueStr.startsWith("\"")) { insert(key, unquote(valueStr)); } else { insert(key, value); } } insert("ConfigVersion", "1.3"); } else if (_settings_obj.value("ConfigVersion").toString() == "1.2") { for (auto&& key : _settings_obj.allKeys()) { auto value = migrateQByteArrayToBase64(key, _settings_obj.value(key)); insert(key, value); } insert("ConfigVersion", "1.3"); } else { for (auto&& key : _settings_obj.allKeys()) { insert(key, _settings_obj.value(key)); } } return true; } bool INIFile::loadFile(QByteArray data) { QTemporaryFile file; if (!file.open()) return false; file.write(data); file.flush(); file.close(); auto loaded = loadFile(file.fileName()); file.remove(); return loaded; } QVariant INIFile::get(QString key, QVariant def) const { if (!this->contains(key)) return def; else return this->operator[](key); } void INIFile::set(QString key, QVariant val) { this->operator[](key) = val; } PrismLauncher-11.0.3/launcher/settings/PassthroughSetting.h0000644000175100017510000000247315224505336023450 0ustar runnerrunner/* 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 #include #include "Setting.h" /*! * \brief A setting that 'overrides another.' based on the value of a 'gate' setting * If 'gate' evaluates to true, the override stores and returns data * If 'gate' evaluates to false, the original does, */ class PassthroughSetting : public Setting { Q_OBJECT public: explicit PassthroughSetting(std::shared_ptr overridden, std::shared_ptr gate); virtual QVariant defValue() const; virtual QVariant get() const; virtual void set(QVariant value); virtual void reset(); private: bool isOverriding() const; protected: std::shared_ptr m_other; std::shared_ptr m_gate; }; PrismLauncher-11.0.3/launcher/settings/SettingsObject.cpp0000644000175100017510000001762115224505336023066 0ustar runnerrunner/* 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 "settings/SettingsObject.h" #include #include "PassthroughSetting.h" #include "settings/OverrideSetting.h" #include "settings/Setting.h" #include #include #include #ifdef Q_OS_MACOS #include "macsandbox/SecurityBookmarkFileAccess.h" #endif SettingsObject::SettingsObject(QObject* parent) : QObject(parent) {} SettingsObject::~SettingsObject() { m_settings.clear(); } std::shared_ptr SettingsObject::registerOverride(std::shared_ptr original, std::shared_ptr gate) { if (contains(original->id())) { qCritical() << QString("Failed to register setting %1. ID already exists.").arg(original->id()); return nullptr; // Fail } auto override = std::make_shared(original, gate); override->m_storage = this; connectSignals(*override); m_settings.insert(override->id(), override); return override; } std::shared_ptr SettingsObject::registerPassthrough(std::shared_ptr original, std::shared_ptr gate) { if (contains(original->id())) { qCritical() << QString("Failed to register setting %1. ID already exists.").arg(original->id()); return nullptr; // Fail } auto passthrough = std::make_shared(original, gate); passthrough->m_storage = this; connectSignals(*passthrough); m_settings.insert(passthrough->id(), passthrough); return passthrough; } std::shared_ptr SettingsObject::registerSetting(QStringList synonyms, QVariant defVal) { if (synonyms.empty()) return nullptr; if (contains(synonyms.first())) { qCritical() << QString("Failed to register setting %1. ID already exists.").arg(synonyms.first()); return nullptr; // Fail } auto setting = std::make_shared(synonyms, defVal); setting->m_storage = this; connectSignals(*setting); m_settings.insert(setting->id(), setting); return setting; } std::shared_ptr SettingsObject::getSetting(const QString& id) const { // Make sure there is a setting with the given ID. if (!m_settings.contains(id)) return NULL; return m_settings[id]; } QVariant SettingsObject::get(const QString& id) { auto setting = getSetting(id); #ifdef Q_OS_MACOS // for macOS, use a security scoped bookmark for the paths if (id.endsWith("Dir")) { return { getPathFromBookmark(id) }; } #endif return (setting ? setting->get() : QVariant()); } bool SettingsObject::set(const QString& id, QVariant value) { auto setting = getSetting(id); if (!setting) { qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id); return false; } #ifdef Q_OS_MACOS // for macOS, keep a security scoped bookmark for the paths if (value.userType() == QMetaType::QString && id.endsWith("Dir")) { setPathWithBookmark(id, value.toString()); } #endif setting->set(std::move(value)); return true; } #ifdef Q_OS_MACOS QString SettingsObject::getPathFromBookmark(const QString& id) { auto setting = getSetting(id); if (!setting) { qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id); return ""; } // there is no need to use bookmarks if the default value is used or the directory is within the data directory (already can access) if (setting->get() == setting->defValue() || QDir(setting->get().toString()).absolutePath().startsWith(QDir::current().absolutePath())) { return setting->get().toString(); } auto bookmarkId = id + "Bookmark"; auto bookmarkSetting = getSetting(bookmarkId); if (!bookmarkSetting) { qCritical() << QString("Error changing setting %1. Bookmark setting doesn't exist.").arg(id); return ""; } QByteArray bookmark = bookmarkSetting->get().toByteArray(); if (bookmark.isEmpty()) { qDebug() << "Creating bookmark for" << id << "at" << setting->get().toString(); setPathWithBookmark(id, setting->get().toString()); return setting->get().toString(); } bool stale; QUrl url = m_sandboxedFileAccess.securityScopedBookmarkToURL(bookmark, stale); if (url.isValid()) { if (stale) { setting->set(url.path()); bookmarkSetting->set(bookmark); } m_sandboxedFileAccess.startUsingSecurityScopedBookmark(bookmark, stale); // already did a stale check, no need to do it again // convert to relative path to current directory if `url` is a descendant of the current directory QDir currentDir = QDir::current().absolutePath(); return url.path().startsWith(currentDir.absolutePath()) ? currentDir.relativeFilePath(url.path()) : url.path(); } return setting->get().toString(); } bool SettingsObject::setPathWithBookmark(const QString& id, const QString& path) { auto setting = getSetting(id); if (!setting) { qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id); return false; } QDir dir(path); if (!dir.exists()) { qCritical() << QString("Error changing setting %1. Path doesn't exist.").arg(id); return false; } QString absolutePath = dir.absolutePath(); QString bookmarkId = id + "Bookmark"; std::shared_ptr bookmarkSetting = getSetting(bookmarkId); // there is no need to use bookmarks if the default value is used or the directory is within the data directory (already can access) if (path == setting->defValue().toString() || absolutePath.startsWith(QDir::current().absolutePath())) { bookmarkSetting->reset(); return true; } QByteArray bytes = m_sandboxedFileAccess.pathToSecurityScopedBookmark(absolutePath); if (bytes.isEmpty()) { qCritical() << QString("Failed to create bookmark for %1 - no access?").arg(id); // TODO: show an alert to the user asking them to reselect the directory return false; } auto oldBookmark = bookmarkSetting->get().toByteArray(); m_sandboxedFileAccess.stopUsingSecurityScopedBookmark(oldBookmark); if (!bytes.isEmpty() && bookmarkSetting) { bookmarkSetting->set(bytes); bool stale; m_sandboxedFileAccess.startUsingSecurityScopedBookmark(bytes, stale); // just created the bookmark, it shouldn't be stale } setting->set(path); return true; } #endif void SettingsObject::reset(const QString& id) const { auto setting = getSetting(id); if (setting) setting->reset(); } bool SettingsObject::contains(const QString& id) { return m_settings.contains(id); } bool SettingsObject::reload() { for (auto setting : m_settings.values()) { setting->set(setting->get()); } return true; } void SettingsObject::connectSignals(const Setting& setting) { connect(&setting, &Setting::SettingChanged, this, &SettingsObject::changeSetting); connect(&setting, &Setting::SettingChanged, this, &SettingsObject::SettingChanged); connect(&setting, &Setting::settingReset, this, &SettingsObject::resetSetting); connect(&setting, &Setting::settingReset, this, &SettingsObject::settingReset); } std::shared_ptr SettingsObject::getOrRegisterSetting(const QString& id, QVariant defVal) { return contains(id) ? getSetting(id) : registerSetting(id, defVal); } PrismLauncher-11.0.3/launcher/translations/0000755000175100017510000000000015224505336020305 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/translations/TranslationsModel.h0000644000175100017510000000433515224505336024125 0ustar runnerrunner/* 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 #include #include struct Language; class TranslationsModel : public QAbstractListModel { Q_OBJECT public: explicit TranslationsModel(const QString& path, QObject* parent = nullptr); ~TranslationsModel() override; // hide copy constructor TranslationsModel(const TranslationsModel&) = delete; // hide assign op TranslationsModel& operator=(const TranslationsModel&) = delete; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; // NOLINT(*-default-arguments) QVariant headerData(int section, Qt::Orientation orientation, int role) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; // NOLINT(*-default-arguments) bool selectLanguage(QString key) const; void updateLanguage(const QString& key); QModelIndex selectedIndex() const; QString selectedLanguage() const; void downloadIndex(); void setUseSystemLocale(bool useSystemLocale) const; private: int columnCount(const QModelIndex& parent) const override; QList::Iterator findLanguage(const QString& key) const; std::optional findLanguageAsOptional(const QString& key) const; void reloadLocalFiles(); void downloadTranslation(const QString& key); void downloadNext(); private slots: void indexReceived(); void indexFailed(const QString& reason) const; void dlFailed(const QString& reason); void dlGood(); void translationDirChanged(const QString& path); private: /* data */ struct Private; std::unique_ptr d; }; PrismLauncher-11.0.3/launcher/translations/TranslationsModel.cpp0000644000175100017510000005215215224505336024460 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "TranslationsModel.h" #include #include #include #include "BuildConfig.h" #include "FileSystem.h" #include "Json.h" #include "net/ChecksumValidator.h" #include "net/NetJob.h" #include "POTranslator.h" #include "Application.h" #include "settings/SettingsObject.h" static constexpr QLatin1String g_defaultLangCode("en_US"); namespace { enum class FileType : std::uint8_t { None, Qm, Po }; QString getSystemLocaleName() { return QLocale::system().name(); } QString getSystemLanguage() { return getSystemLocaleName().split('_').front(); } } // namespace struct Language { Language() : updated(true) {} explicit Language(QString _key) : key(std::move(_key)), updated(key == g_defaultLangCode) { locale = QLocale(key); } QString languageName() const { QString result; if (key == "ja_KANJI") { result = locale.nativeLanguageName() + u8" (漢字)"; } else if (key == "es_UY") { result = u8"Español de Latinoamérica"; } else if (key == "en_NZ") { result = u8"New Zealand English"; // No idea why qt translates this to just english and not to New Zealand English } else if (key == "en@pirate") { result = u8"Tongue of the High Seas"; } else if (key == "en@uwu") { result = u8"Cute Engwish"; } else if (key == "tok") { result = u8"toki pona"; } else if (key == "nan") { result = u8"閩南語"; // Using traditional Chinese script. Not sure if we should use simplified instead? } else { result = locale.nativeLanguageName(); } if (result.isEmpty()) { result = key; } return result; } float percentTranslated() const { if (total == 0) { return 100.0F; } return 100.0F * static_cast(translated) / static_cast(total); } void setTranslationStats(const unsigned _translated, const unsigned _untranslated, const unsigned _fuzzy) { translated = _translated; untranslated = _untranslated; fuzzy = _fuzzy; total = translated + untranslated + fuzzy; } bool isOfSameNameAs(const Language& other) const { return key == other.key; } bool isIdenticalTo(const Language& other) const { return (key == other.key && fileName == other.fileName && fileSize == other.fileSize && fileSha1 == other.fileSha1 && translated == other.translated && fuzzy == other.fuzzy && total == other.fuzzy && localFileType == other.localFileType); } Language& apply(const Language& other) { if (!isOfSameNameAs(other)) { return *this; } fileName = other.fileName; fileSize = other.fileSize; fileSha1 = other.fileSha1; translated = other.translated; fuzzy = other.fuzzy; total = other.total; localFileType = other.localFileType; return *this; } QString key; QLocale locale; bool updated; QString fileName = QString(); std::size_t fileSize = 0; QString fileSha1 = QString(); unsigned translated = 0; unsigned untranslated = 0; unsigned fuzzy = 0; unsigned total = 0; FileType localFileType = FileType::None; }; struct TranslationsModel::Private { QDir m_dir; // initial state is just english QList m_languages = { Language(g_defaultLangCode) }; QString m_selectedLanguage = g_defaultLangCode; std::unique_ptr m_qtTranslator; std::unique_ptr m_appTranslator; Net::Download* m_indexTask = nullptr; QString m_downloadingTranslation; NetJob::Ptr m_downloadJob; NetJob::Ptr m_indexJob; QString m_nextDownload; QFileSystemWatcher* watcher = nullptr; bool m_noLanguageSet = false; }; TranslationsModel::TranslationsModel(const QString& path, QObject* parent) : QAbstractListModel(parent) { d = std::make_unique(); d->m_dir.setPath(path); d->m_selectedLanguage = APPLICATION->settings()->get("Language").toString(); FS::ensureFolderPathExists(path); reloadLocalFiles(); d->watcher = new QFileSystemWatcher(this); connect(d->watcher, &QFileSystemWatcher::directoryChanged, this, &TranslationsModel::translationDirChanged); d->watcher->addPath(d->m_dir.canonicalPath()); } TranslationsModel::~TranslationsModel() = default; void TranslationsModel::translationDirChanged(const QString& path) { qDebug() << "Dir changed:" << path; if (!d->m_noLanguageSet) { reloadLocalFiles(); } selectLanguage(selectedLanguage()); } void TranslationsModel::indexReceived() { qDebug() << "Got translations index!"; d->m_indexJob.reset(); reloadLocalFiles(); if (d->m_noLanguageSet) { auto language = getSystemLocaleName(); if (!findLanguageAsOptional(language).has_value()) { language = getSystemLanguage(); } selectLanguage(language); APPLICATION->settings()->set("Language", selectedLanguage()); d->m_noLanguageSet = false; } if (selectedLanguage() != g_defaultLangCode) { updateLanguage(selectedLanguage()); } } namespace { void readIndex(const QString& path, QMap& languages) { QByteArray data; try { data = FS::read(path); } catch ([[maybe_unused]] const Exception& e) { qCritical() << "Translations Download Failed: index file not readable"; return; } try { auto toplevelDoc = Json::requireDocument(data); auto doc = Json::requireObject(toplevelDoc); auto fileType = Json::requireString(doc, "file_type"); if (fileType != "MMC-TRANSLATION-INDEX") { qCritical() << "Translations Download Failed: index file is of unknown file type" << fileType; return; } auto version = Json::requireInteger(doc, "version"); if (version > 2) { qCritical() << "Translations Download Failed: index file is of unknown format version" << fileType; return; } auto langObjs = Json::requireObject(doc, "languages"); for (auto iter = langObjs.begin(); iter != langObjs.end(); ++iter) { Language lang(iter.key()); auto langObj = Json::requireObject(iter.value()); lang.setTranslationStats(langObj["translated"].toInt(), langObj["untranslated"].toInt(), langObj["fuzzy"].toInt()); lang.fileName = Json::requireString(langObj, "file"); lang.fileSha1 = Json::requireString(langObj, "sha1"); lang.fileSize = Json::requireInteger(langObj, "size"); languages.insert(lang.key, lang); } } catch ([[maybe_unused]] Json::JsonException& e) { qCritical() << "Translations Download Failed: index file could not be parsed as json"; } } } // namespace void TranslationsModel::reloadLocalFiles() { QMap languages = { { g_defaultLangCode, Language(g_defaultLangCode) } }; const auto indexPath = d->m_dir.absoluteFilePath("index_v2.json"); if (!QFileInfo::exists(indexPath)) { downloadIndex(); return; } readIndex(indexPath, languages); auto entries = d->m_dir.entryInfoList({ "mmc_*.qm", "*.po" }, QDir::Files | QDir::NoDotAndDotDot); for (auto& entry : entries) { auto completeSuffix = entry.completeSuffix(); QString langCode; FileType fileType = FileType::None; if (completeSuffix == "qm") { langCode = entry.baseName().remove(0, 4); fileType = FileType::Qm; } else if (completeSuffix == "po") { langCode = entry.baseName(); fileType = FileType::Po; } else { continue; } auto langIter = languages.find(langCode); if (langIter != languages.end()) { auto& language = *langIter; // TODO: use std::to_underlying in C++23 if (static_cast(fileType) > static_cast(language.localFileType)) { language.localFileType = fileType; } } else { if (fileType == FileType::Po) { Language localFound(langCode); localFound.localFileType = FileType::Po; languages.insert(langCode, localFound); } } } // changed and removed languages for (auto iter = d->m_languages.begin(); iter != d->m_languages.end();) { auto& language = *iter; auto row = iter - d->m_languages.begin(); auto updatedLanguageIter = languages.find(language.key); if (updatedLanguageIter != languages.end()) { if (language.isIdenticalTo(*updatedLanguageIter)) { languages.remove(language.key); } else { language.apply(*updatedLanguageIter); emit dataChanged(index(row), index(row)); languages.remove(language.key); } ++iter; } else { beginRemoveRows(QModelIndex(), row, row); iter = d->m_languages.erase(iter); endRemoveRows(); } } // added languages if (languages.isEmpty()) { return; } beginInsertRows(QModelIndex(), 0, d->m_languages.size() + languages.size() - 1); for (auto& language : languages) { d->m_languages.append(language); } const auto comp = [systemLocale = getSystemLocaleName(), systemLanguage = getSystemLanguage()](const Language& a, const Language& b) { if (a.key != b.key) { if (a.key == systemLocale || a.key == systemLanguage) { return true; } if (b.key == systemLocale || b.key == systemLanguage) { return false; } } return a.languageName().toLower() < b.languageName().toLower(); }; std::ranges::sort(d->m_languages, comp); endInsertRows(); } namespace { enum class Column : std::uint8_t { Language, Completeness }; } QVariant TranslationsModel::data(const QModelIndex& index, const int role) const { if (!index.isValid()) { return {}; } const int row = index.row(); const auto column = static_cast(index.column()); if (row < 0 || row >= d->m_languages.size()) { return {}; } auto& lang = d->m_languages[row]; switch (role) { case Qt::DisplayRole: { switch (column) { case Column::Language: { return lang.languageName(); } case Column::Completeness: { return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1); } } qWarning("TranslationModel::data not implemented when role is DisplayRole"); } case Qt::ToolTipRole: { return tr("%1:\n%2 translated\n%3 fuzzy\n%4 total") .arg(lang.key, QString::number(lang.translated), QString::number(lang.fuzzy), QString::number(lang.total)); } case Qt::UserRole: return lang.key; default: return {}; } } QVariant TranslationsModel::headerData(int section, const Qt::Orientation orientation, const int role) const { auto column = static_cast(section); if (role == Qt::DisplayRole) { switch (column) { case Column::Language: { return tr("Language"); } case Column::Completeness: { return tr("Completeness"); } } } else if (role == Qt::ToolTipRole) { switch (column) { case Column::Language: { return tr("The native language name."); } case Column::Completeness: { return tr("Completeness is the percentage of fully translated strings, not counting automatically guessed ones."); } } } return QAbstractListModel::headerData(section, orientation, role); } int TranslationsModel::rowCount([[maybe_unused]] const QModelIndex& parent) const { return d->m_languages.size(); } int TranslationsModel::columnCount([[maybe_unused]] const QModelIndex& parent) const { return 2; } QList::Iterator TranslationsModel::findLanguage(const QString& key) const { return std::ranges::find_if(d->m_languages, [key](const Language& lang) { return lang.key == key; }); } std::optional TranslationsModel::findLanguageAsOptional(const QString& key) const { auto found = findLanguage(key); if (found != d->m_languages.end()) { return *found; } return {}; } void TranslationsModel::setUseSystemLocale(const bool useSystemLocale) const { APPLICATION->settings()->set("UseSystemLocale", useSystemLocale); QLocale::setDefault(useSystemLocale ? QLocale::system() : QLocale(selectedLanguage())); } bool TranslationsModel::selectLanguage(QString key) const { QString& langCode = key; auto langPtr = findLanguageAsOptional(key); if (langCode.isEmpty()) { d->m_noLanguageSet = true; } if (!langPtr.has_value()) { qWarning() << "Selected invalid language" << key << ", defaulting to" << g_defaultLangCode; langCode = g_defaultLangCode; } else { langCode = langPtr->key; } // uninstall existing translators if there are any if (d->m_appTranslator) { QCoreApplication::removeTranslator(d->m_appTranslator.get()); d->m_appTranslator.reset(); } if (d->m_qtTranslator) { QCoreApplication::removeTranslator(d->m_qtTranslator.get()); d->m_qtTranslator.reset(); } /* * FIXME: potential source of crashes: * In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created. * This function is not reentrant. */ const bool useSystemLocale = APPLICATION->settings()->get("UseSystemLocale").toBool(); QLocale::setDefault(useSystemLocale ? QLocale::system() : QLocale(langCode)); // if it's the default UI language, finish if (langCode == g_defaultLangCode) { d->m_selectedLanguage = langCode; return true; } // otherwise install new translations bool successful = false; // FIXME: this is likely never present. FIX IT. d->m_qtTranslator = std::make_unique(); if (d->m_qtTranslator->load("qt_" + langCode, QLibraryInfo::path(QLibraryInfo::TranslationsPath))) { qDebug() << "Loading Qt Language File for" << langCode.toLocal8Bit().constData() << "..."; if (!QCoreApplication::installTranslator(d->m_qtTranslator.get())) { qCritical() << "Loading Qt Language File failed."; d->m_qtTranslator.reset(); } else { successful = true; } } else { d->m_qtTranslator.reset(); } if (langPtr->localFileType == FileType::Po) { qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "..."; d->m_appTranslator = std::make_unique(FS::PathCombine(d->m_dir.path(), langCode + ".po")); if (!d->m_appTranslator->isEmpty()) { if (!QCoreApplication::installTranslator(d->m_appTranslator.get())) { qCritical() << "Installing Application Language File failed."; d->m_appTranslator.reset(); } else { successful = true; } } else { qCritical() << "Loading Application Language File failed."; d->m_appTranslator.reset(); } } else if (langPtr->localFileType == FileType::Qm) { d->m_appTranslator = std::make_unique(); if (d->m_appTranslator->load("mmc_" + langCode, d->m_dir.path())) { qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "..."; if (!QCoreApplication::installTranslator(d->m_appTranslator.get())) { qCritical() << "Installing Application Language File failed."; d->m_appTranslator.reset(); } else { successful = true; } } else { d->m_appTranslator.reset(); } } else { d->m_appTranslator.reset(); } d->m_selectedLanguage = langCode; return successful; } QModelIndex TranslationsModel::selectedIndex() const { auto found = findLanguage(d->m_selectedLanguage); if (found != d->m_languages.end()) { return index(std::distance(d->m_languages.begin(), found), 0, QModelIndex()); } return {}; } QString TranslationsModel::selectedLanguage() const { return d->m_selectedLanguage; } void TranslationsModel::downloadIndex() { if (d->m_indexJob || d->m_downloadJob) { return; } qDebug() << "Downloading Translations Index..."; d->m_indexJob.reset(new NetJob("Translations Index", APPLICATION->network())); const MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json"); entry->setStale(true); auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + "index_v2.json"), entry); d->m_indexTask = task.get(); d->m_indexJob->addNetAction(task); d->m_indexJob->setAskRetry(false); connect(d->m_indexJob.get(), &NetJob::failed, this, &TranslationsModel::indexFailed); connect(d->m_indexJob.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived); d->m_indexJob->start(); } void TranslationsModel::updateLanguage(const QString& key) { if (key == g_defaultLangCode) { qWarning() << "Cannot update builtin language" << key; return; } auto found = findLanguageAsOptional(key); if (!found.has_value()) { qWarning() << "Cannot update invalid language" << key; return; } if (!found->updated) { downloadTranslation(key); } } void TranslationsModel::downloadTranslation(const QString& key) { if (d->m_downloadJob) { d->m_nextDownload = key; return; } auto lang = findLanguageAsOptional(key); if (!lang.has_value()) { qWarning() << "Will not download an unknown translation" << key; return; } d->m_downloadingTranslation = key; const MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "mmc_" + key + ".qm"); entry->setStale(true); auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + lang->fileName), entry); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, lang->fileSha1)); dl->setProgress(dl->getProgress(), lang->fileSize); d->m_downloadJob.reset(new NetJob("Translation for " + key, APPLICATION->network())); d->m_downloadJob->addNetAction(dl); d->m_downloadJob->setAskRetry(false); connect(d->m_downloadJob.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood); connect(d->m_downloadJob.get(), &NetJob::failed, this, &TranslationsModel::dlFailed); d->m_downloadJob->start(); } void TranslationsModel::downloadNext() { if (!d->m_nextDownload.isEmpty()) { downloadTranslation(d->m_nextDownload); d->m_nextDownload.clear(); } } void TranslationsModel::dlFailed(const QString& reason) { qCritical() << "Translations Download Failed:" << reason; d->m_downloadJob.reset(); downloadNext(); } void TranslationsModel::dlGood() { qDebug() << "Got translation:" << d->m_downloadingTranslation; if (d->m_downloadingTranslation == d->m_selectedLanguage) { selectLanguage(d->m_selectedLanguage); } d->m_downloadJob.reset(); downloadNext(); } void TranslationsModel::indexFailed(const QString& reason) const { qCritical() << "Translations Index Download Failed:" << reason; d->m_indexJob.reset(); } PrismLauncher-11.0.3/launcher/translations/POTranslator.cpp0000644000175100017510000002370515224505336023410 0ustar runnerrunner#include "POTranslator.h" #include #include "FileSystem.h" struct POEntry { QString text; bool fuzzy; }; struct POTranslatorPrivate { QString filename; QHash mapping; QHash mapping_disambiguatrion; bool loaded = false; void reload(); }; class ParserArray : public QByteArray { public: ParserArray(const QByteArray& in) : QByteArray(in) {} bool chomp(const char* data, int length) { if (startsWith(data)) { remove(0, length); return true; } return false; } bool chompString(QByteArray& appendHere) { QByteArray msg; bool escape = false; if (size() < 2) { qDebug() << "String fragment is too short"; return false; } if (!startsWith('"')) { qDebug() << "String fragment does not start with \""; return false; } if (!endsWith('"')) { qDebug() << "String fragment does not end with \", instead, there is" << at(size() - 1); return false; } for (int i = 1; i < size() - 1; i++) { char c = operator[](i); if (escape) { switch (c) { case 'r': msg += '\r'; break; case 'n': msg += '\n'; break; case 't': msg += '\t'; break; case 'v': msg += '\v'; break; case 'a': msg += '\a'; break; case 'b': msg += '\b'; break; case 'f': msg += '\f'; break; case '"': msg += '"'; break; case '\\': msg.append('\\'); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': { int octal_start = i; while ((c = operator[](i)) >= '0' && c <= '7') { i++; if (i == length() - 1) { qDebug() << "Something went bad while parsing an octal escape string..."; return false; } } msg += mid(octal_start, i - octal_start).toUInt(0, 8); break; } case 'x': { // chomp the 'x' i++; int hex_start = i; while (isxdigit(operator[](i))) { i++; if (i == length() - 1) { qDebug() << "Something went bad while parsing a hex escape string..."; return false; } } msg += mid(hex_start, i - hex_start).toUInt(0, 16); break; } default: { qDebug() << "Invalid escape sequence character:" << c; return false; } } escape = false; } else if (c == '\\') { escape = true; } else { msg += c; } } if (escape) { qDebug() << "Unterminated escape sequence..."; return false; } appendHere += msg; return true; } }; void POTranslatorPrivate::reload() { QFile file(filename); if (!file.open(QFile::OpenMode::enum_type::ReadOnly | QFile::OpenMode::enum_type::Text)) { qDebug() << "Failed to open PO file:" << filename << "error:" << file.errorString(); return; } QByteArray context; QByteArray disambiguation; QByteArray id; QByteArray str; bool fuzzy = false; bool nextFuzzy = false; enum class Mode { First, MessageContext, MessageId, MessageString } mode = Mode::First; int lineNumber = 0; QHash newMapping; QHash newMapping_disambiguation; auto endEntry = [&]() { auto strStr = QString::fromUtf8(str); // NOTE: PO header has empty id. We skip it. if (!id.isEmpty()) { auto normalKey = context + "|" + id; newMapping.insert(normalKey, { strStr, fuzzy }); if (!disambiguation.isEmpty()) { auto disambiguationKey = context + "|" + id + "@" + disambiguation; newMapping_disambiguation.insert(disambiguationKey, { strStr, fuzzy }); } } context.clear(); disambiguation.clear(); id.clear(); str.clear(); fuzzy = nextFuzzy; nextFuzzy = false; }; while (!file.atEnd()) { ParserArray line = file.readLine(); if (line.endsWith('\n')) { line.resize(line.size() - 1); } if (line.endsWith('\r')) { line.resize(line.size() - 1); } if (!line.size()) { // NIL } else if (line[0] == '#') { if (line.contains(", fuzzy")) { nextFuzzy = true; } } else if (line.startsWith('"')) { QByteArray temp; QByteArray* out = &temp; switch (mode) { case Mode::First: qDebug() << "Unexpected escaped string during initial state... line:" << lineNumber; return; case Mode::MessageString: out = &str; break; case Mode::MessageContext: out = &context; break; case Mode::MessageId: out = &id; break; } if (!line.chompString(*out)) { qDebug() << "Badly formatted string on line:" << lineNumber; return; } } else if (line.chomp("msgctxt ", 8)) { switch (mode) { case Mode::First: break; case Mode::MessageString: endEntry(); break; case Mode::MessageContext: case Mode::MessageId: qDebug() << "Unexpected msgctxt line:" << lineNumber; return; } if (line.chompString(context)) { auto parts = context.split('|'); context = parts[0]; if (parts.size() > 1 && !parts[1].isEmpty()) { disambiguation = parts[1]; } mode = Mode::MessageContext; } } else if (line.chomp("msgid ", 6)) { switch (mode) { case Mode::MessageContext: case Mode::First: break; case Mode::MessageString: endEntry(); break; case Mode::MessageId: qDebug() << "Unexpected msgid line:" << lineNumber; return; } if (line.chompString(id)) { mode = Mode::MessageId; } } else if (line.chomp("msgstr ", 7)) { switch (mode) { case Mode::First: case Mode::MessageString: case Mode::MessageContext: qDebug() << "Unexpected msgstr line:" << lineNumber; return; case Mode::MessageId: break; } if (line.chompString(str)) { mode = Mode::MessageString; } } else { qDebug() << "I did not understand line:" << lineNumber << ":" << QString::fromUtf8(line); } lineNumber++; } endEntry(); mapping = std::move(newMapping); mapping_disambiguatrion = std::move(newMapping_disambiguation); loaded = true; } POTranslator::POTranslator(const QString& filename, QObject* parent) : QTranslator(parent) { d = new POTranslatorPrivate; d->filename = filename; d->reload(); } POTranslator::~POTranslator() { delete d; } QString POTranslator::translate(const char* context, const char* sourceText, const char* disambiguation, [[maybe_unused]] int n) const { if (disambiguation) { auto disambiguationKey = QByteArray(context) + "|" + QByteArray(sourceText) + "@" + QByteArray(disambiguation); auto iter = d->mapping_disambiguatrion.find(disambiguationKey); if (iter != d->mapping_disambiguatrion.end()) { auto& entry = *iter; if (entry.text.isEmpty()) { qDebug() << "Translation entry has no content:" << disambiguationKey; } if (entry.fuzzy) { qDebug() << "Translation entry is fuzzy:" << disambiguationKey << "->" << entry.text; } return entry.text; } } auto key = QByteArray(context) + "|" + QByteArray(sourceText); auto iter = d->mapping.find(key); if (iter != d->mapping.end()) { auto& entry = *iter; if (entry.text.isEmpty()) { qDebug() << "Translation entry has no content:" << key; } if (entry.fuzzy) { qDebug() << "Translation entry is fuzzy:" << key << "->" << entry.text; } return entry.text; } return QString(); } bool POTranslator::isEmpty() const { return !d->loaded; } PrismLauncher-11.0.3/launcher/translations/POTranslator.h0000644000175100017510000000066615224505336023056 0ustar runnerrunner#pragma once #include struct POTranslatorPrivate; class POTranslator : public QTranslator { Q_OBJECT public: explicit POTranslator(const QString& filename, QObject* parent = nullptr); virtual ~POTranslator(); QString translate(const char* context, const char* sourceText, const char* disambiguation, int n) const override; bool isEmpty() const override; private: POTranslatorPrivate* d; }; PrismLauncher-11.0.3/launcher/updater/0000755000175100017510000000000015224505336017230 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/updater/prismupdater/0000755000175100017510000000000015224505336021747 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/updater/prismupdater/GitHubRelease.h0000644000175100017510000000314315224505336024604 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include #include #include #include "Version.h" struct GitHubReleaseAsset { int id = -1; QString name; QString label; QString content_type; int size; QDateTime created_at; QDateTime updated_at; QString browser_download_url; bool isValid() { return id > 0; } }; struct GitHubRelease { int id = -1; QString name; QString tag_name; QDateTime created_at; QDateTime published_at; bool prerelease; bool draft; QString body; QList assets; Version version; bool isValid() const { return id > 0; } }; QDebug operator<<(QDebug debug, const GitHubReleaseAsset& rls); QDebug operator<<(QDebug debug, const GitHubRelease& rls); PrismLauncher-11.0.3/launcher/updater/prismupdater/PrismUpdater.h0000644000175100017510000000765615224505336024555 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include "QObjectPtr.h" #include "net/Download.h" #define PRISM_EXTERNAL_EXE #include "FileSystem.h" #include "GitHubRelease.h" class PrismUpdaterApp : public QApplication { Q_OBJECT public: enum Status { Starting, Failed, Succeeded, Initialized, Aborted }; PrismUpdaterApp(int& argc, char** argv); virtual ~PrismUpdaterApp(); void loadReleaseList(); void run(); Status status() const { return m_status; } private: void fail(const QString& reason); void abort(const QString& reason); void showFatalErrorMessage(const QString& title, const QString& content); bool loadPrismVersionFromExe(const QString& exe_path); void downloadReleasePage(const QString& api_url, int page); int parseReleasePage(const QByteArray* response); bool needUpdate(const GitHubRelease& release); GitHubRelease getLatestRelease(); GitHubRelease selectRelease(); QList newerReleases(); QList nonDraftReleases(); void printReleases(); QList validReleaseArtifacts(const GitHubRelease& release); GitHubReleaseAsset selectAsset(const QList& assets); void performUpdate(const GitHubRelease& release); void performInstall(QFileInfo file); void unpackAndInstall(QFileInfo file); void backupAppDir(); std::optional unpackArchive(QFileInfo file); QFileInfo downloadAsset(const GitHubReleaseAsset& asset); bool callAppImageUpdate(); void moveAndFinishUpdate(QDir target); public slots: void downloadError(QString reason); private: const QString& root() { return m_rootPath; } bool isPortable() { return m_isPortable; } void clearUpdateLog(); void logUpdate(const QString& msg); QString m_rootPath; QString m_dataPath; bool m_isPortable = false; bool m_isAppimage = false; bool m_isFlatpak = false; QString m_appimagePath; QString m_prismExecutable; QUrl m_prismRepoUrl; Version m_userSelectedVersion; bool m_checkOnly; bool m_forceUpdate; bool m_printOnly; bool m_selectUI; bool m_allowDowngrade; bool m_allowPreRelease; QString m_updateLogPath; QString m_prismBinaryName; QString m_prismVersion; int m_prismVersionMajor = -1; int m_prismVersionMinor = -1; int m_prismVersionPatch = -1; QString m_prsimVersionChannel; QString m_prismGitCommit; GitHubRelease m_install_release; Status m_status = Status::Starting; std::unique_ptr m_network; QString m_current_url; Task::Ptr m_current_task; QList m_releases; public: std::unique_ptr logFile; bool logToConsole = false; #if defined Q_OS_WIN32 // used on Windows to attach the standard IO streams bool consoleAttached = false; #endif }; PrismLauncher-11.0.3/launcher/updater/prismupdater/PrismUpdater.cpp0000644000175100017510000013560215224505336025101 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "PrismUpdater.h" #include "BuildConfig.h" #include "ui/dialogs/ProgressDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; #include "DesktopServices.h" #include "updater/prismupdater/UpdaterDialogs.h" #include "FileSystem.h" #include "Json.h" #include "StringUtils.h" #include "net/Download.h" #include "net/RawHeaderProxy.h" #include "MMCZip.h" /** output to the log file */ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) { static std::mutex loggerMutex; const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe QString out = qFormatLogMessage(type, context, msg); out += QChar::LineFeed; PrismUpdaterApp* app = static_cast(QCoreApplication::instance()); app->logFile->write(out.toUtf8()); app->logFile->flush(); if (app->logToConsole) { QTextStream(stderr) << out.toLocal8Bit(); fflush(stderr); } } PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, argv) { setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME + "Updater"); setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); // Command line parsing QCommandLineParser parser; parser.setApplicationDescription(QObject::tr("An auto-updater for Prism Launcher")); parser.addOptions( { { { "d", "dir" }, tr("Use a custom path as application root (use '.' for current directory)."), tr("directory") }, { { "V", "prism-version" }, tr("Use this version as the installed launcher version. (provided because stdout can not be reliably captured on windows)"), tr("installed launcher version") }, { { "I", "install-version" }, "Install a specific version.", tr("version name") }, { { "U", "update-url" }, tr("Update from the specified repo."), tr("github repo url") }, { { "c", "check-only" }, tr("Only check if an update is needed. Exit status 100 if true, 0 if false (or non 0 if there was an error).") }, { { "p", "pre-release" }, tr("Allow updating to pre-release releases") }, { { "F", "force" }, tr("Force an update, even if one is not needed.") }, { { "l", "list" }, tr("List available releases.") }, { "debug", tr("Log debug to console.") }, { { "S", "select-ui" }, tr("Select the version to install with a GUI.") }, { { "D", "allow-downgrade" }, tr("Allow the updater to downgrade to previous versions.") } }); parser.addHelpOption(); parser.addVersionOption(); parser.process(arguments()); logToConsole = parser.isSet("debug"); QString origCwdPath = QDir::currentPath(); QString binPath = applicationDirPath(); { // find data director // Root path is used for updates and portable data #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr m_rootPath = foo.absolutePath(); #elif defined(Q_OS_WIN32) m_rootPath = binPath; #elif defined(Q_OS_MAC) QDir foo(FS::PathCombine(binPath, "../..")); m_rootPath = foo.absolutePath(); // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues) FS::updateTimestamp(m_rootPath); #endif } QString adjustedBy; // change folder QString dirParam = parser.value("dir"); if (!dirParam.isEmpty()) { // the dir param. it makes prism launcher data path point to whatever the user specified // on command line adjustedBy = "Command line"; m_dataPath = dirParam; #ifndef Q_OS_MACOS if (QDir(FS::PathCombine(m_rootPath, "UserData")).exists()) { m_isPortable = true; } if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { m_isPortable = true; } #endif } else if (auto dataDirEnv = QProcessEnvironment::systemEnvironment().value(QString("%1_DATA_DIR").arg(BuildConfig.LAUNCHER_NAME.toUpper())); !dataDirEnv.isEmpty()) { adjustedBy = "System environment"; m_dataPath = dataDirEnv; #ifndef Q_OS_MACOS if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { m_isPortable = true; } #endif } else { QDir foo(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); m_dataPath = foo.absolutePath(); adjustedBy = "Persistent data path"; #ifndef Q_OS_MACOS if (auto portableUserData = FS::PathCombine(m_rootPath, "UserData"); QDir(portableUserData).exists()) { m_dataPath = portableUserData; adjustedBy = "Portable user data path"; m_isPortable = true; } else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { m_dataPath = m_rootPath; adjustedBy = "Portable data path"; m_isPortable = true; } #endif } m_updateLogPath = FS::PathCombine(m_dataPath, "logs", "prism_launcher_update.log"); { // setup logging FS::ensureFolderPathExists(FS::PathCombine(m_dataPath, "logs")); static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "Updater" + (m_checkOnly ? "-CheckOnly" : "") + "-%0.log"; static const QString logBase = FS::PathCombine(m_dataPath, "logs", baseLogFile); if (FS::ensureFolderPathExists("logs")) { // enough history to track both launches of the updater during a portable install FS::move(logBase.arg(1), logBase.arg(2)); FS::move(logBase.arg(0), logBase.arg(1)); } logFile = std::unique_ptr(new QFile(logBase.arg(0))); if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { showFatalErrorMessage(tr("The launcher data folder is not writable!"), tr("The updater couldn't create a log file - %1.\n" "\n" "Make sure you have write permissions to the data folder.\n" "(%2)\n" "\n" "The updater cannot continue until you fix this problem.") .arg(logFile->errorString()) .arg(m_dataPath)); return; } qInstallMessageHandler(appDebugOutput); qSetMessagePattern( "%{time process}" " " "%{if-debug}D%{endif}" "%{if-info}I%{endif}" "%{if-warning}W%{endif}" "%{if-critical}C%{endif}" "%{if-fatal}F%{endif}" " " "|" " " "%{if-category}[%{category}]: %{endif}" "%{message}"); bool foundLoggingRules = false; auto logRulesFile = QStringLiteral("qtlogging.ini"); auto logRulesPath = FS::PathCombine(m_dataPath, logRulesFile); qDebug() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); // search the dataPath() // seach app data standard path if (!foundLoggingRules && !isPortable() && dirParam.isEmpty()) { logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); if (!logRulesPath.isEmpty()) { qDebug() << "Found" << logRulesPath << "..."; foundLoggingRules = true; } } // seach root path if (!foundLoggingRules) { logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); qDebug() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); } if (foundLoggingRules) { // load and set logging rules qDebug() << "Loading logging rules from:" << logRulesPath; QSettings loggingRules(logRulesPath, QSettings::IniFormat); loggingRules.beginGroup("Rules"); QStringList rule_names = loggingRules.childKeys(); QStringList rules; qDebug() << "Setting log rules:"; for (auto rule_name : rule_names) { auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString()); rules.append(rule); qDebug() << " " << rule; } auto rules_str = rules.join("\n"); QLoggingCategory::setFilterRules(rules_str); } qDebug() << "<> Log initialized."; } { // log debug program info qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + " Updater, " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); qDebug() << "Version :" << BuildConfig.printableVersionString(); qDebug() << "Git commit :" << BuildConfig.GIT_COMMIT; qDebug() << "Git refspec :" << BuildConfig.GIT_REFSPEC; qDebug() << "Compiled for :" << BuildConfig.systemID(); qDebug() << "Compiled by :" << BuildConfig.compilerID(); qDebug() << "Build Artifact :" << BuildConfig.BUILD_ARTIFACT; if (adjustedBy.size()) { qDebug() << "Data dir before adjustment :" << origCwdPath; qDebug() << "Data dir after adjustment :" << m_dataPath; qDebug() << "Adjusted by :" << adjustedBy; } else { qDebug() << "Data dir :" << QDir::currentPath(); } qDebug() << "Work dir :" << QDir::currentPath(); qDebug() << "Binary path :" << binPath; qDebug() << "Application root path :" << m_rootPath; qDebug() << "Portable install :" << m_isPortable; qDebug() << "<> Paths set."; } { // network m_network = std::make_unique(); qDebug() << "Detecting proxy settings..."; QNetworkProxy proxy = QNetworkProxy::applicationProxy(); m_network->setProxy(proxy); } #ifdef Q_OS_MACOS showFatalErrorMessage(tr("MacOS Not Supported"), tr("The updater does not support installations on MacOS")); #endif if (binPath.startsWith("/tmp/.mount_")) { m_isAppimage = true; m_appimagePath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); if (m_appimagePath.isEmpty()) { showFatalErrorMessage(tr("Unsupported Installation"), tr("Updater is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); } } m_isFlatpak = DesktopServices::isFlatpak(); QString prism_executable = FS::PathCombine(binPath, BuildConfig.LAUNCHER_APP_BINARY_NAME); #if defined Q_OS_WIN32 prism_executable.append(".exe"); #endif if (!QFileInfo(prism_executable).isFile()) { showFatalErrorMessage(tr("Unsupported Installation"), tr("The updater can not find the main executable.")); } m_prismExecutable = prism_executable; auto prism_update_url = parser.value("update-url"); if (prism_update_url.isEmpty()) prism_update_url = BuildConfig.UPDATER_GITHUB_REPO; m_prismRepoUrl = QUrl::fromUserInput(prism_update_url); m_checkOnly = parser.isSet("check-only"); m_forceUpdate = parser.isSet("force"); m_printOnly = parser.isSet("list"); auto user_version = parser.value("install-version"); if (!user_version.isEmpty()) { m_userSelectedVersion = Version(user_version); } m_selectUI = parser.isSet("select-ui"); m_allowDowngrade = parser.isSet("allow-downgrade"); auto version = parser.value("prism-version"); if (!version.isEmpty()) { if (version.contains('-')) { auto index = version.indexOf('-'); m_prsimVersionChannel = version.mid(index + 1); version = version.left(index); } else { m_prsimVersionChannel = "stable"; } auto version_parts = version.split('.'); m_prismVersionMajor = version_parts.takeFirst().toInt(); m_prismVersionMinor = version_parts.takeFirst().toInt(); if (!version_parts.isEmpty()) m_prismVersionPatch = version_parts.takeFirst().toInt(); else m_prismVersionPatch = 0; } m_allowPreRelease = parser.isSet("pre-release"); auto marker_file_path = QDir(m_rootPath).absoluteFilePath(".prism_launcher_updater_unpack.marker"); auto marker_file = QFileInfo(marker_file_path); if (marker_file.exists()) { auto target_dir = QString(FS::read(marker_file_path)).trimmed(); if (target_dir.isEmpty()) { qWarning() << "Empty updater marker file contains no install target. making best guess of parent dir"; target_dir = QDir(m_rootPath).absoluteFilePath(".."); } QMetaObject::invokeMethod(this, [this, target_dir]() { moveAndFinishUpdate(target_dir); }, Qt::QueuedConnection); } else { QMetaObject::invokeMethod(this, &PrismUpdaterApp::loadReleaseList, Qt::QueuedConnection); } } PrismUpdaterApp::~PrismUpdaterApp() { qDebug() << "updater shutting down"; // Shut down logger by setting the logger function to nothing qInstallMessageHandler(nullptr); } void PrismUpdaterApp::fail(const QString& reason) { qCritical() << qPrintable(reason); m_status = Failed; exit(1); } void PrismUpdaterApp::abort(const QString& reason) { qCritical() << qPrintable(reason); m_status = Aborted; exit(2); } void PrismUpdaterApp::showFatalErrorMessage(const QString& title, const QString& content) { m_status = Failed; auto msgBox = new QMessageBox(); msgBox->setWindowTitle(title); msgBox->setText(content); msgBox->setStandardButtons(QMessageBox::Ok); msgBox->setDefaultButton(QMessageBox::Ok); msgBox->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); msgBox->setIcon(QMessageBox::Critical); msgBox->setMinimumWidth(460); msgBox->adjustSize(); msgBox->exec(); exit(1); } void PrismUpdaterApp::run() { qDebug() << "found" << m_releases.length() << "releases on github"; qDebug() << "loading exe at" << m_prismExecutable; if (m_printOnly) { printReleases(); m_status = Succeeded; return exit(0); } if (!loadPrismVersionFromExe(m_prismExecutable)) { m_prismVersion = BuildConfig.printableVersionString(); m_prismVersionMajor = BuildConfig.VERSION_MAJOR; m_prismVersionMinor = BuildConfig.VERSION_MINOR; m_prismVersionPatch = BuildConfig.VERSION_PATCH; m_prsimVersionChannel = BuildConfig.VERSION_CHANNEL; m_prismGitCommit = BuildConfig.GIT_COMMIT; } m_status = Succeeded; qDebug() << "Executable reports as:" << m_prismBinaryName << "version:" << m_prismVersion; qDebug() << "Version major:" << m_prismVersionMajor; qDebug() << "Version minor:" << m_prismVersionMinor; qDebug() << "Version minor:" << m_prismVersionPatch; qDebug() << "Version channel:" << m_prsimVersionChannel; qDebug() << "Git Commit:" << m_prismGitCommit; auto latest = getLatestRelease(); qDebug() << "Latest release" << latest.version; auto need_update = needUpdate(latest); if (m_checkOnly) { if (need_update) { QTextStream stdOutStream(stdout); stdOutStream << "Name: " << latest.name << "\n"; stdOutStream << "Version: " << latest.tag_name << "\n"; stdOutStream << "TimeStamp: " << latest.created_at.toString(Qt::ISODate) << "\n"; stdOutStream << latest.body << "\n"; stdOutStream.flush(); return exit(100); } else { return exit(0); } } if (m_isFlatpak) { showFatalErrorMessage(tr("Updating flatpack not supported"), tr("Actions outside of checking if an update is available are not " "supported when running the flatpak version of Prism Launcher.")); return; } if (m_isAppimage) { bool result = true; if (need_update) result = callAppImageUpdate(); return exit(result ? 0 : 1); } if (need_update || m_forceUpdate || !m_userSelectedVersion.isEmpty()) { GitHubRelease update_release = latest; if (!m_userSelectedVersion.isEmpty()) { bool found = false; for (auto rls : m_releases) { if (rls.version == m_userSelectedVersion) { found = true; update_release = rls; break; } } if (!found) { showFatalErrorMessage( "No release for version!", QString("Can not find a github release for specified version %1").arg(m_userSelectedVersion.toString())); return; } } else if (m_selectUI) { update_release = selectRelease(); if (!update_release.isValid()) { showFatalErrorMessage("No version selected.", "No version was selected."); return; } } performUpdate(update_release); } exit(0); } void PrismUpdaterApp::moveAndFinishUpdate(QDir target) { logUpdate("Finishing update process"); logUpdate("Waiting 2 seconds for resources to free"); this->thread()->sleep(2); auto manifest_path = FS::PathCombine(m_rootPath, "manifest.txt"); QFileInfo manifest(manifest_path); auto app_dir = QDir(m_rootPath); QStringList file_list; if (manifest.isFile()) { // load manifest from file logUpdate(tr("Reading manifest from %1").arg(manifest.absoluteFilePath())); try { auto contents = QString::fromUtf8(FS::read(manifest.absoluteFilePath())); auto files = contents.split('\n'); for (auto file : files) { file_list.append(file.trimmed()); } } catch (FS::FileSystemException&) { } } if (file_list.isEmpty()) { logUpdate(tr("Manifest empty, making best guess of the directory contents of %1").arg(m_rootPath)); auto entries = target.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); for (auto entry : entries) { file_list.append(entry.fileName()); } } logUpdate(tr("Installing the following to %1 :\n %2").arg(target.absolutePath()).arg(file_list.join(",\n "))); bool error = false; QProgressDialog progress(tr("Installing from %1").arg(m_rootPath), "", 0, file_list.length()); progress.setCancelButton(nullptr); progress.setMinimumWidth(400); progress.adjustSize(); progress.show(); QCoreApplication::processEvents(); logUpdate(tr("Installing from %1").arg(m_rootPath)); auto copy = [this, app_dir, target](QString to_install_file) { auto rel_path = app_dir.relativeFilePath(to_install_file); auto install_path = FS::PathCombine(target.absolutePath(), rel_path); logUpdate(tr("Installing %1 from %2").arg(install_path).arg(to_install_file)); FS::ensureFilePathExists(install_path); auto result = FS::copy(to_install_file, install_path).overwrite(true)(); if (!result) { logUpdate(tr("Failed copy %1 to %2").arg(to_install_file).arg(install_path)); return true; } return false; }; int i = 0; for (auto glob : file_list) { QDirIterator iter(m_rootPath, QStringList({ glob }), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); progress.setValue(i); QCoreApplication::processEvents(); if (!iter.hasNext() && !glob.isEmpty()) { if (auto file_info = QFileInfo(FS::PathCombine(m_rootPath, glob)); file_info.exists()) { error |= copy(file_info.absoluteFilePath()); } else { logUpdate(tr("File doesn't exist, ignoring: %1").arg(FS::PathCombine(m_rootPath, glob))); } } else { while (iter.hasNext()) { error |= copy(iter.next()); } } i++; } progress.setValue(i); QCoreApplication::processEvents(); if (error) { logUpdate(tr("There were errors installing the update.")); auto fail_marker = FS::PathCombine(m_dataPath, ".prism_launcher_update.fail"); FS::copy(m_updateLogPath, fail_marker).overwrite(true)(); } else { logUpdate(tr("Update succeed.")); auto success_marker = FS::PathCombine(m_dataPath, ".prism_launcher_update.success"); FS::copy(m_updateLogPath, success_marker).overwrite(true)(); } auto update_lock_path = FS::PathCombine(m_dataPath, ".prism_launcher_update.lock"); FS::deletePath(update_lock_path); QProcess proc; auto app_exe_name = BuildConfig.LAUNCHER_APP_BINARY_NAME; #if defined Q_OS_WIN32 app_exe_name.append(".exe"); auto env = QProcessEnvironment::systemEnvironment(); env.insert("__COMPAT_LAYER", "RUNASINVOKER"); proc.setProcessEnvironment(env); #else app_exe_name.prepend("bin/"); #endif auto app_exe_path = target.absoluteFilePath(app_exe_name); proc.startDetached(app_exe_path); exit(error ? 1 : 0); } void PrismUpdaterApp::printReleases() { for (auto release : m_releases) { std::cout << release.name.toStdString() << " Version: " << release.tag_name.toStdString() << std::endl; } } QList PrismUpdaterApp::nonDraftReleases() { QList nonDraft; for (auto rls : m_releases) { if (rls.isValid() && !rls.draft) nonDraft.append(rls); } return nonDraft; } QList PrismUpdaterApp::newerReleases() { QList newer; for (auto rls : nonDraftReleases()) { if (rls.version > m_prismVersion) newer.append(rls); } return newer; } GitHubRelease PrismUpdaterApp::selectRelease() { QList releases; if (m_allowDowngrade) { releases = nonDraftReleases(); } else { releases = newerReleases(); } if (releases.isEmpty()) return {}; SelectReleaseDialog dlg(Version(m_prismVersion), releases); auto result = dlg.exec(); if (result == QDialog::Rejected) { return {}; } GitHubRelease release = dlg.selectedRelease(); return release; } QList PrismUpdaterApp::validReleaseArtifacts(const GitHubRelease& release) { QList valid; qDebug() << "Selecting best asset from" << release.tag_name << "for platform" << BuildConfig.BUILD_ARTIFACT << "portable:" << m_isPortable; if (BuildConfig.BUILD_ARTIFACT.isEmpty()) qWarning() << "Build platform is not set!"; for (auto asset : release.assets) { if (asset.name.endsWith(".zsync")) { qDebug() << "Rejecting zsync file" << asset.name; continue; } if (!m_isAppimage && asset.name.toLower().endsWith("appimage")) { qDebug() << "Rejecting" << asset.name << "because it is an AppImage"; continue; } else if (m_isAppimage && !asset.name.toLower().endsWith("appimage")) { qDebug() << "Rejecting" << asset.name << "because it is not an AppImage"; continue; } auto asset_name = asset.name.toLower(); auto [platform, platform_qt_ver] = StringUtils::splitFirst(BuildConfig.BUILD_ARTIFACT.toLower(), "-qt"); auto system_is_arm = QSysInfo::buildCpuArchitecture().contains("arm64"); auto asset_is_arm = asset_name.contains("arm64"); auto asset_is_archive = asset_name.endsWith(".zip") || asset_name.endsWith(".tar.gz"); bool for_platform = !platform.isEmpty() && asset_name.contains(platform); if (!for_platform) { qDebug() << "Rejecting" << asset.name << "because platforms do not match"; } bool for_portable = asset_name.contains("portable"); if (for_platform && asset_name.contains("legacy") && !platform.contains("legacy")) { qDebug() << "Rejecting" << asset.name << "because platforms do not match"; for_platform = false; } if (for_platform && ((asset_is_arm && !system_is_arm) || (!asset_is_arm && system_is_arm))) { qDebug() << "Rejecting" << asset.name << "because architecture does not match"; for_platform = false; } if (for_platform && platform.contains("windows") && !m_isPortable && asset_is_archive) { qDebug() << "Rejecting" << asset.name << "because it is not an installer"; for_platform = false; } static const QRegularExpression s_qtPattern("-qt(\\d+)"); auto qt_match = s_qtPattern.match(asset_name); if (for_platform && qt_match.hasMatch()) { if (platform_qt_ver.isEmpty() || platform_qt_ver.toInt() != qt_match.captured(1).toInt()) { qDebug() << "Rejecting" << asset.name << "because it is not for the correct qt version" << platform_qt_ver.toInt() << "vs" << qt_match.captured(1).toInt(); for_platform = false; } } if (((m_isPortable && for_portable) || (!m_isPortable && !for_portable)) && for_platform) { qDebug() << "Accepting" << asset.name; valid.append(asset); } } return valid; } GitHubReleaseAsset PrismUpdaterApp::selectAsset(const QList& assets) { SelectReleaseAssetDialog dlg(assets); auto result = dlg.exec(); if (result == QDialog::Rejected) { return {}; } GitHubReleaseAsset asset = dlg.selectedAsset(); return asset; } void PrismUpdaterApp::performUpdate(const GitHubRelease& release) { m_install_release = release; qDebug() << "Updating to" << release.tag_name; auto valid_assets = validReleaseArtifacts(release); qDebug() << "valid release assets:" << valid_assets; GitHubReleaseAsset selected_asset; if (valid_assets.isEmpty()) { return showFatalErrorMessage( tr("No Valid Release Assets"), tr("Github release %1 has no valid assets for this platform: %2") .arg(release.tag_name) .arg(tr("%1 portable: %2").arg(BuildConfig.BUILD_ARTIFACT).arg(m_isPortable ? tr("yes") : tr("no")))); } else if (valid_assets.length() > 1) { selected_asset = selectAsset(valid_assets); } else { selected_asset = valid_assets.takeFirst(); } if (!selected_asset.isValid()) { return showFatalErrorMessage(tr("No version selected."), tr("No version was selected.")); } qDebug() << "will install" << selected_asset; auto file = downloadAsset(selected_asset); if (!file.exists()) { return showFatalErrorMessage(tr("Failed to Download"), tr("Failed to download the selected asset.")); } performInstall(file); } QFileInfo PrismUpdaterApp::downloadAsset(const GitHubReleaseAsset& asset) { auto temp_dir = QDir::tempPath(); auto file_url = QUrl(asset.browser_download_url); auto out_file_path = FS::PathCombine(temp_dir, file_url.fileName()); qDebug() << "downloading" << file_url << "to" << out_file_path; auto download = Net::Download::makeFile(file_url, out_file_path); download->setNetwork(m_network.get()); auto progress_dialog = ProgressDialog(); progress_dialog.adjustSize(); progress_dialog.execWithTask(download.get()); qDebug() << "download complete"; QFileInfo out_file(out_file_path); return out_file; } bool PrismUpdaterApp::callAppImageUpdate() { auto appimage_path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); QProcess proc = QProcess(); qDebug() << "Calling: AppImageUpdate" << appimage_path; proc.setProgram(FS::PathCombine(m_rootPath, "bin", "AppImageUpdate.AppImage")); proc.setArguments({ appimage_path }); auto result = proc.startDetached(); if (!result) qDebug() << "Failed to start AppImageUpdate reason:" << proc.errorString(); return result; } void PrismUpdaterApp::clearUpdateLog() { FS::deletePath(m_updateLogPath); } void PrismUpdaterApp::logUpdate(const QString& msg) { qDebug() << qUtf8Printable(msg); FS::append(m_updateLogPath, QStringLiteral("%1\n").arg(msg).toUtf8()); } std::tuple read_lock_File(const QString& path) { auto contents = QString(FS::read(path)); auto lines = contents.split('\n'); QDateTime timestamp; QString from, to, target, data_path; for (auto line : lines) { auto index = line.indexOf("="); if (index < 0) continue; auto left = line.left(index); auto right = line.mid(index + 1); if (left.toLower() == "timestamp") { timestamp = QDateTime::fromString(right, Qt::ISODate); } else if (left.toLower() == "from") { from = right; } else if (left.toLower() == "to") { to = right; } else if (left.toLower() == "target") { target = right; } else if (left.toLower() == "data_path") { data_path = right; } } return std::make_tuple(timestamp, from, to, target, data_path); } bool write_lock_file(const QString& path, QDateTime timestamp, QString from, QString to, QString target, QString data_path) { try { FS::write(path, QStringLiteral("TIMESTAMP=%1\nFROM=%2\nTO=%3\nTARGET=%4\nDATA_PATH=%5\n") .arg(timestamp.toString(Qt::ISODate)) .arg(from) .arg(to) .arg(target) .arg(data_path) .toUtf8()); } catch (FS::FileSystemException& err) { qWarning() << "Error writing lockfile:" << err.what() << "\n" << err.cause(); return false; } return true; } void PrismUpdaterApp::performInstall(QFileInfo file) { qDebug() << "starting install"; auto update_lock_path = FS::PathCombine(m_dataPath, ".prism_launcher_update.lock"); QFileInfo update_lock(update_lock_path); if (update_lock.exists()) { auto [timestamp, from, to, target, data_path] = read_lock_File(update_lock_path); auto msg = tr("Update already in progress\n"); auto infoMsg = tr("This installation has a update lock file present at: %1\n" "\n" "Timestamp: %2\n" "Updating from version %3 to %4\n" "Target install path: %5\n" "Data Path: %6" "\n" "This likely means that a previous update attempt failed. Please ensure your installation is in working order before " "proceeding.\n" "Check the Prism Launcher updater log at: \n" "%7\n" "for details on the last update attempt.\n" "\n" "To overwrite this lock and proceed with this update anyway, select \"Ignore\" below.") .arg(update_lock_path) .arg(timestamp.toString(Qt::ISODate), from, to, target, data_path) .arg(m_updateLogPath); QMessageBox msgBox; msgBox.setText(msg); msgBox.setInformativeText(infoMsg); msgBox.setStandardButtons(QMessageBox::Ignore | QMessageBox::Cancel); msgBox.setDefaultButton(QMessageBox::Cancel); msgBox.setMinimumWidth(460); msgBox.adjustSize(); switch (msgBox.exec()) { case QMessageBox::AcceptRole: break; case QMessageBox::RejectRole: [[fallthrough]]; default: return showFatalErrorMessage(tr("Update Aborted"), tr("The update attempt was aborted")); } } clearUpdateLog(); auto changelog_path = FS::PathCombine(m_dataPath, ".prism_launcher_update.changelog"); FS::write(changelog_path, m_install_release.body.toUtf8()); logUpdate(tr("Updating from %1 to %2").arg(m_prismVersion).arg(m_install_release.tag_name)); if (m_isPortable || file.fileName().endsWith(".zip") || file.fileName().endsWith(".tar.gz")) { write_lock_file(update_lock_path, QDateTime::currentDateTime(), m_prismVersion, m_install_release.tag_name, m_rootPath, m_dataPath); logUpdate(tr("Updating portable install at %1").arg(m_rootPath)); unpackAndInstall(file); } else { logUpdate(tr("Running installer file at %1").arg(file.absoluteFilePath())); QProcess proc = QProcess(); #if defined Q_OS_WIN auto env = QProcessEnvironment::systemEnvironment(); env.insert("__COMPAT_LAYER", "RUNASINVOKER"); proc.setProcessEnvironment(env); #endif proc.setProgram(file.absoluteFilePath()); bool result = proc.startDetached(); logUpdate(tr("Process start result: %1").arg(result ? tr("yes") : tr("no"))); exit(result ? 0 : 1); } } void PrismUpdaterApp::unpackAndInstall(QFileInfo archive) { logUpdate(tr("Backing up install")); backupAppDir(); if (auto loc = unpackArchive(archive)) { auto marker_file_path = loc.value().absoluteFilePath(".prism_launcher_updater_unpack.marker"); FS::write(marker_file_path, m_rootPath.toUtf8()); QProcess proc = QProcess(); auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); #if defined Q_OS_WIN32 exe_name.append(".exe"); auto env = QProcessEnvironment::systemEnvironment(); env.insert("__COMPAT_LAYER", "RUNASINVOKER"); proc.setProcessEnvironment(env); #else exe_name.prepend("bin/"); #endif auto new_updater_path = loc.value().absoluteFilePath(exe_name); logUpdate(tr("Starting new updater at '%1'").arg(new_updater_path)); if (!proc.startDetached(new_updater_path, { "-d", m_dataPath }, loc.value().absolutePath())) { logUpdate(tr("Failed to launch '%1' %2").arg(new_updater_path).arg(proc.errorString())); return exit(10); } return exit(); // up to the new updater now } return exit(1); // unpack failure } void PrismUpdaterApp::backupAppDir() { auto manifest_path = FS::PathCombine(m_rootPath, "manifest.txt"); QFileInfo manifest(manifest_path); QStringList file_list; if (manifest.isFile()) { // load manifest from file logUpdate(tr("Reading manifest from %1").arg(manifest.absoluteFilePath())); try { auto contents = QString::fromUtf8(FS::read(manifest.absoluteFilePath())); auto files = contents.split('\n'); for (auto file : files) { file_list.append(file.trimmed()); } } catch (FS::FileSystemException&) { } } if (file_list.isEmpty()) { // best guess if (BuildConfig.BUILD_ARTIFACT.toLower().contains("linux")) { file_list.append({ "PrismLauncher", "bin", "share", "lib" }); } else { // windows by process of elimination file_list.append({ "jars", "prismlauncher.exe", "prismlauncher_filelink.exe", "prismlauncher_updater.exe", "qtlogging.ini", "imageformats", "iconengines", "platforms", "styles", "tls", "qt.conf", "Qt*.dll", }); } logUpdate("manifest.txt empty or missing. making best guess at files to back up."); } logUpdate(tr("Backing up:\n %1").arg(file_list.join(",\n "))); static const QRegularExpression s_replaceRegex("[" + QRegularExpression::escape("\\/:*?\"<>|") + "]"); auto app_dir = QDir(m_rootPath); auto backup_dir = FS::PathCombine(app_dir.absolutePath(), QStringLiteral("backup_") + QString(m_prismVersion).replace(s_replaceRegex, QString("_")) + "-" + m_prismGitCommit); FS::ensureFolderPathExists(backup_dir); auto backup_marker_path = FS::PathCombine(m_dataPath, ".prism_launcher_update_backup_path.txt"); FS::write(backup_marker_path, backup_dir.toUtf8()); QProgressDialog progress(tr("Backing up install at %1").arg(m_rootPath), "", 0, file_list.length()); progress.setCancelButton(nullptr); progress.setMinimumWidth(400); progress.adjustSize(); progress.show(); QCoreApplication::processEvents(); logUpdate(tr("Backing up install at %1").arg(m_rootPath)); auto copy = [this, app_dir, backup_dir](QString to_bak_file) { auto rel_path = app_dir.relativeFilePath(to_bak_file); auto bak_path = FS::PathCombine(backup_dir, rel_path); logUpdate(tr("Backing up and then removing %1").arg(to_bak_file)); FS::ensureFilePathExists(bak_path); auto result = FS::copy(to_bak_file, bak_path).overwrite(true)(); if (!result) { logUpdate(tr("Failed to backup %1 to %2").arg(to_bak_file).arg(bak_path)); } else { if (!FS::deletePath(to_bak_file)) logUpdate(tr("Failed to remove %1").arg(to_bak_file)); } }; int i = 0; for (auto glob : file_list) { QDirIterator iter(app_dir.absolutePath(), QStringList({ glob }), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); progress.setValue(i); QCoreApplication::processEvents(); if (!iter.hasNext() && !glob.isEmpty()) { if (auto file_info = QFileInfo(FS::PathCombine(app_dir.absolutePath(), glob)); file_info.exists()) { copy(file_info.absoluteFilePath()); } else { logUpdate(tr("File doesn't exist, ignoring: %1").arg(FS::PathCombine(app_dir.absolutePath(), glob))); } } else { while (iter.hasNext()) { copy(iter.next()); } } i++; } progress.setValue(i); QCoreApplication::processEvents(); } std::optional PrismUpdaterApp::unpackArchive(QFileInfo archive) { auto temp_extract_path = FS::PathCombine(m_dataPath, "prism_launcher_update_release"); FS::ensureFolderPathExists(temp_extract_path); auto tmp_extract_dir = QDir(temp_extract_path); auto result = MMCZip::extractDir(archive.absoluteFilePath(), tmp_extract_dir.absolutePath()); if (result) { logUpdate(tr("Extracted the following to \"%1\":\n %2").arg(tmp_extract_dir.absolutePath()).arg(result->join("\n "))); } else { logUpdate(tr("Failed to extract %1 to %2").arg(archive.absoluteFilePath()).arg(tmp_extract_dir.absolutePath())); showFatalErrorMessage("Failed to extract archive", tr("Failed to extract %1 to %2").arg(archive.absoluteFilePath()).arg(tmp_extract_dir.absolutePath())); return std::nullopt; } return tmp_extract_dir; } bool PrismUpdaterApp::loadPrismVersionFromExe(const QString& exe_path) { QProcess proc = QProcess(); proc.setProcessChannelMode(QProcess::MergedChannels); proc.setReadChannel(QProcess::StandardOutput); proc.start(exe_path, { "--version" }); if (!proc.waitForStarted(5000)) { showFatalErrorMessage(tr("Failed to Check Version"), tr("Failed to launch child process to read version.")); return false; } // wait 5 seconds to start if (!proc.waitForFinished(5000)) { showFatalErrorMessage(tr("Failed to Check Version"), tr("Child launcher process failed.")); return false; } auto out = proc.readAllStandardOutput(); auto lines = out.split('\n'); lines.removeAll(""); if (lines.length() < 2) return false; else if (lines.length() > 2) { auto line1 = lines.takeLast(); auto line2 = lines.takeLast(); lines = { line2, line1 }; } auto first = lines.takeFirst(); auto first_parts = first.split(' '); if (first_parts.length() < 2) return false; m_prismBinaryName = first_parts.takeFirst(); auto version = first_parts.takeFirst().trimmed(); m_prismVersion = version; if (version.contains('-')) { auto index = version.indexOf('-'); m_prsimVersionChannel = version.mid(index + 1); version = version.left(index); } else { m_prsimVersionChannel = "stable"; } auto version_parts = version.split('.'); if (version_parts.length() < 2) return false; m_prismVersionMajor = version_parts.takeFirst().toInt(); m_prismVersionMinor = version_parts.takeFirst().toInt(); if (!version_parts.isEmpty()) m_prismVersionPatch = version_parts.takeFirst().toInt(); else m_prismVersionPatch = 0; m_prismGitCommit = lines.takeFirst().simplified(); return true; } void PrismUpdaterApp::loadReleaseList() { auto github_repo = m_prismRepoUrl; if (github_repo.host() != "github.com") return fail("updating from a non github url is not supported"); auto path_parts = github_repo.path().split('/'); path_parts.removeFirst(); // empty segment from leading / auto repo_owner = path_parts.takeFirst(); auto repo_name = path_parts.takeFirst(); auto api_url = QString("https://api.github.com/repos/%1/%2/releases").arg(repo_owner, repo_name); qDebug() << "Fetching release list from" << api_url; downloadReleasePage(api_url, 1); } void PrismUpdaterApp::downloadReleasePage(const QString& api_url, int page) { int per_page = 30; auto page_url = QString("%1?per_page=%2&page=%3").arg(api_url).arg(QString::number(per_page)).arg(QString::number(page)); auto [download, response] = Net::Download::makeByteArray(page_url); download->setNetwork(m_network.get()); m_current_url = page_url; auto github_api_headers = std::make_unique(); github_api_headers->addHeaders({ { "Accept", "application/vnd.github+json" }, { "X-GitHub-Api-Version", "2022-11-28" }, }); download->addHeaderProxy(std::move(github_api_headers)); connect(download.get(), &Net::Download::succeeded, this, [this, response, per_page, api_url, page]() { int num_found = parseReleasePage(response); if (!(num_found < per_page)) { // there may be more, fetch next page downloadReleasePage(api_url, page + 1); } else { run(); } }); connect(download.get(), &Net::Download::failed, this, &PrismUpdaterApp::downloadError); m_current_task.reset(download); connect(download.get(), &Net::Download::finished, this, [this]() { qDebug() << "Download" << m_current_task->getUid().toString() << "finished"; }); QCoreApplication::processEvents(); QMetaObject::invokeMethod(download.get(), &Task::start, Qt::QueuedConnection); } int PrismUpdaterApp::parseReleasePage(const QByteArray* response) { if (response->isEmpty()) // empty page return 0; int num_releases = 0; try { auto doc = Json::requireDocument(*response); auto release_list = Json::requireArray(doc); for (auto release_json : release_list) { auto release_obj = Json::requireObject(release_json); GitHubRelease release = {}; release.id = Json::requireInteger(release_obj, "id"); release.name = release_obj["name"].toString(); release.tag_name = Json::requireString(release_obj, "tag_name"); release.created_at = QDateTime::fromString(Json::requireString(release_obj, "created_at"), Qt::ISODate); release.published_at = QDateTime::fromString(release_obj["published_at"].toString(), Qt::ISODate); release.draft = Json::requireBoolean(release_obj, "draft"); release.prerelease = Json::requireBoolean(release_obj, "prerelease"); release.body = release_obj["body"].toString(); release.version = Version(release.tag_name); auto release_assets_obj = Json::requireArray(release_obj, "assets"); for (auto asset_json : release_assets_obj) { auto asset_obj = Json::requireObject(asset_json); GitHubReleaseAsset asset = {}; asset.id = Json::requireInteger(asset_obj, "id"); asset.name = Json::requireString(asset_obj, "name"); asset.label = asset_obj["label"].toString(); asset.content_type = Json::requireString(asset_obj, "content_type"); asset.size = Json::requireInteger(asset_obj, "size"); asset.created_at = QDateTime::fromString(Json::requireString(asset_obj, "created_at"), Qt::ISODate); asset.updated_at = QDateTime::fromString(Json::requireString(asset_obj, "updated_at"), Qt::ISODate); asset.browser_download_url = Json::requireString(asset_obj, "browser_download_url"); release.assets.append(asset); } m_releases.append(release); num_releases++; } } catch (Json::JsonException& e) { auto err_msg = QString("Failed to parse releases from github: %1\n%2").arg(e.what()).arg(QString::fromStdString(response->toStdString())); fail(err_msg); } return num_releases; } GitHubRelease PrismUpdaterApp::getLatestRelease() { GitHubRelease latest; for (auto release : m_releases) { if (release.draft) continue; if (release.prerelease && !m_allowPreRelease) continue; if (!latest.isValid() || (release.version > latest.version)) { latest = release; } } return latest; } bool PrismUpdaterApp::needUpdate(const GitHubRelease& release) { auto current_ver = Version(QString("%1.%2.%3").arg(m_prismVersionMajor).arg(m_prismVersionMinor).arg(m_prismVersionPatch)); return current_ver < release.version; } void PrismUpdaterApp::downloadError(QString reason) { fail(QString("Network request Failed: %1 with reason %2").arg(m_current_url).arg(reason)); } PrismLauncher-11.0.3/launcher/updater/prismupdater/UpdaterDialogs.h0000644000175100017510000000442015224505336025027 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include #include "GitHubRelease.h" #include "Version.h" namespace Ui { class SelectReleaseDialog; } class SelectReleaseDialog : public QDialog { Q_OBJECT public: explicit SelectReleaseDialog(const Version& cur_version, const QList& releases, QWidget* parent = 0); ~SelectReleaseDialog(); void loadReleases(); void appendRelease(GitHubRelease const& release); GitHubRelease selectedRelease() { return m_selectedRelease; } private slots: GitHubRelease getRelease(QTreeWidgetItem* item); void selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous); protected: QList m_releases; GitHubRelease m_selectedRelease; Version m_currentVersion; Ui::SelectReleaseDialog* ui; }; class SelectReleaseAssetDialog : public QDialog { Q_OBJECT public: explicit SelectReleaseAssetDialog(const QList& assets, QWidget* parent = 0); ~SelectReleaseAssetDialog(); void loadAssets(); void appendAsset(GitHubReleaseAsset const& asset); GitHubReleaseAsset selectedAsset() { return m_selectedAsset; } private slots: GitHubReleaseAsset getAsset(QTreeWidgetItem* item); void selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous); protected: QList m_assets; GitHubReleaseAsset m_selectedAsset; Ui::SelectReleaseDialog* ui; }; PrismLauncher-11.0.3/launcher/updater/prismupdater/updater_main.cpp0000644000175100017510000000273015224505336025125 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "PrismUpdater.h" #if defined Q_OS_WIN32 #include "console/WindowsConsole.h" #endif int main(int argc, char* argv[]) { #if defined Q_OS_WIN32 // attach the parent console if stdout not already captured console::WindowsConsoleGuard _consoleGuard; #endif PrismUpdaterApp wUpApp(argc, argv); switch (wUpApp.status()) { case PrismUpdaterApp::Starting: case PrismUpdaterApp::Initialized: { return wUpApp.exec(); } case PrismUpdaterApp::Failed: return 1; case PrismUpdaterApp::Succeeded: return 0; default: return -1; } } PrismLauncher-11.0.3/launcher/updater/prismupdater/updater.exe.manifest0000644000175100017510000000173615224505336025732 0ustar runnerrunner PrismLauncher-11.0.3/launcher/updater/prismupdater/UpdaterDialogs.cpp0000644000175100017510000001362115224505336025365 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "UpdaterDialogs.h" #include "ui_SelectReleaseDialog.h" #include #include #include "Markdown.h" #include "StringUtils.h" SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, const QList& releases, QWidget* parent) : QDialog(parent), m_releases(releases), m_currentVersion(current_version), ui(new Ui::SelectReleaseDialog) { ui->setupUi(this); ui->changelogTextBrowser->setOpenExternalLinks(true); ui->changelogTextBrowser->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); ui->changelogTextBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); ui->versionsTree->setColumnCount(2); ui->versionsTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); ui->versionsTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); ui->versionsTree->setHeaderLabels({ tr("Version"), tr("Published Date") }); ui->versionsTree->header()->setStretchLastSection(false); ui->eplainLabel->setText(tr("Select a version to install.\n" "\n" "Currently installed version: %1") .arg(m_currentVersion.toString())); loadReleases(); connect(ui->versionsTree, &QTreeWidget::currentItemChanged, this, &SelectReleaseDialog::selectionChanged); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectReleaseDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectReleaseDialog::reject); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } SelectReleaseDialog::~SelectReleaseDialog() { delete ui; } void SelectReleaseDialog::loadReleases() { for (auto rls : m_releases) { appendRelease(rls); } } void SelectReleaseDialog::appendRelease(GitHubRelease const& release) { auto rls_item = new QTreeWidgetItem(ui->versionsTree); rls_item->setText(0, release.tag_name); rls_item->setExpanded(true); rls_item->setText(1, release.published_at.toString()); rls_item->setData(0, Qt::UserRole, QVariant(release.id)); ui->versionsTree->addTopLevelItem(rls_item); } GitHubRelease SelectReleaseDialog::getRelease(QTreeWidgetItem* item) { int id = item->data(0, Qt::UserRole).toInt(); GitHubRelease release; for (auto rls : m_releases) { if (rls.id == id) release = rls; } return release; } void SelectReleaseDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* /*previous*/) { GitHubRelease release = getRelease(current); QString body = markdownToHTML(release.body.toUtf8()); m_selectedRelease = release; ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(body)); } SelectReleaseAssetDialog::SelectReleaseAssetDialog(const QList& assets, QWidget* parent) : QDialog(parent), m_assets(assets), ui(new Ui::SelectReleaseDialog) { ui->setupUi(this); ui->changelogTextBrowser->setOpenExternalLinks(true); ui->changelogTextBrowser->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); ui->changelogTextBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); ui->versionsTree->setColumnCount(2); ui->versionsTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); ui->versionsTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); ui->versionsTree->setHeaderLabels({ tr("Version"), tr("Published Date") }); ui->versionsTree->header()->setStretchLastSection(false); ui->eplainLabel->setText(tr("Select a version to install.")); ui->changelogTextBrowser->setHidden(true); loadAssets(); connect(ui->versionsTree, &QTreeWidget::currentItemChanged, this, &SelectReleaseAssetDialog::selectionChanged); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectReleaseAssetDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectReleaseAssetDialog::reject); } SelectReleaseAssetDialog::~SelectReleaseAssetDialog() { delete ui; } void SelectReleaseAssetDialog::loadAssets() { for (auto rls : m_assets) { appendAsset(rls); } } void SelectReleaseAssetDialog::appendAsset(GitHubReleaseAsset const& asset) { auto rls_item = new QTreeWidgetItem(ui->versionsTree); rls_item->setText(0, asset.name); rls_item->setExpanded(true); rls_item->setText(1, asset.updated_at.toString()); rls_item->setData(0, Qt::UserRole, QVariant(asset.id)); ui->versionsTree->addTopLevelItem(rls_item); } GitHubReleaseAsset SelectReleaseAssetDialog::getAsset(QTreeWidgetItem* item) { int id = item->data(0, Qt::UserRole).toInt(); GitHubReleaseAsset selected_asset; for (auto asset : m_assets) { if (asset.id == id) selected_asset = asset; } return selected_asset; } void SelectReleaseAssetDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* /*previous*/) { GitHubReleaseAsset asset = getAsset(current); m_selectedAsset = asset; } PrismLauncher-11.0.3/launcher/updater/prismupdater/GitHubRelease.cpp0000644000175100017510000000575515224505336025152 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "GitHubRelease.h" QDebug operator<<(QDebug debug, const GitHubReleaseAsset& asset) { QDebugStateSaver saver(debug); debug.nospace() << "GitHubReleaseAsset( " "id: " << asset.id << ", " "name " << asset.name << ", " "label: " << asset.label << ", " "content_type: " << asset.content_type << ", " "size: " << asset.size << ", " "created_at: " << asset.created_at << ", " "updated_at: " << asset.updated_at << ", " "browser_download_url: " << asset.browser_download_url << " " ")"; return debug; } QDebug operator<<(QDebug debug, const GitHubRelease& rls) { QDebugStateSaver saver(debug); debug.nospace() << "GitHubRelease( " "id: " << rls.id << ", " "name " << rls.name << ", " "tag_name: " << rls.tag_name << ", " "created_at: " << rls.created_at << ", " "published_at: " << rls.published_at << ", " "prerelease: " << rls.prerelease << ", " "draft: " << rls.draft << ", " "version" << rls.version << ", " "body: " << rls.body << ", " "assets: " << rls.assets << " " ")"; return debug; } PrismLauncher-11.0.3/launcher/updater/prismupdater/SelectReleaseDialog.ui0000644000175100017510000000412415224505336026147 0ustar runnerrunner SelectReleaseDialog 0 0 468 385 Select Release to Install true Please select the release you wish to update to. true 1 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() SelectReleaseDialog accept() 248 254 157 274 buttonBox rejected() SelectReleaseDialog reject() 316 260 286 274 PrismLauncher-11.0.3/launcher/updater/PrismExternalUpdater.h0000644000175100017510000000604015224505336023523 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include "ExternalUpdater.h" /*! * An implementation for the updater on Windows and linux that uses out external updater. */ class PrismExternalUpdater : public ExternalUpdater { Q_OBJECT public: PrismExternalUpdater(QWidget* parent, const QString& appDir, const QString& dataDir); ~PrismExternalUpdater() override; /*! * Check for updates manually, showing the user a progress bar and an alert if no updates are found. */ void checkForUpdates() override; void checkForUpdates(bool triggeredByUser) const; /*! * Indicates whether or not to check for updates automatically. */ bool getAutomaticallyChecksForUpdates() override; /*! * Indicates the current automatic update check interval in seconds. */ double getUpdateCheckInterval() override; /*! * Indicates whether or not beta updates should be checked for in addition to regular releases. */ bool getBetaAllowed() override; /*! * Set whether or not to check for updates automatically. * * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow * reverting this property without kicking off a schedule change immediately. */ void setAutomaticallyChecksForUpdates(bool check) override; /*! * Set the current automatic update check interval in seconds. * * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow * reverting this property without kicking off a schedule change immediately. */ void setUpdateCheckInterval(double seconds) override; /*! * Set whether or not beta updates should be checked for in addition to regular releases. */ void setBetaAllowed(bool allowed) override; void resetAutoCheckTimer() const; void disconnectTimer(); void connectTimer(); void offerUpdate(const QString& versionName, const QString& versionTag, const QString& releaseNotes, bool triggeredByUser) const; void performUpdate(const QString& versionTag) const; public slots: void autoCheckTimerFired() const; private: class Private; Private* priv; }; PrismLauncher-11.0.3/launcher/updater/ExternalUpdater.h0000644000175100017510000000602515224505336022513 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Kenneth Chew * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef LAUNCHER_EXTERNALUPDATER_H #define LAUNCHER_EXTERNALUPDATER_H #include /*! * A base class for an updater that uses an external library. * This class contains basic functions to control the updater. * * To implement the updater on a new platform, create a new class that inherits from this class and * implement the pure virtual functions. * * The initializer of the new class should have the side effect of starting the automatic updater. That is, * once the class is initialized, the program should automatically check for updates if necessary. */ class ExternalUpdater : public QObject { Q_OBJECT public: /*! * Check for updates manually, showing the user a progress bar and an alert if no updates are found. */ virtual void checkForUpdates() = 0; /*! * Indicates whether or not to check for updates automatically. */ virtual bool getAutomaticallyChecksForUpdates() = 0; /*! * Indicates the current automatic update check interval in seconds. */ virtual double getUpdateCheckInterval() = 0; /*! * Indicates whether or not beta updates should be checked for in addition to regular releases. */ virtual bool getBetaAllowed() = 0; /*! * Set whether or not to check for updates automatically. */ virtual void setAutomaticallyChecksForUpdates(bool check) = 0; /*! * Set the current automatic update check interval in seconds. */ virtual void setUpdateCheckInterval(double seconds) = 0; /*! * Set whether or not beta updates should be checked for in addition to regular releases. */ virtual void setBetaAllowed(bool allowed) = 0; signals: /*! * Emits whenever the user's ability to check for updates changes. * * As per Sparkle documentation, "An update check can be made by the user when an update session isn’t in progress, * or when an update or its progress is being shown to the user. A user cannot check for updates when data (such * as the feed or an update) is still being downloaded automatically in the background. * * This property is suitable to use for menu item validation for seeing if checkForUpdates can be invoked." */ void canCheckForUpdatesChanged(bool canCheck); }; #endif // LAUNCHER_EXTERNALUPDATER_H PrismLauncher-11.0.3/launcher/updater/PrismExternalUpdater.cpp0000644000175100017510000003140215224505336024056 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "PrismExternalUpdater.h" #include #include #include #include #include #include #include #include #include #include "StringUtils.h" #include "BuildConfig.h" #include "ui/dialogs/UpdateAvailableDialog.h" class PrismExternalUpdater::Private { public: QDir appDir; QDir dataDir; QTimer updateTimer; bool allowBeta{}; bool autoCheck{}; double updateInterval{}; QDateTime lastCheck; std::unique_ptr settings; QWidget* parent{}; }; PrismExternalUpdater::PrismExternalUpdater(QWidget* parent, const QString& appDir, const QString& dataDir) : priv(new PrismExternalUpdater::Private()) { priv->appDir = QDir(appDir); priv->dataDir = QDir(dataDir); auto settingsFile = priv->dataDir.absoluteFilePath("prismlauncher_update.cfg"); priv->settings = std::make_unique(settingsFile, QSettings::Format::IniFormat); priv->allowBeta = priv->settings->value("allow_beta", false).toBool(); priv->autoCheck = priv->settings->value("auto_check", true).toBool(); bool intervalOk = false; // default once per day priv->updateInterval = priv->settings->value("update_interval", 86400).toInt(&intervalOk); if (!intervalOk) { priv->updateInterval = 86400; } if (const auto lastCheck = priv->settings->value("last_check"); !lastCheck.isNull() && lastCheck.isValid()) { priv->lastCheck = QDateTime::fromString(lastCheck.toString(), Qt::ISODate); } priv->parent = parent; connectTimer(); resetAutoCheckTimer(); if (priv->updateInterval == 0) { // "On Launch" checkForUpdates(false); } } PrismExternalUpdater::~PrismExternalUpdater() { if (priv->updateTimer.isActive()) { priv->updateTimer.stop(); } disconnectTimer(); priv->settings->sync(); delete priv; } void PrismExternalUpdater::checkForUpdates() { checkForUpdates(true); } void PrismExternalUpdater::checkForUpdates(bool triggeredByUser) const { QProgressDialog progress(tr("Checking for updates..."), "", 0, 0, priv->parent); progress.setMinimumDuration(0); // Appear immediately without waiting progress.setCancelButton(nullptr); progress.adjustSize(); if (triggeredByUser) { progress.show(); } QCoreApplication::processEvents(); QProcess proc; auto exeName = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); #ifdef Q_OS_WIN32 exeName.append(".exe"); auto env = QProcessEnvironment::systemEnvironment(); env.insert("__COMPAT_LAYER", "RUNASINVOKER"); proc.setProcessEnvironment(env); #else exeName = QString("bin/%1").arg(exeName); #endif QStringList args = { "--check-only", "--dir", priv->dataDir.absolutePath(), "--debug" }; if (priv->allowBeta) { args.append("--pre-release"); } proc.start(priv->appDir.absoluteFilePath(exeName), args); if (auto resultStart = proc.waitForStarted(5000); !resultStart) { auto err = proc.error(); qDebug() << "Failed to start updater after 5 seconds." << "reason:" << err << proc.errorString(); auto msgBox = QMessageBox(QMessageBox::Information, tr("Update Check Failed"), tr("Failed to start after 5 seconds\nReason: %1.").arg(proc.errorString()), QMessageBox::Ok, priv->parent); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); priv->lastCheck = QDateTime::currentDateTime(); priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate)); priv->settings->sync(); resetAutoCheckTimer(); return; } QCoreApplication::processEvents(); if (auto resultFinished = proc.waitForFinished(60000); !resultFinished) { proc.kill(); auto err = proc.error(); auto output = proc.readAll(); qDebug() << "Updater failed to close after 60 seconds." << "reason:" << err << proc.errorString(); auto msgBox = QMessageBox(QMessageBox::Information, tr("Update Check Failed"), tr("Updater failed to close 60 seconds\nReason: %1.").arg(proc.errorString()), QMessageBox::Ok, priv->parent); msgBox.setDetailedText(output); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); priv->lastCheck = QDateTime::currentDateTime(); priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate)); priv->settings->sync(); resetAutoCheckTimer(); return; } auto exitCode = proc.exitCode(); auto stdOutput = proc.readAllStandardOutput(); auto stdError = proc.readAllStandardError(); progress.cancel(); QCoreApplication::processEvents(); switch (exitCode) { case 0: // no update available if (triggeredByUser) { qDebug() << "No update available"; auto msgBox = QMessageBox(QMessageBox::Information, tr("No Update Available"), tr("You are running the latest version."), QMessageBox::Ok, priv->parent); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); } break; case 1: // there was an error { qDebug() << "Updater subprocess error" << qPrintable(stdError); auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update Check Error"), tr("There was an error running the update check."), QMessageBox::Ok, priv->parent); msgBox.setDetailedText(QString(stdError)); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); } break; case 100: // update available { auto [firstLine, remainder1] = StringUtils::splitFirst(stdOutput, '\n'); auto [secondLine, remainder2] = StringUtils::splitFirst(remainder1, '\n'); auto [thirdLine, releaseNotes] = StringUtils::splitFirst(remainder2, '\n'); auto versionName = StringUtils::splitFirst(firstLine, ": ").second.trimmed(); auto versionTag = StringUtils::splitFirst(secondLine, ": ").second.trimmed(); auto releaseTimestamp = QDateTime::fromString(StringUtils::splitFirst(thirdLine, ": ").second.trimmed(), Qt::ISODate); qDebug() << "Update available:" << versionName << versionTag << releaseTimestamp; qDebug() << "Update release notes:" << releaseNotes; offerUpdate(versionName, versionTag, releaseNotes, triggeredByUser); } break; default: // unknown error code { qDebug() << "Updater exited with unknown code" << exitCode; auto msgBox = QMessageBox(QMessageBox::Information, tr("Unknown Update Error"), tr("The updater exited with an unknown condition.\nExit Code: %1").arg(QString::number(exitCode)), QMessageBox::Ok, priv->parent); auto detailTxt = tr("StdOut: %1\nStdErr: %2").arg(QString(stdOutput)).arg(QString(stdError)); msgBox.setDetailedText(detailTxt); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); } } priv->lastCheck = QDateTime::currentDateTime(); priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate)); priv->settings->sync(); resetAutoCheckTimer(); } bool PrismExternalUpdater::getAutomaticallyChecksForUpdates() { return priv->autoCheck; } double PrismExternalUpdater::getUpdateCheckInterval() { return priv->updateInterval; } bool PrismExternalUpdater::getBetaAllowed() { return priv->allowBeta; } void PrismExternalUpdater::setAutomaticallyChecksForUpdates(bool check) { priv->autoCheck = check; priv->settings->setValue("auto_check", check); priv->settings->sync(); resetAutoCheckTimer(); } void PrismExternalUpdater::setUpdateCheckInterval(double seconds) { priv->updateInterval = seconds; priv->settings->setValue("update_interval", seconds); priv->settings->sync(); resetAutoCheckTimer(); } void PrismExternalUpdater::setBetaAllowed(bool allowed) { priv->allowBeta = allowed; priv->settings->setValue("auto_beta", allowed); priv->settings->sync(); } void PrismExternalUpdater::resetAutoCheckTimer() const { if (priv->autoCheck && priv->updateInterval > 0) { auto now = QDateTime::currentDateTime(); qint64 timeoutMs = 0; if (priv->lastCheck.isValid()) { const qint64 diff = priv->lastCheck.secsTo(now); const qint64 secsLeft = std::max(priv->updateInterval - diff, 0); timeoutMs = secsLeft * 1000; } timeoutMs = std::min(timeoutMs, static_cast(INT_MAX)); qDebug() << "Auto update timer starting," << timeoutMs / 1000 << "seconds left"; priv->updateTimer.start(static_cast(timeoutMs)); } else { if (priv->updateTimer.isActive()) { priv->updateTimer.stop(); } } } void PrismExternalUpdater::connectTimer() { connect(&priv->updateTimer, &QTimer::timeout, this, &PrismExternalUpdater::autoCheckTimerFired); } void PrismExternalUpdater::disconnectTimer() { disconnect(&priv->updateTimer, &QTimer::timeout, this, &PrismExternalUpdater::autoCheckTimerFired); } void PrismExternalUpdater::autoCheckTimerFired() const { qDebug() << "Auto update Timer fired"; checkForUpdates(false); } void PrismExternalUpdater::offerUpdate(const QString& versionName, const QString& versionTag, const QString& releaseNotes, const bool triggeredByUser) const { priv->settings->beginGroup("skip"); auto shouldSkip = !triggeredByUser && priv->settings->value(versionTag, false).toBool(); priv->settings->endGroup(); if (shouldSkip) { if (triggeredByUser) { auto msgBox = QMessageBox(QMessageBox::Information, tr("No Update Available"), tr("There are no new updates available."), QMessageBox::Ok, priv->parent); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); } return; } UpdateAvailableDialog dlg(BuildConfig.printableVersionString(), versionName, releaseNotes); auto result = dlg.exec(); qDebug() << "offer dlg result" << result; priv->settings->beginGroup("skip"); if (result == UpdateAvailableDialog::Skip) { priv->settings->setValue(versionTag, true); } else { if (result == UpdateAvailableDialog::Install) { performUpdate(versionTag); } priv->settings->remove(versionTag); } priv->settings->endGroup(); priv->settings->sync(); } void PrismExternalUpdater::performUpdate(const QString& versionTag) const { QProcess proc; auto exeName = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); #ifdef Q_OS_WIN32 exeName.append(".exe"); auto env = QProcessEnvironment::systemEnvironment(); env.insert("__COMPAT_LAYER", "RUNASINVOKER"); proc.setProcessEnvironment(env); #else exeName = QString("bin/%1").arg(exeName); #endif QStringList args = { "--dir", priv->dataDir.absolutePath(), "--install-version", versionTag }; if (priv->allowBeta) { args.append("--pre-release"); } proc.setProgram(priv->appDir.absoluteFilePath(exeName)); proc.setArguments(args); auto result = proc.startDetached(); if (!result) { qDebug() << "Failed to start updater:" << proc.error() << proc.errorString(); } QCoreApplication::exit(); } PrismLauncher-11.0.3/launcher/updater/MacSparkleUpdater.h0000644000175100017510000001061015224505336022746 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Kenneth Chew * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef LAUNCHER_MACSPARKLEUPDATER_H #define LAUNCHER_MACSPARKLEUPDATER_H #include #include #include "ExternalUpdater.h" /*! * An implementation for the updater on macOS that uses the Sparkle framework. */ class MacSparkleUpdater : public ExternalUpdater { Q_OBJECT public: /*! * Start the Sparkle updater, which automatically checks for updates if necessary. */ MacSparkleUpdater(); ~MacSparkleUpdater() override; /*! * Check for updates manually, showing the user a progress bar and an alert if no updates are found. */ void checkForUpdates() override; /*! * Indicates whether or not to check for updates automatically. */ bool getAutomaticallyChecksForUpdates() override; /*! * Indicates the current automatic update check interval in seconds. */ double getUpdateCheckInterval() override; /*! * Indicates the set of Sparkle channels the updater is allowed to find new updates from. */ QSet getAllowedChannels(); /*! * Indicates whether or not beta updates should be checked for in addition to regular releases. */ bool getBetaAllowed() override; /*! * Set whether or not to check for updates automatically. * * As per Sparkle documentation, "By default, Sparkle asks users on second launch for permission if they want * automatic update checks enabled and sets this property based on their response. If SUEnableAutomaticChecks is * set in the Info.plist, this permission request is not performed however. * * Setting this property will persist in the host bundle’s user defaults. Only set this property if you need * dynamic behavior (e.g. user preferences). * * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow * reverting this property without kicking off a schedule change immediately." */ void setAutomaticallyChecksForUpdates(bool check) override; /*! * Set the current automatic update check interval in seconds. * * As per Sparkle documentation, "Setting this property will persist in the host bundle’s user defaults. For this * reason, only set this property if you need dynamic behavior (eg user preferences). Otherwise prefer to set * SUScheduledCheckInterval directly in your Info.plist. * * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow * reverting this property without kicking off a schedule change immediately." */ void setUpdateCheckInterval(double seconds) override; /*! * Clears all allowed Sparkle channels, returning to the default updater channel behavior. */ void clearAllowedChannels(); /*! * Set a single Sparkle channel the updater is allowed to find new updates from. * * Items in the default channel can always be found, regardless of this setting. If an empty string is passed, * return to the default behavior. */ void setAllowedChannel(const QString& channel); /*! * Set a set of Sparkle channels the updater is allowed to find new updates from. * * Items in the default channel can always be found, regardless of this setting. If an empty set is passed, * return to the default behavior. */ void setAllowedChannels(const QSet& channels); /*! * Set whether or not beta updates should be checked for in addition to regular releases. */ void setBetaAllowed(bool allowed) override; private: class Private; Private* priv; }; #endif // LAUNCHER_MACSPARKLEUPDATER_H PrismLauncher-11.0.3/launcher/updater/MacSparkleUpdater.mm0000644000175100017510000001340415224505336023134 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Kenneth Chew * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "MacSparkleUpdater.h" #include "Application.h" #include #include @interface UpdaterObserver : NSObject @property(nonatomic, readonly) SPUUpdater* updater; /// A callback to run when the state of `canCheckForUpdates` for the `updater` changes. @property(nonatomic, copy) void (^callback)(bool); - (id)initWithUpdater:(SPUUpdater*)updater; @end @implementation UpdaterObserver - (id)initWithUpdater:(SPUUpdater*)updater { self = [super init]; _updater = updater; [self addObserver:self forKeyPath:@"updater.canCheckForUpdates" options:NSKeyValueObservingOptionNew context:nil]; return self; } - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context { if ([keyPath isEqualToString:@"updater.canCheckForUpdates"]) { bool canCheck = [change[NSKeyValueChangeNewKey] boolValue]; self.callback(canCheck); } } @end @interface UpdaterDelegate : NSObject @property(nonatomic, copy) NSSet* allowedChannels; @end @implementation UpdaterDelegate - (NSSet*)allowedChannelsForUpdater:(SPUUpdater*)updater { return _allowedChannels; } @end class MacSparkleUpdater::Private { public: SPUStandardUpdaterController* updaterController; UpdaterObserver* updaterObserver; UpdaterDelegate* updaterDelegate; NSAutoreleasePool* autoReleasePool; }; MacSparkleUpdater::MacSparkleUpdater() { priv = new MacSparkleUpdater::Private(); // Enable Cocoa's memory management. NSApplicationLoad(); priv->autoReleasePool = [[NSAutoreleasePool alloc] init]; // Delegate is used for setting/getting allowed update channels. priv->updaterDelegate = [[UpdaterDelegate alloc] init]; // Controller is the interface for actually doing the updates. priv->updaterController = [[SPUStandardUpdaterController alloc] initWithStartingUpdater:true updaterDelegate:priv->updaterDelegate userDriverDelegate:nil]; priv->updaterObserver = [[UpdaterObserver alloc] initWithUpdater:priv->updaterController.updater]; // Use KVO to run a callback that emits a Qt signal when `canCheckForUpdates` changes, so the UI can respond accordingly. priv->updaterObserver.callback = ^(bool canCheck) { emit canCheckForUpdatesChanged(canCheck); }; } MacSparkleUpdater::~MacSparkleUpdater() { [priv->updaterObserver removeObserver:priv->updaterObserver forKeyPath:@"updater.canCheckForUpdates"]; [priv->updaterController release]; [priv->updaterObserver release]; [priv->updaterDelegate release]; [priv->autoReleasePool release]; delete priv; } void MacSparkleUpdater::checkForUpdates() { [priv->updaterController checkForUpdates:nil]; } bool MacSparkleUpdater::getAutomaticallyChecksForUpdates() { return priv->updaterController.updater.automaticallyChecksForUpdates; } double MacSparkleUpdater::getUpdateCheckInterval() { return priv->updaterController.updater.updateCheckInterval; } QSet MacSparkleUpdater::getAllowedChannels() { // Convert NSSet -> QSet __block QSet channels; [priv->updaterDelegate.allowedChannels enumerateObjectsUsingBlock:^(NSString* channel, BOOL* stop) { channels.insert(QString::fromNSString(channel)); }]; return channels; } bool MacSparkleUpdater::getBetaAllowed() { return getAllowedChannels().contains("beta"); } void MacSparkleUpdater::setAutomaticallyChecksForUpdates(bool check) { priv->updaterController.updater.automaticallyChecksForUpdates = check ? YES : NO; // make clang-tidy happy } void MacSparkleUpdater::setUpdateCheckInterval(double seconds) { priv->updaterController.updater.updateCheckInterval = seconds; } void MacSparkleUpdater::clearAllowedChannels() { priv->updaterDelegate.allowedChannels = [NSSet set]; } void MacSparkleUpdater::setAllowedChannel(const QString& channel) { if (channel.isEmpty()) { clearAllowedChannels(); return; } NSSet* nsChannels = [NSSet setWithObject:channel.toNSString()]; priv->updaterDelegate.allowedChannels = nsChannels; } void MacSparkleUpdater::setAllowedChannels(const QSet& channels) { if (channels.isEmpty()) { clearAllowedChannels(); return; } QString channelsConfig = ""; // Convert QSet -> NSSet NSMutableSet* nsChannels = [NSMutableSet setWithCapacity:channels.count()]; for (const QString& channel : channels) { [nsChannels addObject:channel.toNSString()]; channelsConfig += channel + " "; } priv->updaterDelegate.allowedChannels = nsChannels; } void MacSparkleUpdater::setBetaAllowed(bool allowed) { if (allowed) { setAllowedChannel("beta"); } else { clearAllowedChannels(); } } PrismLauncher-11.0.3/launcher/Commandline.cpp0000644000175100017510000000522315224505336020520 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan * * 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 "Commandline.h" /** * @file libutil/src/cmdutils.cpp */ namespace Commandline { // commandline splitter QStringList splitArgs(QString args) { QStringList argv; QString current; bool escape = false; QChar inquotes; for (int i = 0; i < args.length(); i++) { QChar cchar = args.at(i); // \ escaped if (escape) { current += cchar; escape = false; // in "quotes" } else if (!inquotes.isNull()) { if (cchar == '\\') escape = true; else if (cchar == inquotes) inquotes = QChar::Null; else current += cchar; // otherwise } else { if (cchar == ' ') { if (!current.isEmpty()) { argv << current; current.clear(); } } else if (cchar == '"' || cchar == '\'') inquotes = cchar; else current += cchar; } } if (!current.isEmpty()) argv << current; return argv; } } // namespace Commandline PrismLauncher-11.0.3/launcher/InstanceCopyTask.h0000644000175100017510000000170115224505336021156 0ustar runnerrunner#pragma once #include #include #include #include "BaseInstance.h" #include "BaseVersion.h" #include "Filter.h" #include "InstanceCopyPrefs.h" #include "InstanceTask.h" #include "net/NetJob.h" #include "settings/SettingsObject.h" #include "tasks/Task.h" class InstanceCopyTask : public InstanceTask { Q_OBJECT public: explicit InstanceCopyTask(BaseInstance* origInstance, const InstanceCopyPrefs& prefs); protected: //! Entry point for tasks. virtual void executeTask() override; bool abort() override; void copyFinished(); void copyAborted(); private: /* data */ BaseInstance* m_origInstance; QFuture m_copyFuture; QFutureWatcher m_copyFutureWatcher; Filter m_matcher; bool m_keepPlaytime; bool m_useLinks = false; bool m_useHardLinks = false; bool m_copySaves = false; bool m_linkRecursively = false; bool m_useClone = false; }; PrismLauncher-11.0.3/launcher/Application.h0000644000175100017510000002227515224505336020210 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Tayou * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include #include #include "QObjectPtr.h" #include "minecraft/auth/MinecraftAccount.h" class LaunchController; class LocalPeer; class InstanceWindow; class MainWindow; class ViewLogWindow; class SetupWizard; class GenericPageProvider; class QFile; class HttpMetaCache; class SettingsObject; class InstanceList; class AccountList; class IconList; class QNetworkAccessManager; class JavaInstallList; class ExternalUpdater; class BaseProfilerFactory; class BaseDetachedToolFactory; class TranslationsModel; class ITheme; class MCEditTool; class ThemeManager; class IconTheme; class BaseInstance; class LogModel; struct MinecraftTarget; class MinecraftAccount; namespace Meta { class Index; } #if defined(APPLICATION) #undef APPLICATION #endif #define APPLICATION (static_cast(QCoreApplication::instance())) // Used for checking if is a test #if defined(APPLICATION_DYN) #undef APPLICATION_DYN #endif #define APPLICATION_DYN (dynamic_cast(QCoreApplication::instance())) class Application : public QApplication { Q_OBJECT public: enum Status { StartingUp, Failed, Succeeded, Initialized }; enum Capability { None = 0, SupportsMSA = 1 << 0, SupportsFlame = 1 << 1, SupportsGameMode = 1 << 2, SupportsMangoHud = 1 << 3, }; Q_DECLARE_FLAGS(Capabilities, Capability) public: Application(int& argc, char** argv); virtual ~Application(); bool event(QEvent* event) override; SettingsObject* settings() const { return m_settings.get(); } qint64 timeSinceStart() const { return m_startTime.msecsTo(QDateTime::currentDateTime()); } QIcon logo(); ThemeManager* themeManager() { return m_themeManager.get(); } ExternalUpdater* updater() { return m_updater.get(); } void triggerUpdateCheck(); TranslationsModel* translations(); JavaInstallList* javalist(); InstanceList* instances() const { return m_instances.get(); } IconList* icons() const { return m_icons.get(); } MCEditTool* mcedit() const { return m_mcedit.get(); } AccountList* accounts() const { return m_accounts.get(); } Status status() const { return m_status; } const QMap>& profilers() const { return m_profilers; } void updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password); QNetworkAccessManager* network(); HttpMetaCache* metacache(); Meta::Index* metadataIndex(); void updateCapabilities(); void detectLibraries(); /*! * Finds and returns the full path to a jar file. * Returns a null-string if it could not be found. */ QString getJarPath(QString jarFile); QString getMSAClientID(); QString getFlameAPIKey(); QString getModrinthAPIToken(); QString getUserAgent(); /// this is the root of the 'installation'. Used for automatic updates const QString& root() { return m_rootPath; } /// the data path the application is using const QString& dataRoot() { return m_dataPath; } /// the java installed path the application is using const QString javaPath(); bool isPortable() { return m_portable; } const Capabilities capabilities() { return m_capabilities; } /*! * Opens a json file using either a system default editor, or, if not empty, the editor * specified in the settings */ bool openJsonEditor(const QString& filename); InstanceWindow* showInstanceWindow(BaseInstance* instance, QString page = QString()); MainWindow* showMainWindow(bool minimized = false); ViewLogWindow* showLogWindow(); void updateIsRunning(bool running); bool updatesAreAllowed(); void ShowGlobalSettings(class QWidget* parent, QString open_page = QString()); bool updaterEnabled(); QString updaterBinaryName(); QUrl normalizeImportUrl(const QString& url); signals: void updateAllowedChanged(bool status); void globalSettingsAboutToOpen(); void globalSettingsApplied(); int currentCatChanged(int index); void oauthReplyRecieved(QVariantMap); #ifdef Q_OS_MACOS void clickedOnDock(); #endif public slots: bool launch(BaseInstance* instance, LaunchMode mode = LaunchMode::Normal, std::shared_ptr targetToJoin = nullptr, shared_qobject_ptr accountToUse = nullptr, const QString& offlineName = QString()); bool kill(BaseInstance* instance); void closeCurrentWindow(); private slots: void on_windowClose(); void messageReceived(const QByteArray& message); void controllerFinished(); void setupWizardFinished(int status); private: bool handleDataMigration(const QString& currentData, const QString& oldData, const QString& name, const QString& configFile) const; bool createSetupWizard(); void performMainStartupAction(); // sets the fatal error message and m_status to Failed. void showFatalErrorMessage(const QString& title, const QString& content); private: void addRunningInstance(); void subRunningInstance(); bool shouldExitNow() const; private: QHash m_qsaveResources; mutable QMutex m_qsaveResourcesMutex; private: QDateTime m_startTime; std::unique_ptr m_network; std::unique_ptr m_updater; std::unique_ptr m_accounts; std::unique_ptr m_metacache; std::unique_ptr m_metadataIndex; std::unique_ptr m_settings; std::unique_ptr m_instances; std::unique_ptr m_icons; std::unique_ptr m_javalist; std::unique_ptr m_translations; std::unique_ptr m_globalSettingsProvider; std::unique_ptr m_mcedit; QSet m_features; std::unique_ptr m_themeManager; QMap> m_profilers; QString m_rootPath; QString m_dataPath; Status m_status = Application::StartingUp; Capabilities m_capabilities; bool m_portable = false; #ifdef Q_OS_MACOS Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive; #endif // FIXME: attach to instances instead. struct InstanceXtras { InstanceWindow* window = nullptr; std::unique_ptr controller; }; std::map m_instanceExtras; mutable QMutex m_instanceExtrasMutex; // main state variables size_t m_openWindows = 0; size_t m_runningInstances = 0; bool m_updateRunning = false; // main window, if any MainWindow* m_mainWindow = nullptr; // log window, if any ViewLogWindow* m_viewLogWindow = nullptr; // peer launcher instance connector - used to implement single instance launcher and signalling LocalPeer* m_peerInstance = nullptr; SetupWizard* m_setupWizard = nullptr; public: QString m_detectedGLFWPath; QString m_detectedOpenALPath; QString m_instanceIdToLaunch; QString m_serverToJoin; QString m_worldToJoin; QString m_profileToUse; bool m_launchOffline = false; QString m_offlineName; bool m_liveCheck = false; QList m_urlsToImport; QString m_instanceIdToShowWindowOf; bool m_showMainWindow = false; std::unique_ptr logFile; std::unique_ptr logModel; public: void addQSavePath(QString); void removeQSavePath(QString); bool checkQSavePath(QString); }; PrismLauncher-11.0.3/launcher/PSaveFile.h0000644000175100017510000000477215224505336017565 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "Application.h" #if defined(LAUNCHER_APPLICATION) /* PSaveFile * A class that mimics QSaveFile for Windows. * * When reading resources, we need to avoid accessing temporary files * generated by QSaveFile. If we start reading such a file, we may * inadvertently keep it open while QSaveFile is trying to remove it, * or we might detect the file just before it is removed, leading to * race conditions and errors. * * Unfortunately, QSaveFile doesn't provide a way to retrieve the * temporary file name or to set a specific template for the temporary * file name it uses. By default, QSaveFile appends a `.XXXXXX` suffix * to the original file name, where the `XXXXXX` part is dynamically * generated to ensure uniqueness. * * This class acts like a lock by adding and removing the target file * name into/from a global string set, helping to manage access to * files during critical operations. * * Note: Please do not use the `setFileName` function directly, as it * is not virtual and cannot be overridden. */ class PSaveFile : public QSaveFile { public: PSaveFile(const QString& name) : QSaveFile(name) { addPath(name); } PSaveFile(const QString& name, QObject* parent) : QSaveFile(name, parent) { addPath(name); } virtual ~PSaveFile() { if (auto app = APPLICATION_DYN) { app->removeQSavePath(m_absoluteFilePath); } } private: void addPath(const QString& path) { m_absoluteFilePath = QFileInfo(path).absoluteFilePath() + "."; // add dot for tmp files only if (auto app = APPLICATION_DYN) { app->addQSavePath(m_absoluteFilePath); } } QString m_absoluteFilePath; }; #else using PSaveFile = QSaveFile; #endif PrismLauncher-11.0.3/launcher/BaseInstance.h0000644000175100017510000002404015224505336020274 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include #include #include "QObjectPtr.h" #include "settings/SettingsObject.h" #include "BaseVersionList.h" #include "MessageLevel.h" #include "minecraft/auth/MinecraftAccount.h" #include "settings/INIFile.h" #include "net/Mode.h" #include "RuntimeContext.h" #include "minecraft/launch/MinecraftTarget.h" class QDir; class Task; class LaunchTask; class BaseInstance; /// Shortcut saving target representations enum class ShortcutTarget { Desktop, Applications, Other }; /// Shortcut data representation struct ShortcutData { QString name; QString filePath; ShortcutTarget target = ShortcutTarget::Other; }; /// Console settings int getConsoleMaxLines(SettingsObject* settings); bool shouldStopOnConsoleOverflow(SettingsObject* settings); /*! * \brief Base class for instances. * This class implements many functions that are common between instances and * provides a standard interface for all instances. * * To create a new instance type, create a new class inheriting from this class * and implement the pure virtual functions. */ class BaseInstance : public QObject { Q_OBJECT protected: /// no-touchy! BaseInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir); public: /* types */ enum class Status { Present, Gone // either nuked or invalidated }; public: /// virtual destructor to make sure the destruction is COMPLETE virtual ~BaseInstance(); virtual void saveNow() = 0; /*** * the instance has been invalidated - it is no longer tracked by the launcher for some reason, * but it has not necessarily been deleted. * * Happens when the instance folder changes to some other location, or the instance is removed by external means. */ void invalidate(); /// The instance's ID. The ID SHALL be determined by LAUNCHER internally. The ID IS guaranteed to /// be unique. virtual QString id() const; void setMinecraftRunning(bool running); void setRunning(bool running); bool isRunning() const; int64_t totalTimePlayed() const; int64_t lastTimePlayed() const; void resetTimePlayed(); /// get the type of this instance QString instanceType() const; /// Path to the instance's root directory. QString instanceRoot() const; /// Path to the instance's game root directory. virtual QString gameRoot() const { return instanceRoot(); } /// Path to the instance's mods directory. virtual QString modsRoot() const = 0; QString name() const; void setName(QString val); /// Sync name and rename instance dir accordingly; returns true if successful bool syncInstanceDirName(const QString& newRoot) const; /// Register a created shortcut void registerShortcut(const ShortcutData& data); QList shortcuts() const; void setShortcuts(const QList& shortcuts); /// Value used for instance window titles QString windowTitle() const; QString iconKey() const; void setIconKey(QString val); QString notes() const; void setNotes(QString val); QString getPreLaunchCommand(); QString getPostExitCommand(); QString getWrapperCommand(); bool isManagedPack() const; QString getManagedPackType() const; QString getManagedPackID() const; QString getManagedPackName() const; QString getManagedPackVersionID() const; QString getManagedPackVersionName() const; void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version); void copyManagedPack(BaseInstance& other); virtual QStringList extraArguments(); /// Traits. Normally inside the version, depends on instance implementation. virtual QSet traits() const = 0; /** * Gets the time that the instance was last launched. * Stored in milliseconds since epoch. */ qint64 lastLaunch() const; /// Sets the last launched time to 'val' milliseconds since epoch void setLastLaunch(qint64 val = QDateTime::currentMSecsSinceEpoch()); /*! * \brief Gets this instance's settings object. * This settings object stores instance-specific settings. * * Note that this method is not const. * It may call loadSpecificSettings() to ensure those are loaded. * * \return A pointer to this instance's settings object. */ virtual SettingsObject* settings(); /*! * \brief Loads settings specific to an instance type if they're not already loaded. */ virtual void loadSpecificSettings() = 0; /// returns a valid update task virtual QList createUpdateTask() = 0; /// returns a valid launcher (task container) virtual LaunchTask* createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) = 0; /// returns the current launch task (if any) LaunchTask* getLaunchTask(); /*! * Create envrironment variables for running the instance */ virtual QProcessEnvironment createEnvironment() = 0; virtual QProcessEnvironment createLaunchEnvironment() = 0; /*! * Returns the root folder to use for looking up log files */ virtual QStringList getLogFileSearchPaths() = 0; virtual QString getStatusbarDescription() = 0; /// FIXME: this really should be elsewhere... virtual QString instanceConfigFolder() const = 0; /// get variables this instance exports virtual QMap getVariables() = 0; virtual QString typeName() const = 0; virtual void updateRuntimeContext(); RuntimeContext runtimeContext() const { return m_runtimeContext; } bool hasVersionBroken() const { return m_hasBrokenVersion; } void setVersionBroken(bool value) { if (m_hasBrokenVersion != value) { m_hasBrokenVersion = value; emit propertiesChanged(this); } } bool hasUpdateAvailable() const { return m_hasUpdate; } void setUpdateAvailable(bool value) { if (m_hasUpdate != value) { m_hasUpdate = value; emit propertiesChanged(this); } } bool hasCrashed() const { return m_crashed; } void setCrashed(bool value) { if (m_crashed != value) { m_crashed = value; emit propertiesChanged(this); } } virtual bool canLaunch() const; virtual bool canEdit() const = 0; virtual bool canExport() const = 0; virtual void populateLaunchMenu(QMenu* menu) = 0; bool reloadSettings(); /** * 'print' a verbose description of the instance into a QStringList */ virtual QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) = 0; Status currentStatus() const; QStringList getLinkedInstances() const; void setLinkedInstances(const QStringList& list); void addLinkedInstanceId(const QString& id); bool removeLinkedInstanceId(const QString& id); bool isLinkedToInstanceId(const QString& id) const; bool isLegacy(); protected: void changeStatus(Status newStatus); SettingsObject* globalSettings() const { return m_global_settings; } bool isSpecificSettingsLoaded() const { return m_specific_settings_loaded; } void setSpecificSettingsLoaded(bool loaded) { m_specific_settings_loaded = loaded; } signals: /*! * \brief Signal emitted when properties relevant to the instance view change */ void propertiesChanged(BaseInstance* inst); void launchTaskChanged(LaunchTask*); void runningStatusChanged(bool running); void profilerChanged(); void statusChanged(Status from, Status to); protected slots: void iconUpdated(QString key); protected: /* data */ QString m_rootDir; std::unique_ptr m_settings; // InstanceFlags m_flags; bool m_isRunning = false; std::unique_ptr m_launchProcess; QDateTime m_timeStarted; RuntimeContext m_runtimeContext; private: /* data */ Status m_status = Status::Present; bool m_crashed = false; bool m_hasUpdate = false; bool m_hasBrokenVersion = false; SettingsObject* m_global_settings; bool m_specific_settings_loaded = false; }; Q_DECLARE_METATYPE(shared_qobject_ptr) // Q_DECLARE_METATYPE(BaseInstance::InstanceFlag) // Q_DECLARE_OPERATORS_FOR_FLAGS(BaseInstance::InstanceFlags) PrismLauncher-11.0.3/launcher/SeparatorPrefixTree.h0000644000175100017510000001573715224505336021710 0ustar runnerrunner#pragma once #include #include #include template class SeparatorPrefixTree { public: SeparatorPrefixTree(QStringList paths) { insert(paths); } SeparatorPrefixTree(bool contained = false) { m_contained = contained; } void insert(QStringList paths) { for (auto& path : paths) { insert(path); } } /// insert an exact path into the tree SeparatorPrefixTree& insert(QString path) { auto sepIndex = path.indexOf(Tseparator); if (sepIndex == -1) { children[path] = SeparatorPrefixTree(true); return children[path]; } else { auto prefix = path.left(sepIndex); if (!children.contains(prefix)) { children[prefix] = SeparatorPrefixTree(false); } return children[prefix].insert(path.mid(sepIndex + 1)); } } /// is the path fully contained in the tree? bool contains(QString path) const { auto node = find(path); return node != nullptr; } /// does the tree cover a path? That means the prefix of the path is contained in the tree bool covers(QString path) const { // if we found some valid node, it's good enough. the tree covers the path if (m_contained) { return true; } auto sepIndex = path.indexOf(Tseparator); if (sepIndex == -1) { auto found = children.find(path); if (found == children.end()) { return false; } return (*found).covers(QString()); } else { auto prefix = path.left(sepIndex); auto found = children.find(prefix); if (found == children.end()) { return false; } return (*found).covers(path.mid(sepIndex + 1)); } } /// return the contained path that covers the path specified QString cover(QString path) const { // if we found some valid node, it's good enough. the tree covers the path if (m_contained) { return QString(""); } auto sepIndex = path.indexOf(Tseparator); if (sepIndex == -1) { auto found = children.find(path); if (found == children.end()) { return QString(); } auto nested = (*found).cover(QString()); if (nested.isNull()) { return nested; } if (nested.isEmpty()) return path; return path + Tseparator + nested; } else { auto prefix = path.left(sepIndex); auto found = children.find(prefix); if (found == children.end()) { return QString(); } auto nested = (*found).cover(path.mid(sepIndex + 1)); if (nested.isNull()) { return nested; } if (nested.isEmpty()) return prefix; return prefix + Tseparator + nested; } } /// Does the path-specified node exist in the tree? It does not have to be contained. bool exists(QString path) const { auto sepIndex = path.indexOf(Tseparator); if (sepIndex == -1) { auto found = children.find(path); if (found == children.end()) { return false; } return true; } else { auto prefix = path.left(sepIndex); auto found = children.find(prefix); if (found == children.end()) { return false; } return (*found).exists(path.mid(sepIndex + 1)); } } /// find a node in the tree by name const SeparatorPrefixTree* find(QString path) const { auto sepIndex = path.indexOf(Tseparator); if (sepIndex == -1) { auto found = children.find(path); if (found == children.end()) { return nullptr; } return &(*found); } else { auto prefix = path.left(sepIndex); auto found = children.find(prefix); if (found == children.end()) { return nullptr; } return (*found).find(path.mid(sepIndex + 1)); } } /// is this a leaf node? bool leaf() const { return children.isEmpty(); } /// is this node actually contained in the tree, or is it purely structural? bool contained() const { return m_contained; } /// Remove a path from the tree bool remove(QString path) { return removeInternal(path) != Failed; } /// Clear all children of this node tree node void clear() { children.clear(); } QStringList toStringList() const { QStringList collected; // collecting these is more expensive. auto iter = children.begin(); while (iter != children.end()) { QStringList list = iter.value().toStringList(); for (int i = 0; i < list.size(); i++) { list[i] = iter.key() + Tseparator + list[i]; } collected.append(list); if ((*iter).m_contained) { collected.append(iter.key()); } iter++; } return collected; } private: enum Removal { Failed, Succeeded, HasChildren }; Removal removeInternal(QString path = QString()) { if (path.isEmpty()) { if (!m_contained) { // remove all children - we are removing a prefix clear(); return Succeeded; } m_contained = false; if (children.size()) { return HasChildren; } return Succeeded; } Removal remStatus = Failed; QString childToRemove; auto sepIndex = path.indexOf(Tseparator); if (sepIndex == -1) { childToRemove = path; auto found = children.find(childToRemove); if (found == children.end()) { return Failed; } remStatus = (*found).removeInternal(); } else { childToRemove = path.left(sepIndex); auto found = children.find(childToRemove); if (found == children.end()) { return Failed; } remStatus = (*found).removeInternal(path.mid(sepIndex + 1)); } switch (remStatus) { case Failed: case HasChildren: { return remStatus; } case Succeeded: { children.remove(childToRemove); if (m_contained) { return HasChildren; } if (children.size()) { return HasChildren; } return Succeeded; } } return Failed; } private: QMap> children; bool m_contained = false; }; PrismLauncher-11.0.3/launcher/BaseVersionList.cpp0000644000175100017510000000735715224505336021360 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "BaseVersionList.h" #include "BaseVersion.h" BaseVersionList::BaseVersionList(QObject* parent) : QAbstractListModel(parent) {} BaseVersion::Ptr BaseVersionList::findVersion(const QString& descriptor) { for (int i = 0; i < count(); i++) { if (at(i)->descriptor() == descriptor) return at(i); } return nullptr; } BaseVersion::Ptr BaseVersionList::getRecommended() const { if (count() <= 0) return nullptr; else return at(0); } QVariant BaseVersionList::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() > count()) return QVariant(); BaseVersion::Ptr version = at(index.row()); switch (role) { case VersionPointerRole: return QVariant::fromValue(version); case VersionRole: return version->name(); case VersionIdRole: return version->descriptor(); case TypeRole: return version->typeString(); case JavaMajorRole: { auto major = version->name(); if (major.startsWith("java")) { major = "Java " + major.mid(4); } return major; } default: return QVariant(); } } BaseVersionList::RoleList BaseVersionList::providesRoles() const { return { VersionPointerRole, VersionRole, VersionIdRole, TypeRole }; } int BaseVersionList::rowCount(const QModelIndex& parent) const { // Return count return parent.isValid() ? 0 : count(); } int BaseVersionList::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } QHash BaseVersionList::roleNames() const { QHash roles = QAbstractListModel::roleNames(); roles.insert(VersionRole, "version"); roles.insert(VersionIdRole, "versionId"); roles.insert(ParentVersionRole, "parentGameVersion"); roles.insert(RecommendedRole, "recommended"); roles.insert(LatestRole, "latest"); roles.insert(TypeRole, "type"); roles.insert(BranchRole, "branch"); roles.insert(PathRole, "path"); roles.insert(JavaNameRole, "javaName"); roles.insert(CPUArchitectureRole, "architecture"); roles.insert(JavaMajorRole, "javaMajor"); return roles; } PrismLauncher-11.0.3/launcher/LaunchController.cpp0000644000175100017510000004460415224505336021556 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "LaunchController.h" #include "Application.h" #include "launch/steps/PrintServers.h" #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountList.h" #include "net/NetUtils.h" #include "ui/InstanceWindow.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/ProfileSelectDialog.h" #include "ui/dialogs/ProfileSetupDialog.h" #include "ui/dialogs/ProgressDialog.h" #include #include #include #include #include "BuildConfig.h" #include "JavaCommon.h" #include "launch/steps/TextPrint.h" #include "tasks/Task.h" #include "ui/dialogs/ChooseOfflineNameDialog.h" LaunchController::LaunchController() = default; void LaunchController::executeTask() { if (!m_instance) { emitFailed(tr("No instance specified!")); return; } if (!JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget)) { emitFailed(tr("Invalid Java arguments specified. Please fix this first.")); return; } login(); } void LaunchController::decideAccount() { if (m_accountToUse) { return; } // Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used auto* accounts = APPLICATION->accounts(); const auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString(); const auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId); if (instanceAccountIndex == -1 || instanceAccountId.isEmpty()) { m_accountToUse = accounts->defaultAccount(); } else { m_accountToUse = accounts->at(instanceAccountIndex); } if (!accounts->anyAccountIsValid()) { // Tell the user they need to log in at least one account in order to play. auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"), tr("In order to play Minecraft, you must have at least one Microsoft " "account which owns Minecraft logged in. " "Would you like to open the account manager to add an account now?"), QMessageBox::Information, QMessageBox::Yes | QMessageBox::No) ->exec(); if (reply == QMessageBox::Yes) { // Open the account manager. APPLICATION->ShowGlobalSettings(m_parentWidget, "accounts"); } else if (reply == QMessageBox::No) { // Do not open "profile select" dialog. return; } } if (!m_accountToUse && accounts->anyAccountIsValid()) { // If no default account is set, ask the user which one to use. ProfileSelectDialog selectDialog(tr("Which account would you like to use?"), ProfileSelectDialog::GlobalDefaultCheckbox, m_parentWidget); selectDialog.exec(); // Launch the instance with the selected account. m_accountToUse = selectDialog.selectedAccount(); // If the user said to use the account as default, do that. if (selectDialog.useAsGlobalDefault() && m_accountToUse) { accounts->setDefaultAccount(m_accountToUse); } } } LaunchDecision LaunchController::decideLaunchMode() { if (!m_accountToUse || m_wantedLaunchMode == LaunchMode::Demo) { m_actualLaunchMode = LaunchMode::Demo; return LaunchDecision::Continue; } const auto* accounts = APPLICATION->accounts(); MinecraftAccountPtr accountToCheck = nullptr; if (m_accountToUse->accountType() != AccountType::Offline) { accountToCheck = m_accountToUse->ownsMinecraft() ? m_accountToUse : nullptr; } else if (const auto defaultAccount = accounts->defaultAccount(); defaultAccount && defaultAccount->ownsMinecraft()) { accountToCheck = defaultAccount; } else { for (int i = 0; i < accounts->count(); i++) { if (const auto account = accounts->at(i); account->ownsMinecraft()) { accountToCheck = account; break; } } } if (!accountToCheck) { m_actualLaunchMode = LaunchMode::Demo; return LaunchDecision::Continue; } auto state = accountToCheck->accountState(); const bool needsRefresh = m_wantedLaunchMode == LaunchMode::Normal && (state == AccountState::Offline || accountToCheck->shouldRefresh()); if (state == AccountState::Unchecked || state == AccountState::Errored || needsRefresh) { accountToCheck->refresh(); state = AccountState::Working; } if (state == AccountState::Working) { // refresh is in progress, we need to wait for it to finish to proceed. ProgressDialog progDialog(m_parentWidget); progDialog.setSkipButton(true, tr("Abort")); // TODO: this relies on tasks' synchronous signal dispatching nature // TODO: meaning currentTask can't complete and become null while this code is running // TODO: this code will produce a race condition when tasks become fully async auto task = accountToCheck->currentTask(); progDialog.execWithTask(task.get()); if (task->getState() == State::AbortedByUser) { return LaunchDecision::Abort; } state = accountToCheck->accountState(); } QString reauthReason; switch (state) { case AccountState::Errored: reauthReason = tr("An error occurred while refreshing '%1'").arg(accountToCheck->profileName()); break; case AccountState::Expired: reauthReason = tr("'%1' has expired and needs to be reauthenticated").arg(accountToCheck->profileName()); break; case AccountState::Disabled: reauthReason = tr("The launcher's client identification has changed"); break; case AccountState::Gone: reauthReason = tr("'%1' no longer exists on the servers").arg(accountToCheck->profileName()); break; default: m_actualLaunchMode = state == AccountState::Online && m_wantedLaunchMode == LaunchMode::Normal ? LaunchMode::Normal : LaunchMode::Offline; return LaunchDecision::Continue; // All good to go } if (reauthenticateAccount(accountToCheck, reauthReason)) { return LaunchDecision::Undecided; } return LaunchDecision::Abort; } bool LaunchController::askPlayDemo() const { QMessageBox box(m_parentWidget); box.setWindowTitle(tr("Play demo?")); QString text = m_accountToUse ? tr("This account does not own Minecraft.\nYou need to purchase the game first to play the full version.") : tr("No account was selected for launch."); text += tr("\n\nDo you want to play the demo?"); box.setText(text); box.setIcon(QMessageBox::Warning); const auto* demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); auto* cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); box.setDefaultButton(cancelButton); box.exec(); return box.clickedButton() == demoButton; } QString LaunchController::askOfflineName(const QString& playerName, bool* ok) { if (ok != nullptr) { *ok = false; } QString title, message; title = tr("Player name"); switch (m_actualLaunchMode) { case LaunchMode::Normal: Q_ASSERT(false); return ""; case LaunchMode::Demo: message = tr("Choose your demo mode player name"); break; case LaunchMode::Offline: if (m_wantedLaunchMode == LaunchMode::Normal) { auto netErr = m_accountToUse->accountData()->networkError; if (Net::isServerError(netErr)) { title = tr("Auth servers offline"); message = tr("The Minecraft authentication servers are currently unavailable, launching in offline mode.\n\n"); } else { title = tr("No internet connection"); message = tr("You are not connected to the Internet, launching in offline mode.\n\n"); } } message += tr("Choose your offline mode player name"); break; } const QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName; ChooseOfflineNameDialog dialog(message, m_parentWidget); dialog.setWindowTitle(title); dialog.setUsername(usedname); if (dialog.exec() != QDialog::Accepted) { return {}; } usedname = dialog.getUsername(); APPLICATION->settings()->set("LastOfflinePlayerName", usedname); if (ok != nullptr) { *ok = true; } return usedname; } void LaunchController::login() { decideAccount(); LaunchDecision decision = decideLaunchMode(); while (decision == LaunchDecision::Undecided) { decision = decideLaunchMode(); } if (decision == LaunchDecision::Abort) { emitAborted(); return; } if (m_actualLaunchMode == LaunchMode::Demo) { if (m_wantedLaunchMode == LaunchMode::Demo || askPlayDemo()) { bool ok = false; auto name = askOfflineName("Player", &ok); if (ok) { m_session = std::make_shared(); m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString(QUuid::Id128)); launchInstance(); return; } } emitFailed(tr("No account selected for launch")); return; } m_session = std::make_shared(); m_session->launchMode = m_actualLaunchMode; m_accountToUse->fillSession(m_session); if (m_accountToUse->accountType() != AccountType::Offline) { if (m_actualLaunchMode == LaunchMode::Normal && !m_accountToUse->hasProfile()) { // Now handle setting up a profile name here... if (ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); dialog.exec() != QDialog::Accepted) { emitAborted(); return; } } if (m_actualLaunchMode == LaunchMode::Offline && m_accountToUse->accountType() != AccountType::Offline) { bool ok = false; QString name = m_offlineName; if (name.isEmpty()) { name = askOfflineName(m_session->player_name, &ok); if (!ok) { emitAborted(); return; } } m_session->MakeOffline(name); } } launchInstance(); } bool LaunchController::reauthenticateAccount(const MinecraftAccountPtr& account, const QString& reason) { auto button = QMessageBox::warning( m_parentWidget, tr("Account refresh failed"), tr("%1. Do you want to reauthenticate this account?").arg(reason), QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::Yes); if (button == QMessageBox::StandardButton::Yes) { auto* accounts = APPLICATION->accounts(); const bool isDefault = accounts->defaultAccount() == account; if (account->accountType() == AccountType::MSA) { auto newAccount = MSALoginDialog::newAccount(m_parentWidget); if (newAccount != nullptr) { accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId()))); accounts->addAccount(newAccount); if (isDefault) { accounts->setDefaultAccount(newAccount); } if (m_accountToUse == account) { m_accountToUse = nullptr; decideAccount(); } return true; } } } return false; } void LaunchController::launchInstance() { Q_ASSERT(m_instance != nullptr); Q_ASSERT(m_session.get() != nullptr); if (!m_instance->reloadSettings()) { QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't load the instance profile.")); emitFailed(tr("Couldn't load the instance profile.")); return; } m_launcher = m_instance->createLaunchTask(m_session, m_targetToJoin); if (!m_launcher) { emitFailed(tr("Couldn't instantiate a launcher.")); return; } const auto* console = qobject_cast(m_parentWidget); const auto showConsole = m_instance->settings()->get("ShowConsole").toBool(); if (!console && showConsole) { APPLICATION->showInstanceWindow(m_instance); } connect(m_launcher, &LaunchTask::readyForLaunch, this, &LaunchController::readyForLaunch); connect(m_launcher, &LaunchTask::succeeded, this, &LaunchController::onSucceeded); connect(m_launcher, &LaunchTask::failed, this, &LaunchController::onFailed); connect(m_launcher, &LaunchTask::requestProgress, this, &LaunchController::onProgressRequested); // Prepend Online and Auth Status QString online_mode; if (m_actualLaunchMode == LaunchMode::Normal) { online_mode = "online"; // Prepend Server Status const QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; m_launcher->prependStep(makeShared(m_launcher, servers)); } else { online_mode = m_actualLaunchMode == LaunchMode::Demo ? "demo" : "offline"; } m_launcher->prependStep(makeShared(m_launcher, "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); // Prepend Version { auto versionString = QString("%1 version: %2 (%3)") .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString(), BuildConfig.BUILD_PLATFORM); m_launcher->prependStep(makeShared(m_launcher, versionString + "\n", MessageLevel::Launcher)); } m_launcher->start(); } void LaunchController::readyForLaunch() { if (!m_profiler) { m_launcher->proceed(); return; } QString error; if (!m_profiler->check(&error)) { m_launcher->abort(); emitFailed("Profiler startup failed!"); QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Profiler check for %1 failed: %2").arg(m_profiler->name(), error)); return; } BaseProfiler* profilerInstance = m_profiler->createProfiler(m_launcher->instance(), this); connect(profilerInstance, &BaseProfiler::readyToLaunch, [this](const QString& message) { QMessageBox msg(m_parentWidget); msg.setText(tr("The game launch is delayed until you press the " "button. This is the right time to setup the profiler, as the " "profiler server is running now.\n\n%1") .arg(message)); msg.setWindowTitle(tr("Waiting.")); msg.setIcon(QMessageBox::Information); msg.addButton(tr("&Launch"), QMessageBox::AcceptRole); msg.exec(); m_launcher->proceed(); }); connect(profilerInstance, &BaseProfiler::abortLaunch, [this](const QString& message) { QMessageBox msg; msg.setText(tr("Couldn't start the profiler: %1").arg(message)); msg.setWindowTitle(tr("Error")); msg.setIcon(QMessageBox::Critical); msg.addButton(QMessageBox::Ok); msg.setModal(true); msg.exec(); m_launcher->abort(); emitFailed("Profiler startup failed!"); }); profilerInstance->beginProfiling(m_launcher); } void LaunchController::onSucceeded() { emitSucceeded(); } void LaunchController::onFailed(QString reason) { if (m_instance->settings()->get("ShowConsoleOnError").toBool()) { APPLICATION->showInstanceWindow(m_instance, "console"); } emitFailed(std::move(reason)); } void LaunchController::onProgressRequested(Task* task) const { ProgressDialog progDialog(m_parentWidget); progDialog.setSkipButton(true, tr("Abort")); m_launcher->proceed(); progDialog.execWithTask(task); } bool LaunchController::abort() { if (!m_launcher) { return true; } if (!m_launcher->canAbort()) { return false; } auto response = CustomMessageBox::selectable(m_parentWidget, tr("Kill Minecraft?"), tr("This can cause the instance to get corrupted and should only be used if Minecraft " "is frozen for some reason"), QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) ->exec(); if (response == QMessageBox::Yes) { return m_launcher->abort(); } return false; } PrismLauncher-11.0.3/launcher/InstancePageProvider.h0000644000175100017510000000472115224505336022015 0ustar runnerrunner#pragma once #include #include #include "minecraft/MinecraftInstance.h" #include "ui/pages/BasePage.h" #include "ui/pages/BasePageProvider.h" #include "ui/pages/instance/InstanceSettingsPage.h" #include "ui/pages/instance/LogPage.h" #include "ui/pages/instance/ManagedPackPage.h" #include "ui/pages/instance/ModFolderPage.h" #include "ui/pages/instance/NotesPage.h" #include "ui/pages/instance/OtherLogsPage.h" #include "ui/pages/instance/ResourcePackPage.h" #include "ui/pages/instance/ScreenshotsPage.h" #include "ui/pages/instance/ServersPage.h" #include "ui/pages/instance/ShaderPackPage.h" #include "ui/pages/instance/TexturePackPage.h" #include "ui/pages/instance/VersionPage.h" #include "ui/pages/instance/WorldListPage.h" class InstancePageProvider : protected QObject, public BasePageProvider { Q_OBJECT public: explicit InstancePageProvider(BaseInstance* parent) { inst = parent; } virtual ~InstancePageProvider() = default; virtual QList getPages() override { QList values; values.append(new LogPage(inst)); MinecraftInstance* onesix = dynamic_cast(inst); values.append(new VersionPage(onesix)); values.append(ManagedPackPage::createPage(onesix)); auto modsPage = new ModFolderPage(onesix, onesix->loaderModList()); modsPage->setFilter("%1 (*.zip *.jar *.litemod *.nilmod)"); values.append(modsPage); values.append(new CoreModFolderPage(onesix, onesix->coreModList())); values.append(new NilModFolderPage(onesix, onesix->nilModList())); values.append(new ResourcePackPage(onesix, onesix->resourcePackList())); values.append(new GlobalDataPackPage(onesix)); values.append(new TexturePackPage(onesix, onesix->texturePackList())); values.append(new ShaderPackPage(onesix, onesix->shaderPackList())); values.append(new NotesPage(onesix)); values.append(new WorldListPage(onesix, onesix->worldList())); values.append(new ServersPage(onesix)); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); values.append(new InstanceSettingsPage(onesix)); values.append(new OtherLogsPage("logs", tr("Other Logs"), "Other-Logs", inst)); return values; } virtual QString dialogTitle() override { return tr("Edit Instance (%1)").arg(inst->name()); } protected: BaseInstance* inst; }; PrismLauncher-11.0.3/launcher/include/0000755000175100017510000000000015224505336017207 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/include/qtgui.pch.hpp0000644000175100017510000000140715224505336021624 0ustar runnerrunner#pragma once #ifndef PRISM_PRECOMPILED_QTGUI_HEADERS_H #define PRISM_PRECOMPILED_QTGUI_HEADERS_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #endif // PRISM_PRECOMPILED_GUI_HEADERS_H PrismLauncher-11.0.3/launcher/include/base.pch.hpp0000644000175100017510000000053515224505336021406 0ustar runnerrunner#pragma once #ifndef PRISM_PRECOMPILED_BASE_HEADERS_H #define PRISM_PRECOMPILED_BASE_HEADERS_H #include #include #include #include #include #include #include #include #include #include #endif // PRISM_PRECOMPILED_BASE_HEADERS_H PrismLauncher-11.0.3/launcher/include/qtcore.pch.hpp0000644000175100017510000000210015224505336021757 0ustar runnerrunner#pragma once #ifndef PRISM_PRECOMPILED_QTCORE_HEADERS_H #define PRISM_PRECOMPILED_QTCORE_HEADERS_H #include #include #include #include #include #include #include #include #include #include // collections #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #endif // PRISM_PRECOMPILED_QTCORE_HEADERS_H PrismLauncher-11.0.3/launcher/Json.cpp0000644000175100017510000002271115224505336017204 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Json.h" #include #include #include "FileSystem.h" namespace Json { void write(const QJsonDocument& doc, const QString& filename) { FS::write(filename, doc.toJson()); } void write(const QJsonObject& object, const QString& filename) { write(QJsonDocument(object), filename); } void write(const QJsonArray& array, const QString& filename) { write(QJsonDocument(array), filename); } QByteArray toText(const QJsonObject& obj) { return QJsonDocument(obj).toJson(QJsonDocument::Compact); } QByteArray toText(const QJsonArray& array) { return QJsonDocument(array).toJson(QJsonDocument::Compact); } static bool isBinaryJson(const QByteArray& data) { decltype(QJsonDocument::BinaryFormatTag) tag = QJsonDocument::BinaryFormatTag; return memcmp(data.constData(), &tag, sizeof(QJsonDocument::BinaryFormatTag)) == 0; } QJsonDocument requireDocument(const QByteArray& data, const QString& what) { if (isBinaryJson(data)) { // FIXME: Is this needed? throw JsonException(what + ": Invalid JSON. Binary JSON unsupported"); } else { QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { throw JsonException(what + ": Error parsing JSON: " + error.errorString()); } return doc; } } QJsonDocument requireDocument(const QString& filename, const QString& what) { return requireDocument(FS::read(filename), what); } QJsonObject requireObject(const QJsonDocument& doc, const QString& what) { if (!doc.isObject()) { throw JsonException(what + " is not an object"); } return doc.object(); } QJsonArray requireArray(const QJsonDocument& doc, const QString& what) { if (!doc.isArray()) { throw JsonException(what + " is not an array"); } return doc.array(); } QJsonDocument parseUntilGarbage(const QByteArray& json, QJsonParseError* error, QString* garbage) { auto doc = QJsonDocument::fromJson(json, error); if (error->error == QJsonParseError::GarbageAtEnd) { qsizetype offset = error->offset; QByteArray validJson = json.left(offset); doc = QJsonDocument::fromJson(validJson, error); if (garbage) *garbage = json.right(json.size() - offset); } return doc; } void writeString(QJsonObject& to, const QString& key, const QString& value) { if (!value.isEmpty()) { to.insert(key, value); } } void writeStringList(QJsonObject& to, const QString& key, const QStringList& values) { if (!values.isEmpty()) { QJsonArray array; for (auto value : values) { array.append(value); } to.insert(key, array); } } template <> QJsonValue toJson(const QUrl& url) { return QJsonValue(url.toString(QUrl::FullyEncoded)); } template <> QJsonValue toJson(const QByteArray& data) { return QJsonValue(QString::fromLatin1(data.toHex())); } template <> QJsonValue toJson(const QDateTime& datetime) { return QJsonValue(datetime.toString(Qt::ISODate)); } template <> QJsonValue toJson(const QDir& dir) { return QDir::current().relativeFilePath(dir.absolutePath()); } template <> QJsonValue toJson(const QUuid& uuid) { return uuid.toString(); } template <> QJsonValue toJson(const QVariant& variant) { return QJsonValue::fromVariant(variant); } template <> QByteArray requireIsType(const QJsonValue& value, const QString& what) { const QString string = value.toString(what); // ensure that the string can be safely cast to Latin1 if (string != QString::fromLatin1(string.toLatin1())) { throw JsonException(what + " is not encodable as Latin1"); } return QByteArray::fromHex(string.toLatin1()); } template <> QJsonArray requireIsType(const QJsonValue& value, const QString& what) { if (!value.isArray()) { throw JsonException(what + " is not an array"); } return value.toArray(); } template <> QString requireIsType(const QJsonValue& value, const QString& what) { if (!value.isString()) { throw JsonException(what + " is not a string"); } return value.toString(); } template <> bool requireIsType(const QJsonValue& value, const QString& what) { if (!value.isBool()) { throw JsonException(what + " is not a bool"); } return value.toBool(); } template <> double requireIsType(const QJsonValue& value, const QString& what) { if (!value.isDouble()) { throw JsonException(what + " is not a double"); } return value.toDouble(); } template <> int requireIsType(const QJsonValue& value, const QString& what) { const double doubl = requireIsType(value, what); if (fmod(doubl, 1) != 0) { throw JsonException(what + " is not an integer"); } return int(doubl); } template <> QDateTime requireIsType(const QJsonValue& value, const QString& what) { const QString string = requireIsType(value, what); const QDateTime datetime = QDateTime::fromString(string, Qt::ISODate); if (!datetime.isValid()) { throw JsonException(what + " is not a ISO formatted date/time value"); } return datetime; } template <> QUrl requireIsType(const QJsonValue& value, const QString& what) { const QString string = value.toString(what); if (string.isEmpty()) { return QUrl(); } const QUrl url = QUrl(string, QUrl::StrictMode); if (!url.isValid()) { throw JsonException(what + " is not a correctly formatted URL"); } return url; } template <> QDir requireIsType(const QJsonValue& value, const QString& what) { const QString string = requireIsType(value, what); // FIXME: does not handle invalid characters! return QDir::current().absoluteFilePath(string); } template <> QUuid requireIsType(const QJsonValue& value, const QString& what) { const QString string = requireIsType(value, what); const QUuid uuid = QUuid(string); if (uuid.toString() != string) // converts back => valid { throw JsonException(what + " is not a valid UUID"); } return uuid; } template <> QJsonObject requireIsType(const QJsonValue& value, const QString& what) { if (!value.isObject()) { throw JsonException(what + " is not an object"); } return value.toObject(); } template <> QVariant requireIsType(const QJsonValue& value, const QString& what) { if (value.isNull() || value.isUndefined()) { throw JsonException(what + " is null or undefined"); } return value.toVariant(); } template <> QJsonValue requireIsType(const QJsonValue& value, const QString& what) { if (value.isNull() || value.isUndefined()) { throw JsonException(what + " is null or undefined"); } return value; } QStringList toStringList(const QString& jsonString) { QJsonParseError parseError; QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError); if (parseError.error != QJsonParseError::NoError || !doc.isArray()) return {}; try { return requireIsArrayOf(doc); } catch (Json::JsonException&) { return {}; } } QString fromStringList(const QStringList& list) { QJsonArray array; for (const QString& str : list) { array.append(str); } QJsonDocument doc(toJsonArray(list)); return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); } QVariantMap toMap(const QString& jsonString) { QJsonParseError parseError; QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError); if (parseError.error != QJsonParseError::NoError || !doc.isObject()) return {}; QJsonObject obj = doc.object(); return obj.toVariantMap(); } QString fromMap(const QVariantMap& map) { QJsonObject obj = QJsonObject::fromVariantMap(map); QJsonDocument doc(obj); return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); } } // namespace Json PrismLauncher-11.0.3/launcher/RecursiveFileSystemWatcher.h0000644000175100017510000000217115224505336023230 0ustar runnerrunner#pragma once #include #include #include "Filter.h" class RecursiveFileSystemWatcher : public QObject { Q_OBJECT public: RecursiveFileSystemWatcher(QObject* parent); void setRootDir(const QDir& root); QDir rootDir() const { return m_root; } // WARNING: setting this to true may be bad for performance void setWatchFiles(bool watchFiles); bool watchFiles() const { return m_watchFiles; } void setMatcher(Filter matcher) { m_matcher = std::move(matcher); } QStringList files() const { return m_files; } signals: void filesChanged(); void fileChanged(const QString& path); public slots: void enable(); void disable(); private: QDir m_root; bool m_watchFiles = false; bool m_isEnabled = false; Filter m_matcher; QFileSystemWatcher* m_watcher; QStringList m_files; void setFiles(const QStringList& files); void addFilesToWatcherRecursive(const QDir& dir); QStringList scanRecursive(const QDir& dir); private slots: void fileChange(const QString& path); void directoryChange(const QString& path); }; PrismLauncher-11.0.3/launcher/InstanceDirUpdate.h0000644000175100017510000000354515224505336021312 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "BaseInstance.h" /// Update instanceRoot to make it sync with name/id; return newRoot if a directory rename happened QString askToUpdateInstanceDirName(BaseInstance* instance, const QString& oldName, const QString& newName, QWidget* parent); /// Check if there are linked instances, and display a warning; return true if the operation should proceed bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb); PrismLauncher-11.0.3/launcher/Usable.h0000644000175100017510000000175115224505336017154 0ustar runnerrunner#pragma once #include #include #include "QObjectPtr.h" class Usable; /** * Base class for things that can be used by multiple other things and we want to track the use count. * * @see UseLock */ class Usable { friend class UseLock; public: virtual ~Usable() {} std::size_t useCount() const { return m_useCount; } bool isInUse() const { return m_useCount > 0; } protected: virtual void decrementUses() { m_useCount--; } virtual void incrementUses() { m_useCount++; } private: std::size_t m_useCount = 0; }; /** * Lock class to use for keeping track of uses of other things derived from Usable * * @see Usable */ class UseLock { public: UseLock(Usable* usable) : m_usable(usable) { // this doesn't use shared pointer use count, because that wouldn't be correct. this count is separate. m_usable->incrementUses(); } ~UseLock() { m_usable->decrementUses(); } private: Usable* m_usable; }; PrismLauncher-11.0.3/launcher/MTPixmapCache.h0000644000175100017510000001616615224505336020372 0ustar runnerrunner#pragma once #include #include #include #include #include #include #define GET_TYPE() \ Qt::ConnectionType type; \ if (QThread::currentThread() != QCoreApplication::instance()->thread()) \ type = Qt::BlockingQueuedConnection; \ else \ type = Qt::DirectConnection; #define DEFINE_FUNC_NO_PARAM(NAME, RET_TYPE) \ static RET_TYPE NAME() \ { \ RET_TYPE ret; \ GET_TYPE() \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret)); \ return ret; \ } #define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, PARAM_1_TYPE) \ static RET_TYPE NAME(PARAM_1_TYPE p1) \ { \ RET_TYPE ret; \ GET_TYPE() \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1)); \ return ret; \ } #define DEFINE_FUNC_TWO_PARAM(NAME, RET_TYPE, PARAM_1_TYPE, PARAM_2_TYPE) \ static RET_TYPE NAME(PARAM_1_TYPE p1, PARAM_2_TYPE p2) \ { \ RET_TYPE ret; \ GET_TYPE() \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1), \ Q_ARG(PARAM_2_TYPE, p2)); \ return ret; \ } /** A wrapper around QPixmapCache with thread affinity with the main thread. */ class PixmapCache final : public QObject { Q_OBJECT public: PixmapCache(QObject* parent) : QObject(parent) {} ~PixmapCache() override = default; static PixmapCache& instance() { return *s_instance; } static void setInstance(PixmapCache* i) { s_instance = i; } public: DEFINE_FUNC_NO_PARAM(cacheLimit, int) DEFINE_FUNC_NO_PARAM(clear, bool) DEFINE_FUNC_TWO_PARAM(find, bool, const QString&, QPixmap*) DEFINE_FUNC_TWO_PARAM(find, bool, const QPixmapCache::Key&, QPixmap*) DEFINE_FUNC_TWO_PARAM(insert, bool, const QString&, const QPixmap&) DEFINE_FUNC_ONE_PARAM(insert, QPixmapCache::Key, const QPixmap&) DEFINE_FUNC_ONE_PARAM(remove, bool, const QString&) DEFINE_FUNC_ONE_PARAM(remove, bool, const QPixmapCache::Key&) DEFINE_FUNC_TWO_PARAM(replace, bool, const QPixmapCache::Key&, const QPixmap&) DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, int) DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool) DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, int) // NOTE: Every function returns something non-void to simplify the macros. private slots: int _cacheLimit() { return QPixmapCache::cacheLimit(); } bool _clear() { QPixmapCache::clear(); return true; } bool _find(const QString& key, QPixmap* pixmap) { return QPixmapCache::find(key, pixmap); } bool _find(const QPixmapCache::Key& key, QPixmap* pixmap) { return QPixmapCache::find(key, pixmap); } bool _insert(const QString& key, const QPixmap& pixmap) { return QPixmapCache::insert(key, pixmap); } QPixmapCache::Key _insert(const QPixmap& pixmap) { return QPixmapCache::insert(pixmap); } bool _remove(const QString& key) { QPixmapCache::remove(key); return true; } bool _remove(const QPixmapCache::Key& key) { QPixmapCache::remove(key); return true; } bool _replace(const QPixmapCache::Key& key, const QPixmap& pixmap) { return QPixmapCache::replace(key, pixmap); } bool _setCacheLimit(int n) { QPixmapCache::setCacheLimit(n); return true; } /** * Mark that a cache miss occurred because of a eviction if too many of these occur too fast the cache size is increased * @return if the cache size was increased */ bool _markCacheMissByEviciton() { static constexpr uint maxCache = static_cast(std::numeric_limits::max()) / 4; static constexpr uint step = 10240; static constexpr int oneSecond = 1000; auto now = QTime::currentTime(); if (!m_last_cache_miss_by_eviciton.isNull()) { auto diff = m_last_cache_miss_by_eviciton.msecsTo(now); if (diff < oneSecond) { // less than a second ago ++m_consecutive_fast_evicitons; } else { m_consecutive_fast_evicitons = 0; } } m_last_cache_miss_by_eviciton = now; if (m_consecutive_fast_evicitons >= m_consecutive_fast_evicitons_threshold) { // increase the cache size uint newSize = _cacheLimit() + step; if (newSize >= maxCache) { // increase it until you overflow :D newSize = maxCache; qDebug() << m_consecutive_fast_evicitons << tr("pixmap cache misses by eviction happened too fast, doing nothing as the cache size reached it's limit"); } else { qDebug() << m_consecutive_fast_evicitons << tr("pixmap cache misses by eviction happened too fast, increasing cache size to") << static_cast(newSize); } _setCacheLimit(static_cast(newSize)); m_consecutive_fast_evicitons = 0; return true; } return false; } bool _setFastEvictionThreshold(int threshold) { m_consecutive_fast_evicitons_threshold = threshold; return true; } private: static PixmapCache* s_instance; QTime m_last_cache_miss_by_eviciton; int m_consecutive_fast_evicitons = 0; int m_consecutive_fast_evicitons_threshold = 15; }; PrismLauncher-11.0.3/launcher/RecursiveFileSystemWatcher.cpp0000644000175100017510000000517215224505336023567 0ustar runnerrunner#include "RecursiveFileSystemWatcher.h" #include RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject* parent) : QObject(parent), m_watcher(new QFileSystemWatcher(this)) { connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &RecursiveFileSystemWatcher::fileChange); connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &RecursiveFileSystemWatcher::directoryChange); } void RecursiveFileSystemWatcher::setRootDir(const QDir& root) { bool wasEnabled = m_isEnabled; disable(); m_root = root; setFiles(scanRecursive(m_root)); if (wasEnabled) { enable(); } } void RecursiveFileSystemWatcher::setWatchFiles(const bool watchFiles) { bool wasEnabled = m_isEnabled; disable(); m_watchFiles = watchFiles; if (wasEnabled) { enable(); } } void RecursiveFileSystemWatcher::enable() { if (m_isEnabled) { return; } Q_ASSERT(m_root != QDir::root()); addFilesToWatcherRecursive(m_root); m_isEnabled = true; } void RecursiveFileSystemWatcher::disable() { if (!m_isEnabled) { return; } m_isEnabled = false; m_watcher->removePaths(m_watcher->files()); m_watcher->removePaths(m_watcher->directories()); } void RecursiveFileSystemWatcher::setFiles(const QStringList& files) { if (files != m_files) { m_files = files; emit filesChanged(); } } void RecursiveFileSystemWatcher::addFilesToWatcherRecursive(const QDir& dir) { m_watcher->addPath(dir.absolutePath()); for (const QString& directory : dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { addFilesToWatcherRecursive(dir.absoluteFilePath(directory)); } if (m_watchFiles) { for (const QFileInfo& info : dir.entryInfoList(QDir::Files)) { m_watcher->addPath(info.absoluteFilePath()); } } } QStringList RecursiveFileSystemWatcher::scanRecursive(const QDir& directory) { QStringList ret; if (!m_matcher) { return {}; } for (const QString& dir : directory.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden)) { ret.append(scanRecursive(directory.absoluteFilePath(dir))); } for (const QString& file : directory.entryList(QDir::Files | QDir::Hidden)) { auto relPath = m_root.relativeFilePath(directory.absoluteFilePath(file)); if (m_matcher(relPath)) { ret.append(relPath); } } return ret; } void RecursiveFileSystemWatcher::fileChange(const QString& path) { emit fileChanged(path); } void RecursiveFileSystemWatcher::directoryChange([[maybe_unused]] const QString& path) { setFiles(scanRecursive(m_root)); } PrismLauncher-11.0.3/launcher/BaseInstaller.h0000644000175100017510000000235015224505336020465 0ustar runnerrunner/* 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 #include "BaseVersion.h" class MinecraftInstance; class QDir; class QString; class QObject; class Task; class BaseVersion; class BaseInstaller { public: BaseInstaller(); virtual ~BaseInstaller() {}; bool isApplied(MinecraftInstance* on); virtual bool add(MinecraftInstance* to); virtual bool remove(MinecraftInstance* from); virtual Task* createInstallTask(MinecraftInstance* instance, BaseVersion::Ptr version, QObject* parent) = 0; protected: virtual QString id() const = 0; QString filename(const QString& root) const; QDir patchesDir(const QString& root) const; }; PrismLauncher-11.0.3/launcher/Exception.h0000644000175100017510000000402615224505336017675 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include class Exception : public std::exception { public: Exception(const QString& message) : std::exception(), m_message(message.toUtf8()) { qCritical() << "Exception:" << message; } Exception(const Exception& other) : std::exception(), m_message(other.m_message) {} virtual ~Exception() noexcept {} const char* what() const noexcept { return m_message.constData(); } QString cause() const { return QString::fromUtf8(m_message); } private: QByteArray m_message; }; PrismLauncher-11.0.3/launcher/JavaCommon.h0000644000175100017510000000260615224505336017773 0ustar runnerrunner#pragma once #include class QWidget; /** * Common UI bits for the java pages to use. */ namespace JavaCommon { bool checkJVMArgs(QString args, QWidget* parent); // Show a dialog saying that the Java binary was usable void javaWasOk(QWidget* parent, const JavaChecker::Result& result); // Show a dialog saying that the Java binary was not usable because of bad options void javaArgsWereBad(QWidget* parent, const JavaChecker::Result& result); // Show a dialog saying that the Java binary was not usable void javaBinaryWasBad(QWidget* parent, const JavaChecker::Result& result); // Show a dialog if we couldn't find Java Checker void javaCheckNotFound(QWidget* parent); class TestCheck : public QObject { Q_OBJECT public: TestCheck(QWidget* parent, QString path, QString args, int minMem, int maxMem, int permGen) : m_parent(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen) {} virtual ~TestCheck() = default; void run(); signals: void finished(); private slots: void checkFinished(const JavaChecker::Result& result); void checkFinishedWithArgs(const JavaChecker::Result& result); private: JavaChecker::Ptr checker; QWidget* m_parent = nullptr; QString m_path; QString m_args; int m_minMem = 0; int m_maxMem = 0; int m_permGen = 64; }; } // namespace JavaCommon PrismLauncher-11.0.3/launcher/HardwareInfo.cpp0000644000175100017510000002304715224505336020647 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2026 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "HardwareInfo.h" #include #include #if defined(Q_OS_MACOS) || defined(Q_OS_LINUX) namespace { QString afterColon(QString str) { return str.remove(0, str.indexOf(':') + 2).trimmed(); } template bool readFromOutput(const char* command, F function) { FILE* file = popen(command, "r"); // NOLINT(*-command-processor) if (!file) { qWarning().nospace() << "Could not execute command '" << command << "': " << strerror(errno); return false; } constexpr size_t bufferSize = 512; std::array buffer{}; while (fgets(buffer.data(), bufferSize, file) != nullptr) { function(buffer.data()); } const int exitCode = pclose(file); if (exitCode != 0) { if (exitCode == -1) { qWarning().nospace() << "Could not close stream for command '" << command << "': " << strerror(errno); } else { qWarning().nospace() << "Command '" << command << "' exited with code " << exitCode; } return false; } return true; } } // namespace #endif #ifdef Q_OS_WINDOWS #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include #include #include using Microsoft::WRL::ComPtr; QString HardwareInfo::cpuInfo() { const QSettings registry(R"(HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0)", QSettings::NativeFormat); return registry.value("ProcessorNameString").toString(); } uint64_t HardwareInfo::totalRamMiB() { MEMORYSTATUSEX status; status.dwLength = sizeof status; if (GlobalMemoryStatusEx(&status) == TRUE) { // transforming bytes -> mib return status.ullTotalPhys / 1024 / 1024; } qWarning() << "Could not get total RAM: GlobalMemoryStatusEx"; return 0; } uint64_t HardwareInfo::availableRamMiB() { MEMORYSTATUSEX status; status.dwLength = sizeof status; if (GlobalMemoryStatusEx(&status) == TRUE) { // transforming bytes -> mib return status.ullAvailPhys / 1024 / 1024; } qWarning() << "Could not get available RAM: GlobalMemoryStatusEx"; return 0; } QStringList HardwareInfo::gpuInfo() { ComPtr factory; HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory)); if (FAILED(hr)) { qWarning() << "Could not create DXGI factory:" << Qt::hex << hr; return { "GPU discovery failed: could not create DXGI factory" }; } UINT i = 0; ComPtr adapter; QStringList out; while (factory->EnumAdapterByGpuPreference(i, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&adapter)) != DXGI_ERROR_NOT_FOUND) { DXGI_ADAPTER_DESC desc; hr = adapter->GetDesc(&desc); if (SUCCEEDED(hr)) { out << "GPU: " + QString::fromWCharArray(desc.Description); // NOLINT(*-pro-bounds-array-to-pointer-decay, *-no-array-decay) } else { qWarning() << "Could not get DXGI adapter description:" << Qt::hex << hr; } ++i; } return out; } #elif defined(Q_OS_MACOS) #include "sys/sysctl.h" QString HardwareInfo::cpuInfo() { std::array buffer{}; size_t bufferSize = buffer.size(); if (sysctlbyname("machdep.cpu.brand_string", &buffer, &bufferSize, nullptr, 0) == 0) { return { buffer.data() }; } qWarning() << "Could not get CPU model: sysctlbyname"; return ""; } uint64_t HardwareInfo::totalRamMiB() { uint64_t memsize = 0; size_t memsizeSize = sizeof memsize; if (sysctlbyname("hw.memsize", &memsize, &memsizeSize, nullptr, 0) == 0) { // transforming bytes -> mib return memsize / 1024 / 1024; } qWarning() << "Could not get total RAM: sysctlbyname"; return 0; } uint64_t HardwareInfo::availableRamMiB() { return 0; } MacOSHardwareInfo::MemoryPressureLevel MacOSHardwareInfo::memoryPressureLevel() { uint32_t level = 0; size_t levelSize = sizeof level; if (sysctlbyname("kern.memorystatus_vm_pressure_level", &level, &levelSize, nullptr, 0) == 0) { return static_cast(level); } qWarning() << "Could not get memory pressure level: sysctlbyname"; return MemoryPressureLevel::Normal; } QString MacOSHardwareInfo::memoryPressureLevelName() { // The names are internal, users refer to levels by their graph colors in Activity Monitor switch (memoryPressureLevel()) { case MemoryPressureLevel::Normal: return "Green"; case MemoryPressureLevel::Warning: return "Yellow"; case MemoryPressureLevel::Critical: return "Red"; default: Q_ASSERT(false); return ""; } } QStringList HardwareInfo::gpuInfo() { QStringList out; const bool success = readFromOutput("system_profiler SPDisplaysDataType", [&](const QString& str) { // Chipset Model: Intel HD Graphics 620 if (str.contains("Chipset Model")) { out << "GPU: " + afterColon(str); } }); if (!success) { return { "GPU discovery failed: could not read from system_profiler" }; } return out; } #elif defined(Q_OS_LINUX) #include QString HardwareInfo::cpuInfo() { std::ifstream cpuin("/proc/cpuinfo"); for (std::string line; std::getline(cpuin, line);) { // model name : AMD Ryzen 7 5800X 8-Core Processor if (const QString str = QString::fromStdString(line); str.startsWith("model name")) { return afterColon(str); } } qWarning() << "Could not get CPU model: /proc/cpuinfo"; return "unknown"; } namespace { uint64_t readMemInfo(const QString& searchTarget) { std::ifstream memin("/proc/meminfo"); for (std::string line; std::getline(memin, line);) { // MemTotal: 16287480 kB if (const QString str = QString::fromStdString(line); str.startsWith(searchTarget)) { bool ok = false; const uint total = str.simplified().section(' ', 1, 1).toUInt(&ok); if (!ok) { qWarning() << "Could not read /proc/meminfo: failed to parse string:" << str; return 0; } // transforming kib -> mib return total / 1024; } } qWarning() << "Could not read /proc/meminfo: search target not found:" << searchTarget; return 0; } } // namespace uint64_t HardwareInfo::totalRamMiB() { return readMemInfo("MemTotal"); } uint64_t HardwareInfo::availableRamMiB() { return readMemInfo("MemAvailable"); } QStringList HardwareInfo::gpuInfo() { bool readingGpuInfo = false; QString gpu; QString driverInUse = "NONE"; QString driversAvailable = "NONE"; QStringList out; const bool success = readFromOutput("lspci -k", [&](const QString& str) { // clang-format off // 04:00.0 VGA compatible controller: Advanced Micro Devices, Inc. [AMD/ATI] Ellesmere [Radeon RX 470/480/570/570X/580/580X/590] (rev e7) // Subsystem: Sapphire Technology Limited Radeon RX 580 Pulse 4GB // Kernel driver in use: amdgpu // Kernel modules: amdgpu // clang-format on if (str.contains("VGA compatible controller") || str.contains("3D controller")) { readingGpuInfo = true; } else if (!str.startsWith('\t')) { if (readingGpuInfo) { out << QString("GPU: %1 (driver in use: %2; drivers available: %3)").arg(gpu, driverInUse, driversAvailable); driverInUse = "NONE"; driversAvailable = "NONE"; } readingGpuInfo = false; } if (!readingGpuInfo) { return; } const QString value = afterColon(str); if (str.contains("Subsystem")) { gpu = value; } if (str.contains("Kernel driver in use")) { driverInUse = value; } if (str.contains("Kernel modules")) { driversAvailable = value; } }); if (!success) { return { "GPU discovery failed: could not read from lspci" }; } return out; } #else QString HardwareInfo::cpuInfo() { return "unknown"; } #if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) #include uint64_t HardwareInfo::totalRamMiB() { uint64_t out = 0; const bool success = readFromOutput("sysctl hw.physmem", [&](const QString& str) { const uint64_t mem = str.mid(12).toULong(); // transforming kib -> mib out = mem / 1024; }); if (!success) { qWarning() << "Could not get total RAM: could not read from sysctl"; return 0; } return out; } #else uint64_t HardwareInfo::totalRamMiB() { return 0; } #endif uint64_t HardwareInfo::availableRamMiB() { return 0; } QStringList HardwareInfo::gpuInfo() { return { "GPU discovery failed: not implemented for this OS" }; } #endif PrismLauncher-11.0.3/launcher/screenshots/0000755000175100017510000000000015224505336020124 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/screenshots/ImgurAlbumCreation.cpp0000644000175100017510000000766615224505336024400 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ImgurAlbumCreation.h" #include #include #include #include #include #include #include #include #include "BuildConfig.h" #include "net/RawHeaderProxy.h" Net::NetRequest::Ptr ImgurAlbumCreation::make(std::shared_ptr output, QList screenshots) { auto up = makeShared(); up->m_url = BuildConfig.IMGUR_BASE_URL + "album"; up->m_sink.reset(new Sink(output)); up->m_screenshots = screenshots; up->addHeaderProxy(std::make_unique( QList{ { "Content-Type", "application/x-www-form-urlencoded" }, { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() }, { "Accept", "application/json" } })); return up; } QNetworkReply* ImgurAlbumCreation::getReply(QNetworkRequest& request) { QStringList hashes; for (auto shot : m_screenshots) { hashes.append(shot->m_imgurDeleteHash); } const QByteArray data = "deletehashes=" + hashes.join(',').toUtf8() + "&title=Minecraft%20Screenshots&privacy=hidden"; return m_network->post(request, data); } auto ImgurAlbumCreation::Sink::init(QNetworkRequest& request) -> Task::State { m_output.clear(); return Task::State::Running; } auto ImgurAlbumCreation::Sink::write(QByteArray& data) -> Task::State { m_output.append(data); return Task::State::Running; } auto ImgurAlbumCreation::Sink::abort() -> Task::State { m_output.clear(); m_fail_reason = "Aborted"; return Task::State::Failed; } auto ImgurAlbumCreation::Sink::finalize(QNetworkReply&) -> Task::State { QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << jsonError.errorString(); m_fail_reason = "Invalid json reply"; return Task::State::Failed; } auto object = doc.object(); if (!object.value("success").toBool()) { qDebug() << doc.toJson(); m_fail_reason = "Failed to create album"; return Task::State::Failed; } m_result->deleteHash = object.value("data").toObject().value("deletehash").toString(); m_result->id = object.value("data").toObject().value("id").toString(); return Task::State::Succeeded; } PrismLauncher-11.0.3/launcher/screenshots/ImgurUpload.cpp0000644000175100017510000001126215224505336023062 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ImgurUpload.h" #include "BuildConfig.h" #include "net/RawHeaderProxy.h" #include #include #include #include #include #include #include #include QNetworkReply* ImgurUpload::getReply(QNetworkRequest& request) { auto file = new QFile(m_fileInfo.absoluteFilePath(), this); if (!file->open(QFile::ReadOnly)) { emitFailed(tr("Could not open file %1 for reading: %2").arg(m_fileInfo.absoluteFilePath()).arg(file->errorString())); return nullptr; } QHttpMultiPart* multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this); file->setParent(multipart); QHttpPart filePart; filePart.setBodyDevice(file); filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png"); filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"image\"; filename=\"" + file->fileName() + "\""); multipart->append(filePart); QHttpPart typePart; typePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"type\""); typePart.setBody("file"); multipart->append(typePart); QHttpPart namePart; namePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"title\""); namePart.setBody(m_fileInfo.baseName().toUtf8()); multipart->append(namePart); return m_network->post(request, multipart); } auto ImgurUpload::Sink::init(QNetworkRequest& request) -> Task::State { m_output.clear(); return Task::State::Running; } auto ImgurUpload::Sink::write(QByteArray& data) -> Task::State { m_output.append(data); return Task::State::Running; } auto ImgurUpload::Sink::abort() -> Task::State { m_output.clear(); m_fail_reason = "Aborted"; return Task::State::Failed; } auto ImgurUpload::Sink::finalize(QNetworkReply&) -> Task::State { QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "imgur server did not reply with JSON" << jsonError.errorString(); m_fail_reason = "Invalid json reply"; return Task::State::Failed; } auto object = doc.object(); if (!object.value("success").toBool()) { qDebug() << "Screenshot upload not successful:" << doc.toJson(); m_fail_reason = "Screenshot was not uploaded successfully"; return Task::State::Failed; } m_shot->m_imgurId = object.value("data").toObject().value("id").toString(); m_shot->m_url = object.value("data").toObject().value("link").toString(); m_shot->m_imgurDeleteHash = object.value("data").toObject().value("deletehash").toString(); return Task::State::Succeeded; } Net::NetRequest::Ptr ImgurUpload::make(ScreenShot::Ptr m_shot) { auto up = makeShared(m_shot->m_file); up->m_url = BuildConfig.IMGUR_BASE_URL + "image"; up->m_sink.reset(new Sink(m_shot)); up->addHeaderProxy(std::make_unique(QList{ { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() }, { "Accept", "application/json" } })); return up; } PrismLauncher-11.0.3/launcher/screenshots/ImgurAlbumCreation.h0000644000175100017510000000474315224505336024036 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Screenshot.h" #include "net/NetRequest.h" class ImgurAlbumCreation : public Net::NetRequest { public: virtual ~ImgurAlbumCreation() = default; struct Result { QString deleteHash; QString id; }; class Sink : public Net::Sink { public: Sink(std::shared_ptr res) : m_result(res) {}; virtual ~Sink() = default; public: auto init(QNetworkRequest& request) -> Task::State override; auto write(QByteArray& data) -> Task::State override; auto abort() -> Task::State override; auto finalize(QNetworkReply& reply) -> Task::State override; auto hasLocalData() -> bool override { return false; } private: std::shared_ptr m_result; QByteArray m_output; }; static NetRequest::Ptr make(std::shared_ptr output, QList screenshots); QNetworkReply* getReply(QNetworkRequest& request) override; private: QList m_screenshots; }; PrismLauncher-11.0.3/launcher/screenshots/Screenshot.h0000644000175100017510000000046415224505336022416 0ustar runnerrunner#pragma once #include #include #include #include struct ScreenShot { using Ptr = std::shared_ptr; ScreenShot(QFileInfo file) { m_file = file; } QFileInfo m_file; QString m_url; QString m_imgurId; QString m_imgurDeleteHash; }; PrismLauncher-11.0.3/launcher/screenshots/ImgurUpload.h0000644000175100017510000000461315224505336022531 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include "Screenshot.h" #include "net/NetRequest.h" class ImgurUpload : public Net::NetRequest { public: class Sink : public Net::Sink { public: Sink(ScreenShot::Ptr shot) : m_shot(shot) {}; virtual ~Sink() = default; public: auto init(QNetworkRequest& request) -> Task::State override; auto write(QByteArray& data) -> Task::State override; auto abort() -> Task::State override; auto finalize(QNetworkReply& reply) -> Task::State override; auto hasLocalData() -> bool override { return false; } private: ScreenShot::Ptr m_shot; QByteArray m_output; }; ImgurUpload(QFileInfo info) : m_fileInfo(info) {} virtual ~ImgurUpload() = default; static NetRequest::Ptr make(ScreenShot::Ptr m_shot); private: virtual QNetworkReply* getReply(QNetworkRequest&) override; const QFileInfo m_fileInfo; }; PrismLauncher-11.0.3/launcher/BaseVersion.h0000644000175100017510000000306115224505336020155 0ustar runnerrunner/* 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 #include #include /*! * An abstract base class for versions. */ class BaseVersion { public: // TODO: delete using Ptr = std::shared_ptr; virtual ~BaseVersion() {} /*! * A string used to identify this version in config files. * This should be unique within the version list or shenanigans will occur. */ virtual QString descriptor() const = 0; /*! * The name of this version as it is displayed to the user. * For example: "1.5.1" */ virtual QString name() const = 0; /*! * This should return a string that describes * the kind of version this is (Stable, Beta, Snapshot, whatever) */ virtual QString typeString() const = 0; virtual bool operator<(BaseVersion& a) const { return name() < a.name(); } virtual bool operator>(BaseVersion& a) const { return name() > a.name(); } }; Q_DECLARE_METATYPE(BaseVersion::Ptr) PrismLauncher-11.0.3/launcher/ApplicationMessage.cpp0000644000175100017510000000452215224505336022043 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ApplicationMessage.h" #include #include #include "Json.h" void ApplicationMessage::parse(const QByteArray& input) { auto doc = Json::requireDocument(input, "ApplicationMessage"); auto root = Json::requireObject(doc, "ApplicationMessage"); command = root.value("command").toString(); args.clear(); auto parsedArgs = root.value("args").toObject(); for (auto iter = parsedArgs.constBegin(); iter != parsedArgs.constEnd(); iter++) { args.insert(iter.key(), iter.value().toString()); } } QByteArray ApplicationMessage::serialize() { QJsonObject root; root.insert("command", command); QJsonObject outArgs; for (auto iter = args.constBegin(); iter != args.constEnd(); iter++) { outArgs.insert(iter.key(), iter.value()); } root.insert("args", outArgs); return Json::toText(root); } PrismLauncher-11.0.3/launcher/filelink/0000755000175100017510000000000015224505336017361 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/filelink/filelink_main.cpp0000644000175100017510000000263015224505336022667 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "FileLink.h" #if defined Q_OS_WIN32 #include "console/WindowsConsole.h" #endif int main(int argc, char* argv[]) { #if defined Q_OS_WIN32 // attach the parent console console::WindowsConsoleGuard _consoleGuard; #endif FileLinkApp ldh(argc, argv); switch (ldh.status()) { case FileLinkApp::Starting: case FileLinkApp::Initialized: { return ldh.exec(); } case FileLinkApp::Failed: return 1; case FileLinkApp::Succeeded: return 0; default: return -1; } } PrismLauncher-11.0.3/launcher/filelink/FileLink.h0000644000175100017510000000331715224505336021233 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include #include #include #include #include #include #include #include #include #define PRISM_EXTERNAL_EXE #include "FileSystem.h" class FileLinkApp : public QCoreApplication { Q_OBJECT public: enum Status { Starting, Failed, Succeeded, Initialized }; FileLinkApp(int& argc, char** argv); virtual ~FileLinkApp(); Status status() const { return m_status; } private: void joinServer(QString server); void readPathPairs(); void runLink(); void sendResults(); Status m_status = Status::Starting; bool m_useHardLinks = false; QDateTime m_startTime; QLocalSocket socket; QDataStream in; quint32 blockSize; QList m_links_to_make; QList m_path_results; }; PrismLauncher-11.0.3/launcher/filelink/filelink.exe.manifest0000644000175100017510000000177515224505336023500 0ustar runnerrunner PrismLauncher-11.0.3/launcher/filelink/FileLink.cpp0000644000175100017510000001673615224505336021577 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "FileLink.h" #include "BuildConfig.h" #include "StringUtils.h" #include #include #include #include #include #include namespace fs = std::filesystem; FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this)) { setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME + "FileLink"); setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); // Commandline parsing QCommandLineParser parser; parser.setApplicationDescription(QObject::tr("a batch MKLINK program for windows to be used with prismlauncher")); parser.addOptions({ { { "s", "server" }, "Join the specified server on launch", "pipe name" }, { { "H", "hard" }, "use hard links instead of symbolic", "true/false" } }); parser.addHelpOption(); parser.addVersionOption(); parser.process(arguments()); QString serverToJoin = parser.value("server"); m_useHardLinks = QVariant(parser.value("hard")).toBool(); qDebug() << "link program launched"; if (!serverToJoin.isEmpty()) { qDebug() << "joining server" << serverToJoin; joinServer(serverToJoin); } else { qDebug() << "no server to join"; m_status = Failed; exit(); } } void FileLinkApp::joinServer(QString server) { blockSize = 0; in.setDevice(&socket); connect(&socket, &QLocalSocket::connected, this, []() { qDebug() << "connected to server"; }); connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs); connect(&socket, &QLocalSocket::errorOccurred, this, [this](QLocalSocket::LocalSocketError socketError) { m_status = Failed; switch (socketError) { case QLocalSocket::ServerNotFoundError: qDebug() << ("The host was not found. Please make sure " "that the server is running and that the " "server name is correct."); break; case QLocalSocket::ConnectionRefusedError: qDebug() << ("The connection was refused by the peer. " "Make sure the server is running, " "and check that the server name " "is correct."); break; case QLocalSocket::PeerClosedError: qDebug() << ("The connection was closed by the peer. "); break; default: qDebug() << "The following error occurred:" << socket.errorString(); } }); connect(&socket, &QLocalSocket::disconnected, this, [this]() { qDebug() << "disconnected from server, should exit"; m_status = Succeeded; exit(); }); socket.connectToServer(server); } void FileLinkApp::runLink() { std::error_code os_err; qDebug() << "creating links"; for (auto link : m_links_to_make) { QString src_path = link.src; QString dst_path = link.dst; FS::ensureFilePathExists(dst_path); if (m_useHardLinks) { qDebug() << "making hard link:" << src_path << "to" << dst_path; fs::create_hard_link(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err); } else if (fs::is_directory(StringUtils::toStdString(src_path))) { qDebug() << "making directory_symlink:" << src_path << "to" << dst_path; fs::create_directory_symlink(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err); } else { qDebug() << "making symlink:" << src_path << "to" << dst_path; fs::create_symlink(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err); } if (os_err) { qWarning() << "Failed to link files:" << QString::fromStdString(os_err.message()); qDebug() << "Source file:" << src_path; qDebug() << "Destination file:" << dst_path; qDebug() << "Error category:" << os_err.category().name(); qDebug() << "Error code:" << os_err.value(); FS::LinkResult result = { src_path, dst_path, QString::fromStdString(os_err.message()), os_err.value() }; m_path_results.append(result); } else { FS::LinkResult result = { src_path, dst_path, "", 0 }; m_path_results.append(result); } } sendResults(); qDebug() << "done, should exit soon"; } void FileLinkApp::sendResults() { // construct block of data to send QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); qint32 blocksize = quint32(sizeof(quint32)); for (auto result : m_path_results) { blocksize += quint32(result.src.size()); blocksize += quint32(result.dst.size()); blocksize += quint32(result.err_msg.size()); blocksize += quint32(sizeof(quint32)); } qDebug() << "About to write block of size:" << blocksize; out << blocksize; out << quint32(m_path_results.length()); for (auto result : m_path_results) { out << result.src; out << result.dst; out << result.err_msg; out << quint32(result.err_value); } qint64 byteswritten = socket.write(block); bool bytesflushed = socket.flush(); qDebug() << "block flushed" << byteswritten << bytesflushed; } void FileLinkApp::readPathPairs() { m_links_to_make.clear(); qDebug() << "Reading path pairs from server"; qDebug() << "bytes available" << socket.bytesAvailable(); if (blockSize == 0) { // Relies on the fact that QDataStream serializes a quint32 into // sizeof(quint32) bytes if (socket.bytesAvailable() < (int)sizeof(quint32)) return; qDebug() << "reading block size"; in >> blockSize; } qDebug() << "blocksize is" << blockSize; qDebug() << "bytes available" << socket.bytesAvailable(); if (socket.bytesAvailable() < blockSize || in.atEnd()) return; quint32 numLinks; in >> numLinks; qDebug() << "numLinks" << numLinks; for (quint32 i = 0; i < numLinks; i++) { FS::LinkPair pair; in >> pair.src; in >> pair.dst; qDebug() << "link" << pair.src << "to" << pair.dst; m_links_to_make.append(pair); } runLink(); } FileLinkApp::~FileLinkApp() { qDebug() << "link program shutting down"; // Shut down logger by setting the logger function to nothing qInstallMessageHandler(nullptr); } PrismLauncher-11.0.3/launcher/DataMigrationTask.cpp0000644000175100017510000000567615224505336021654 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only #include "DataMigrationTask.h" #include "FileSystem.h" #include #include #include #include DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, Filter pathMatcher) : Task(), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath) { m_copy.matcher(m_pathMatcher).whitelist(true); } void DataMigrationTask::executeTask() { setStatus(tr("Scanning files...")); // 1. Scan // Check how many files we gotta copy m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { return m_copy(true); // dry run to collect amount of files }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted); m_copyFutureWatcher.setFuture(m_copyFuture); } void DataMigrationTask::dryRunFinished() { disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted); if (!m_copyFuture.isValid() || !m_copyFuture.result()) { emitFailed(tr("Failed to scan source path.")); return; } // 2. Copy // Actually copy all files now. m_toCopy = m_copy.totalCopied(); connect(&m_copy, &FS::copy::fileCopied, [&, this](const QString& relativeName) { QString shortenedName = relativeName; // shorten the filename to hopefully fit into one line if (shortenedName.length() > 50) shortenedName = relativeName.left(20) + "…" + relativeName.right(29); setProgress(m_copy.totalCopied(), m_toCopy); setStatus(tr("Copying %1…").arg(shortenedName)); }); m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { return m_copy(false); // actually copy now }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted); m_copyFutureWatcher.setFuture(m_copyFuture); } void DataMigrationTask::dryRunAborted() { emitAborted(); } void DataMigrationTask::copyFinished() { disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted); if (!m_copyFuture.isValid() || !m_copyFuture.result()) { emitFailed(tr("Some paths could not be copied!")); return; } emitSucceeded(); } void DataMigrationTask::copyAborted() { emitAborted(); } PrismLauncher-11.0.3/launcher/SysInfo.cpp0000644000175100017510000000713315224505336017666 0ustar runnerrunner // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 r58Playz * Copyright (C) 2024 timoreo * Copyright (C) 2024 Trial97 * Copyright (C) 2025 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include "HardwareInfo.h" #ifdef Q_OS_MACOS #include bool rosettaDetect() { int ret = 0; size_t size = sizeof(ret); if (sysctlbyname("sysctl.proc_translated", &ret, &size, nullptr, 0) == -1) { return false; } return ret == 1; } #endif namespace SysInfo { QString currentSystem() { #if defined(Q_OS_LINUX) return "linux"; #elif defined(Q_OS_MACOS) return "osx"; #elif defined(Q_OS_WINDOWS) return "windows"; #elif defined(Q_OS_FREEBSD) return "freebsd"; #elif defined(Q_OS_OPENBSD) return "openbsd"; #else return "unknown"; #endif } QString useQTForArch() { #if defined(Q_OS_MACOS) && !defined(Q_PROCESSOR_ARM) if (rosettaDetect()) { return "arm64"; } else { return "x86_64"; } #endif return QSysInfo::currentCpuArchitecture(); } int defaultMaxJvmMem() { // If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB if (const uint64_t totalRAM = HardwareInfo::totalRamMiB(); totalRAM < (4096 * 1.5)) return totalRAM / 1.5; else return 4096; } QString getSupportedJavaArchitecture() { auto sys = currentSystem(); auto arch = useQTForArch(); if (sys == "windows") { if (arch == "x86_64") return "windows-x64"; if (arch == "i386") return "windows-x86"; // Unknown, maybe arm, appending arch return "windows-" + arch; } if (sys == "osx") { if (arch == "arm64") return "mac-os-arm64"; if (arch.contains("64")) return "mac-os-x64"; if (arch.contains("86")) return "mac-os-x86"; // Unknown, maybe something new, appending arch return "mac-os-" + arch; } else if (sys == "linux") { if (arch == "x86_64") return "linux-x64"; if (arch == "i386") return "linux-x86"; // will work for arm32 arm(64) return "linux-" + arch; } return {}; } } // namespace SysInfo PrismLauncher-11.0.3/launcher/SysInfo.h0000644000175100017510000000032315224505336017325 0ustar runnerrunner#pragma once #include #include namespace SysInfo { QString currentSystem(); QString useQTForArch(); QString getSupportedJavaArchitecture(); int defaultMaxJvmMem(); } // namespace SysInfo PrismLauncher-11.0.3/launcher/FileSystem.cpp0000644000175100017510000015650215224505336020365 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "FileSystem.h" #include #include #include "BuildConfig.h" #include #include #include #include #include #include #include #include #include #include #include #include "DesktopServices.h" #include "PSaveFile.h" #include "StringUtils.h" #if defined Q_OS_WIN32 #define NOMINMAX #define WIN32_LEAN_AND_MEAN #include #include #include #include #include #include #include #include // for ShellExecute #include #include #include #else #include #endif #include namespace fs = std::filesystem; // clone #if defined(Q_OS_LINUX) #include #include /* Definition of FICLONE* constants */ #include #include #include #elif defined(Q_OS_MACOS) #include #include #elif defined(Q_OS_WIN) // winbtrfs clone vs rundll32 shellbtrfs.dll,ReflinkCopy #include #include #include #include // refs #include #if defined(__MINGW32__) #include #endif #endif #if defined(Q_OS_WIN) #if defined(__MINGW32__) // Avoid re-defining structs retroactively added to MinGW // https://github.com/mingw-w64/mingw-w64/issues/90#issuecomment-2829284729 #if __MINGW64_VERSION_MAJOR < 13 struct _DUPLICATE_EXTENTS_DATA { HANDLE FileHandle; LARGE_INTEGER SourceFileOffset; LARGE_INTEGER TargetFileOffset; LARGE_INTEGER ByteCount; }; using DUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA; using PDUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA*; #endif struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER { WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 WORD Reserved; // Must be 0 DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx DWORD ChecksumChunkSizeInBytes; DWORD ClusterSizeInBytes; }; using FSCTL_GET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER; using PFSCTL_GET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER*; struct _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER { WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 WORD Reserved; // Must be 0 DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx }; using FSCTL_SET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER; using PFSCTL_SET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER*; #endif #ifndef FSCTL_DUPLICATE_EXTENTS_TO_FILE #define FSCTL_DUPLICATE_EXTENTS_TO_FILE CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 209, METHOD_BUFFERED, FILE_WRITE_DATA) #endif #ifndef FSCTL_GET_INTEGRITY_INFORMATION #define FSCTL_GET_INTEGRITY_INFORMATION \ CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 159, METHOD_BUFFERED, FILE_ANY_ACCESS) // FSCTL_GET_INTEGRITY_INFORMATION_BUFFER #endif #ifndef FSCTL_SET_INTEGRITY_INFORMATION #define FSCTL_SET_INTEGRITY_INFORMATION \ CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 160, METHOD_BUFFERED, FILE_READ_DATA | FILE_WRITE_DATA) // FSCTL_SET_INTEGRITY_INFORMATION_BUFFER #endif #ifndef ERROR_NOT_CAPABLE #define ERROR_NOT_CAPABLE 775L #endif #ifndef ERROR_BLOCK_TOO_MANY_REFERENCES #define ERROR_BLOCK_TOO_MANY_REFERENCES 347L #endif #endif namespace FS { void ensureExists(const QDir& dir) { if (!QDir().mkpath(dir.absolutePath())) { throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + dir.absolutePath() + ")"); } } void write(const QString& filename, const QByteArray& data) { ensureExists(QFileInfo(filename).dir()); PSaveFile file(filename); if (!file.open(PSaveFile::WriteOnly)) { throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } if (data.size() != file.write(data)) { throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); } if (!file.commit()) { throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString()); } } void appendSafe(const QString& filename, const QByteArray& data) { ensureExists(QFileInfo(filename).dir()); QByteArray buffer; try { buffer = read(filename); } catch (FileSystemException&) { buffer = QByteArray(); } buffer.append(data); PSaveFile file(filename); if (!file.open(PSaveFile::WriteOnly)) { throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } if (buffer.size() != file.write(buffer)) { throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); } if (!file.commit()) { throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString()); } } void append(const QString& filename, const QByteArray& data) { ensureExists(QFileInfo(filename).dir()); QFile file(filename); if (!file.open(QFile::Append)) { throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } if (data.size() != file.write(data)) { throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); } } QByteArray read(const QString& filename) { QFile file(filename); if (!file.open(QFile::ReadOnly)) { throw FileSystemException("Unable to open " + filename + " for reading: " + file.errorString()); } const qint64 size = file.size(); QByteArray data(int(size), 0); const qint64 ret = file.read(data.data(), size); if (ret == -1 || ret != size) { throw FileSystemException("Error reading data from " + filename + ": " + file.errorString()); } return data; } bool updateTimestamp(const QString& filename) { #ifdef Q_OS_WIN32 std::wstring filename_utf_16 = filename.toStdWString(); return (_wutime64(filename_utf_16.c_str(), nullptr) == 0); #else QByteArray filenameBA = QFile::encodeName(filename); return (utime(filenameBA.data(), nullptr) == 0); #endif } bool ensureFilePathExists(QString filenamepath) { QFileInfo a(filenamepath); QDir dir; QString ensuredPath = a.path(); bool success = dir.mkpath(ensuredPath); return success; } bool ensureFolderPathExists(const QFileInfo folderPath) { QDir dir; QString ensuredPath = folderPath.filePath(); if (folderPath.exists()) return true; bool success = dir.mkpath(ensuredPath); return success; } bool ensureFolderPathExists(const QString folderPathName) { return ensureFolderPathExists(QFileInfo(folderPathName)); } bool copyFileAttributes(QString src, QString dst) { #ifdef Q_OS_WIN32 auto attrs = GetFileAttributesW(src.toStdWString().c_str()); if (attrs == INVALID_FILE_ATTRIBUTES) return false; return SetFileAttributesW(dst.toStdWString().c_str(), attrs); #else Q_UNUSED(src); Q_UNUSED(dst); #endif return true; } // needs folders to exists void copyFolderAttributes(QString src, QString dst, QString relative) { auto path = PathCombine(src, relative); QDir dsrc(src); while ((path = QFileInfo(path).path()).length() >= src.length()) { auto dst_path = PathCombine(dst, dsrc.relativeFilePath(path)); copyFileAttributes(path, dst_path); } } /** * @brief Copies a directory and it's contents from src to dest * @param offset subdirectory form src to copy to dest * @return if there was an error during the filecopy */ bool copy::operator()(const QString& offset, bool dryRun) { using copy_opts = fs::copy_options; m_copied = 0; // reset counter m_failedPaths.clear(); // NOTE always deep copy on windows. the alternatives are too messy. #if defined Q_OS_WIN32 m_followSymlinks = true; #endif auto src = PathCombine(m_src.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset); std::error_code err; fs::copy_options opt = copy_opts::none; // The default behavior is to follow symlinks if (!m_followSymlinks) opt |= copy_opts::copy_symlinks; if (m_overwrite) opt |= copy_opts::overwrite_existing; // Function that'll do the actual copying auto copy_file = [this, dryRun, src, dst, opt, &err](QString src_path, QString relative_dst_path) { if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) return; auto dst_path = PathCombine(dst, relative_dst_path); if (!dryRun) { ensureFilePathExists(dst_path); #ifdef Q_OS_WIN32 copyFolderAttributes(src, dst, relative_dst_path); #endif fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err); } if (err) { qWarning() << "Failed to copy files:" << QString::fromStdString(err.message()); qDebug() << "Source file:" << src_path; qDebug() << "Destination file:" << dst_path; m_failedPaths.append(dst_path); emit copyFailed(relative_dst_path); return; } m_copied++; emit fileCopied(relative_dst_path); }; // We can't use copy_opts::recursive because we need to take into account the // blacklisted paths, so we iterate over the source directory, and if there's no blacklist // match, we copy the file. QDir src_dir(src); QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories); while (source_it.hasNext()) { auto src_path = source_it.next(); auto relative_path = src_dir.relativeFilePath(src_path); copy_file(src_path, relative_path); } // If the root src is not a directory, the previous iterator won't run. if (!fs::is_directory(StringUtils::toStdString(src))) copy_file(src, ""); return err.value() == 0; } /// qDebug print support for the LinkPair struct QDebug operator<<(QDebug debug, const LinkPair& lp) { QDebugStateSaver saver(debug); debug.nospace() << "LinkPair{ src: " << lp.src << " , dst: " << lp.dst << " }"; return debug; } bool create_link::operator()(const QString& offset, bool dryRun) { m_linked = 0; // reset counter m_path_results.clear(); m_links_to_make.clear(); m_path_results.clear(); make_link_list(offset); if (!dryRun) return make_links(); return true; } /** * @brief Make a list of all the links to make * @param offset subdirectory of src to link to dest */ void create_link::make_link_list(const QString& offset) { for (auto pair : m_path_pairs) { const QString& srcPath = pair.src; const QString& dstPath = pair.dst; auto src = PathCombine(QDir(srcPath).absolutePath(), offset); auto dst = PathCombine(QDir(dstPath).absolutePath(), offset); // you can't hard link a directory so make sure if we deal with a directory we do so recursively if (m_useHardLinks) m_recursive = true; // Function that'll do the actual linking auto link_file = [this, dst](QString src_path, QString relative_dst_path) { if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) { qDebug() << "path" << relative_dst_path << "in black list or not in whitelist"; return; } auto dst_path = PathCombine(dst, relative_dst_path); LinkPair link = { src_path, dst_path }; m_links_to_make.append(link); }; if ((!m_recursive) || !fs::is_directory(StringUtils::toStdString(src))) { if (m_debug) qDebug() << "linking single file or dir:" << src << "to" << dst; link_file(src, ""); } else { if (m_debug) qDebug().nospace() << "linking recursively: " << src << " to " << dst << ", max_depth: " << m_max_depth; QDir src_dir(src); QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories); QStringList linkedPaths; while (source_it.hasNext()) { auto src_path = source_it.next(); auto relative_path = src_dir.relativeFilePath(src_path); if (m_max_depth >= 0 && pathDepth(relative_path) > m_max_depth) { relative_path = pathTruncate(relative_path, m_max_depth); src_path = src_dir.filePath(relative_path); if (linkedPaths.contains(src_path)) { continue; } } linkedPaths.append(src_path); link_file(src_path, relative_path); } } } } bool create_link::make_links() { for (auto link : m_links_to_make) { QString src_path = link.src; QString dst_path = link.dst; auto src_path_std = StringUtils::toStdString(link.src); auto dst_path_std = StringUtils::toStdString(link.dst); ensureFilePathExists(dst_path); if (m_useHardLinks) { if (m_debug) qDebug() << "making hard link:" << src_path << "to" << dst_path; fs::create_hard_link(src_path_std, dst_path_std, m_os_err); } else if (fs::is_directory(src_path_std)) { if (m_debug) qDebug() << "making directory_symlink:" << src_path << "to" << dst_path; fs::create_directory_symlink(src_path_std, dst_path_std, m_os_err); } else { if (m_debug) qDebug() << "making symlink:" << src_path << "to" << dst_path; fs::create_symlink(src_path_std, dst_path_std, m_os_err); } if (m_os_err) { qWarning() << "Failed to link files:" << QString::fromStdString(m_os_err.message()); qDebug() << "Source file:" << src_path; qDebug() << "Destination file:" << dst_path; qDebug() << "Error category:" << m_os_err.category().name(); qDebug() << "Error code:" << m_os_err.value(); emit linkFailed(src_path, dst_path, QString::fromStdString(m_os_err.message()), m_os_err.value()); } else { m_linked++; emit fileLinked(src_path, dst_path); } if (m_os_err) return false; } return true; } void create_link::runPrivileged(const QString& offset) { m_linked = 0; // reset counter m_path_results.clear(); m_links_to_make.clear(); bool gotResults = false; make_link_list(offset); QString serverName = BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink_server" + StringUtils::getRandomAlphaNumeric(); connect(&m_linkServer, &QLocalServer::newConnection, this, [this, &gotResults]() { qDebug() << "Client connected, sending out pairs"; // construct block of data to send QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); qint32 blocksize = quint32(sizeof(quint32)); for (auto link : m_links_to_make) { blocksize += quint32(link.src.size()); blocksize += quint32(link.dst.size()); } qDebug() << "About to write block of size:" << blocksize; out << blocksize; out << quint32(m_links_to_make.length()); for (auto link : m_links_to_make) { out << link.src; out << link.dst; } QLocalSocket* clientConnection = m_linkServer.nextPendingConnection(); connect(clientConnection, &QLocalSocket::disconnected, clientConnection, &QLocalSocket::deleteLater); connect(clientConnection, &QLocalSocket::readyRead, this, [&, clientConnection]() { QDataStream in; quint32 blockSize = 0; in.setDevice(clientConnection); qDebug() << "Reading path results from client"; qDebug() << "bytes available" << clientConnection->bytesAvailable(); // Relies on the fact that QDataStream serializes a quint32 into // sizeof(quint32) bytes if (clientConnection->bytesAvailable() < (int)sizeof(quint32)) return; qDebug() << "reading block size"; in >> blockSize; qDebug() << "blocksize is" << blockSize; qDebug() << "bytes available" << clientConnection->bytesAvailable(); if (clientConnection->bytesAvailable() < blockSize || in.atEnd()) return; quint32 numResults; in >> numResults; qDebug() << "numResults" << numResults; for (quint32 i = 0; i < numResults; i++) { FS::LinkResult result; in >> result.src; in >> result.dst; in >> result.err_msg; qint32 err_value; in >> err_value; result.err_value = err_value; if (result.err_value) { qDebug() << "privileged link fail" << result.src << "to" << result.dst << "code" << result.err_value << result.err_msg; emit linkFailed(result.src, result.dst, result.err_msg, result.err_value); } else { qDebug() << "privileged link success" << result.src << "to" << result.dst; m_linked++; emit fileLinked(result.src, result.dst); } m_path_results.append(result); } gotResults = true; qDebug() << "results received, closing connection"; clientConnection->close(); }); qint64 byteswritten = clientConnection->write(block); bool bytesflushed = clientConnection->flush(); qDebug() << "block flushed" << byteswritten << bytesflushed; }); qDebug() << "Listening on pipe" << serverName; if (!m_linkServer.listen(serverName)) { qDebug() << "Unable to start local pipe server on" << serverName << ":" << m_linkServer.errorString(); return; } ExternalLinkFileProcess* linkFileProcess = new ExternalLinkFileProcess(serverName, m_useHardLinks, this); connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [this, &gotResults]() { emit finishedPrivileged(gotResults); }); connect(linkFileProcess, &ExternalLinkFileProcess::finished, linkFileProcess, &QObject::deleteLater); linkFileProcess->start(); } void ExternalLinkFileProcess::runLinkFile() { QString fileLinkExe = PathCombine(QCoreApplication::instance()->applicationDirPath(), BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink"); QString params = "-s " + m_server; params += " -H " + QVariant(m_useHardLinks).toString(); #if defined Q_OS_WIN32 SHELLEXECUTEINFO ShExecInfo; fileLinkExe = fileLinkExe + ".exe"; qDebug() << "Running: runas" << fileLinkExe << params; LPCWSTR programNameWin = (const wchar_t*)fileLinkExe.utf16(); LPCWSTR paramsWin = (const wchar_t*)params.utf16(); // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfoa ShExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); ShExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS; ShExecInfo.hwnd = NULL; // Optional. A handle to the owner window, used to display and position any UI that the system might produce // while executing this function. ShExecInfo.lpVerb = L"runas"; // elevate to admin, show UAC ShExecInfo.lpFile = programNameWin; ShExecInfo.lpParameters = paramsWin; ShExecInfo.lpDirectory = NULL; ShExecInfo.nShow = SW_HIDE; ShExecInfo.hInstApp = NULL; ShellExecuteEx(&ShExecInfo); WaitForSingleObject(ShExecInfo.hProcess, INFINITE); CloseHandle(ShExecInfo.hProcess); #endif qDebug() << "Process exited"; } bool moveByCopy(const QString& source, const QString& dest) { if (!copy(source, dest)()) { // copy qDebug() << "Copy of" << source << "to" << dest << "failed!"; return false; } if (!deletePath(source)) { // remove original qDebug() << "Deletion of" << source << "failed!"; return false; }; return true; } bool move(const QString& source, const QString& dest) { std::error_code err; ensureFilePathExists(dest); fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err); if (err.value() != 0) { if (moveByCopy(source, dest)) return true; qDebug() << "Move of" << source << "to" << dest << "failed!"; qWarning() << "Failed to move file:" << QString::fromStdString(err.message()) << QString::number(err.value()); return false; } return true; } bool deletePath(QString path) { std::error_code err; fs::remove_all(StringUtils::toStdString(path), err); if (err) { qWarning() << "Failed to remove files:" << QString::fromStdString(err.message()); } return err.value() == 0; } bool deleteContents(const QString& path) { const QFileInfo info(path); if (!info.exists()) { return true; } if (!info.isDir()) { qWarning() << "Attempted to delete contents of non-directory path:" << path; return false; } bool ret = true; for (const auto& entry : fs::directory_iterator(StringUtils::toStdString(path))) { std::error_code err; fs::remove_all(entry.path(), err); if (err.value() != 0) { qWarning().nospace() << "Could not delete directory entry " << entry.path() << ": " << QString::fromStdString(err.message()); ret = false; } } return ret; } bool trash(QString path, QString* pathInTrash) { // FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal if (DesktopServices::isFlatpak()) return false; #if defined Q_OS_WIN32 if (IsWindowsServer()) return false; #endif return QFile::moveToTrash(path, pathInTrash); } QString PathCombine(const QString& path1, const QString& path2) { if (!path1.size()) return path2; if (!path2.size()) return path1; return QDir::cleanPath(path1 + QDir::separator() + path2); } QString PathCombine(const QString& path1, const QString& path2, const QString& path3) { return PathCombine(PathCombine(path1, path2), path3); } QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4) { return PathCombine(PathCombine(path1, path2, path3), path4); } QString AbsolutePath(const QString& path) { return QFileInfo(path).absolutePath(); } int pathDepth(const QString& path) { if (path.isEmpty()) return 0; QFileInfo info(path); auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts); int numParts = parts.length(); numParts -= parts.count("."); numParts -= parts.count("..") * 2; return numParts; } QString pathTruncate(const QString& path, int depth) { if (path.isEmpty() || (depth < 0)) return ""; QString trunc = QFileInfo(path).path(); if (pathDepth(trunc) > depth) { return pathTruncate(trunc, depth); } auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), Qt::SkipEmptyParts); if (parts.startsWith(".") && !path.startsWith(".")) { parts.removeFirst(); } if (QDir::toNativeSeparators(path).startsWith(QDir::separator())) { parts.prepend(""); } trunc = parts.join(QDir::separator()); return trunc; } QString ResolveExecutable(QString path) { if (path.isEmpty()) { return QString(); } if (!path.contains('/')) { path = QStandardPaths::findExecutable(path); } QFileInfo pathInfo(path); if (!pathInfo.exists() || !pathInfo.isExecutable()) { return QString(); } return pathInfo.absoluteFilePath(); } /** * Normalize path * * Any paths inside the current folder will be normalized to relative paths (to current) * Other paths will be made absolute */ QString NormalizePath(QString path) { QDir a = QDir::currentPath(); QString currentAbsolute = a.absolutePath(); QDir b(path); QString newAbsolute = b.absolutePath(); if (newAbsolute.startsWith(currentAbsolute)) { return a.relativeFilePath(newAbsolute); } else { return newAbsolute; } } namespace { const QString g_badChars = "<>:\"|?*\r\n!"; QString removeChars(QString source, QChar replace, const QString& extraChars = "") { auto badChars = g_badChars; if (!extraChars.isEmpty()) { badChars += extraChars; } for (auto& c : source) { if (c.unicode() < 0x20 || !c.isPrint() || badChars.contains(c)) { c = replace; } } return source; } } // namespace QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) { return removeChars(std::move(string), replaceWith, "\\/"); } QString RemoveInvalidPathChars(QString string, QChar replaceWith) { return removeChars(std::move(string), replaceWith); } QString DirNameFromString(QString string, QString inDir) { int num = 0; QString baseName = RemoveInvalidFilenameChars(string, '-'); QString dirName; do { if (num == 0) { dirName = baseName; } else { dirName = baseName + "(" + QString::number(num) + ")"; } // If it's over 9000 if (num > 9000) return ""; num++; } while (QFileInfo(PathCombine(inDir, dirName)).exists()); return dirName; } // Does the folder path contain any '!'? If yes, return true, otherwise false. // (This is a problem for Java) bool checkProblemticPathJava(QDir folder) { QString pathfoldername = folder.absolutePath(); return pathfoldername.contains("!", Qt::CaseInsensitive); } QString getDesktopDir() { return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); } QString getApplicationsDir() { return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); } QString quoteArgs(const QStringList& args, const QString& wrap, const QString& escapeChar, bool wrapOnlyIfNeeded = false) { QString result; auto size = args.size(); for (int i = 0; i < size; ++i) { QString arg = args[i]; arg.replace(wrap, escapeChar); bool needsWrapping = !wrapOnlyIfNeeded || arg.contains(' ') || arg.contains('\t') || arg.contains(wrap); if (needsWrapping) result += wrap + arg + wrap; else result += arg; if (i < size - 1) result += ' '; } return result; } // Cross-platform Shortcut creation QString createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) { if (destination.isEmpty()) { destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name)); } if (!ensureFilePathExists(destination)) { qWarning() << "Destination path can't be created!"; return QString(); } #if defined(Q_OS_MACOS) QDir application = destination + ".app/"; if (application.exists()) { qWarning() << "Application already exists!"; return QString(); } if (!application.mkpath(".")) { qWarning() << "Couldn't create application"; return QString(); } QDir content = application.path() + "/Contents/"; QDir resources = content.path() + "/Resources/"; QDir binaryDir = content.path() + "/MacOS/"; QFile info(content.path() + "/Info.plist"); if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) { qWarning() << "Couldn't create directories within application"; return QString(); } if (!info.open(QIODevice::WriteOnly | QIODevice::Text)) { qWarning() << "Failed to open file" << info.fileName() << "for writing:" << info.errorString(); return QString(); } QFile(icon).rename(resources.path() + "/Icon.icns"); // Create the Command file QString exec = binaryDir.path() + "/Run.command"; QFile f(exec); if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { qWarning() << "Failed to open file" << f.fileName() << "for writing:" << f.errorString(); return QString(); } QTextStream stream(&f); auto argstring = quoteArgs(args, "\"", "\\\""); stream << "#!/bin/bash" << "\n"; stream << "\"" << target << "\" " << argstring << "\n"; stream.flush(); f.close(); f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); // Generate the Info.plist QTextStream infoStream(&info); infoStream << " \n" "" "\n" "\n" " CFBundleExecutable\n" " Run.command\n" // The path to the executable " CFBundleIconFile\n" " Icon.icns\n" " CFBundleName\n" " " << name << "\n" // Name of the application " CFBundlePackageType\n" " APPL\n" " CFBundleShortVersionString\n" " 1.0\n" " CFBundleVersion\n" " 1.0\n" "\n" ""; return application.path(); #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated destination += ".desktop"; QFile f(destination); if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { qWarning() << "Failed to open file" << f.fileName() << "for writing:" << f.errorString(); return QString(); } QTextStream stream(&f); auto argstring = quoteArgs(args, "'", "'\\''"); stream << "[Desktop Entry]" << "\n"; stream << "Type=Application" << "\n"; stream << "Categories=Game;ActionGame;AdventureGame;Simulation" << "\n"; stream << "Exec=\"" << target.toLocal8Bit() << "\" " << argstring.toLocal8Bit() << "\n"; stream << "Name=" << name.toLocal8Bit() << "\n"; if (!icon.isEmpty()) { stream << "Icon=" << icon.toLocal8Bit() << "\n"; } stream.flush(); f.close(); f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); return destination; #elif defined(Q_OS_WIN) QFileInfo targetInfo(target); if (!targetInfo.exists()) { qWarning() << "Target file does not exist!"; return QString(); } target = targetInfo.absoluteFilePath(); if (target.length() >= MAX_PATH) { qWarning() << "Target file path is too long!"; return QString(); } if (!icon.isEmpty() && icon.length() >= MAX_PATH) { qWarning() << "Icon path is too long!"; return QString(); } destination += ".lnk"; if (destination.length() >= MAX_PATH) { qWarning() << "Destination path is too long!"; return QString(); } auto argStr = quoteArgs(args, "\"", "\\\"", true); if (argStr.length() >= MAX_PATH) { qWarning() << "Arguments string is too long!"; return QString(); } HRESULT hres; // ...yes, you need to initialize the entire COM stack just to make a shortcut hres = CoInitialize(nullptr); if (FAILED(hres)) { qWarning() << "Failed to initialize COM!"; return QString(); } WCHAR wsz[MAX_PATH]; IShellLink* psl; // create an IShellLink instance - this stores the shortcut's attributes hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl); if (SUCCEEDED(hres)) { wmemset(wsz, 0, MAX_PATH); target.toWCharArray(wsz); psl->SetPath(wsz); wmemset(wsz, 0, MAX_PATH); argStr.toWCharArray(wsz); psl->SetArguments(wsz); wmemset(wsz, 0, MAX_PATH); targetInfo.absolutePath().toWCharArray(wsz); psl->SetWorkingDirectory(wsz); // "Starts in" attribute if (!icon.isEmpty()) { wmemset(wsz, 0, MAX_PATH); icon.toWCharArray(wsz); psl->SetIconLocation(wsz, 0); } // query an IPersistFile interface from our IShellLink instance // this is the interface that will actually let us save the shortcut to disk! IPersistFile* ppf; hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf); if (SUCCEEDED(hres)) { wmemset(wsz, 0, MAX_PATH); destination.toWCharArray(wsz); hres = ppf->Save(wsz, TRUE); if (FAILED(hres)) { qWarning() << "IPresistFile->Save() failed"; qWarning() << "hres =" << hres; } ppf->Release(); } else { qWarning() << "Failed to query IPersistFile interface from IShellLink instance"; qWarning() << "hres =" << hres; } psl->Release(); } else { qWarning() << "Failed to create IShellLink instance"; qWarning() << "hres =" << hres; } // go away COM, nobody likes you CoUninitialize(); if (SUCCEEDED(hres)) return destination; return QString(); #else qWarning("Desktop Shortcuts not supported on your platform!"); return QString(); #endif } bool overrideFolder(QString overwritten_path, QString override_path) { using copy_opts = fs::copy_options; if (!FS::ensureFolderPathExists(overwritten_path)) return false; std::error_code err; fs::copy_options opt = copy_opts::recursive | copy_opts::overwrite_existing; // FIXME: hello traveller! Apparently std::copy does NOT overwrite existing files on GNU libstdc++ on Windows? fs::copy(StringUtils::toStdString(override_path), StringUtils::toStdString(overwritten_path), opt, err); if (err) { qCritical() << QString("Failed to apply override from %1 to %2").arg(override_path, overwritten_path); qCritical() << "Reason:" << QString::fromStdString(err.message()); } return err.value() == 0; } QString getFilesystemTypeName(FilesystemType type) { auto iter = s_filesystem_type_names.constFind(type); if (iter != s_filesystem_type_names.constEnd()) { return iter.value().constFirst(); } return getFilesystemTypeName(FilesystemType::UNKNOWN); } FilesystemType getFilesystemTypeFuzzy(const QString& name) { for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) { auto fs_names = iter.value(); for (auto fs_name : fs_names) { if (name.toUpper().contains(fs_name.toUpper())) return iter.key(); } } return FilesystemType::UNKNOWN; } FilesystemType getFilesystemType(const QString& name) { for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) { auto fs_names = iter.value(); if (fs_names.contains(name.toUpper())) return iter.key(); } return FilesystemType::UNKNOWN; } /** * @brief path to the near ancestor that exists * */ QString nearestExistentAncestor(const QString& path) { if (QFileInfo::exists(path)) return path; QDir dir(path); if (!dir.makeAbsolute()) return {}; do { dir.setPath(QDir::cleanPath(dir.filePath(QStringLiteral("..")))); } while (!dir.exists() && !dir.isRoot()); return dir.exists() ? dir.path() : QString(); } /** * @brief colect information about the filesystem under a file * */ FilesystemInfo statFS(const QString& path) { FilesystemInfo info; QStorageInfo storage_info(nearestExistentAncestor(path)); info.fsTypeName = storage_info.fileSystemType(); info.fsType = getFilesystemTypeFuzzy(info.fsTypeName); info.blockSize = storage_info.blockSize(); info.bytesAvailable = storage_info.bytesAvailable(); info.bytesFree = storage_info.bytesFree(); info.bytesTotal = storage_info.bytesTotal(); info.name = storage_info.name(); info.rootPath = storage_info.rootPath(); return info; } /** * @brief if the Filesystem is reflink/clone capable * */ bool canCloneOnFS(const QString& path) { FilesystemInfo info = statFS(path); return canCloneOnFS(info); } bool canCloneOnFS(const FilesystemInfo& info) { return canCloneOnFS(info.fsType); } bool canCloneOnFS(FilesystemType type) { return s_clone_filesystems.contains(type); } /** * @brief if the Filesystem is reflink/clone capable and both paths are on the same device * */ bool canClone(const QString& src, const QString& dst) { auto srcVInfo = statFS(src); auto dstVInfo = statFS(dst); bool sameDevice = srcVInfo.rootPath == dstVInfo.rootPath; return sameDevice && canCloneOnFS(srcVInfo) && canCloneOnFS(dstVInfo); } /** * @brief reflink/clones a directory and it's contents from src to dest * @param offset subdirectory form src to copy to dest * @return if there was an error during the filecopy */ bool clone::operator()(const QString& offset, bool dryRun) { if (!canClone(m_src.absolutePath(), m_dst.absolutePath())) { qWarning() << "Can not clone: not same device or not clone/reflink filesystem"; qDebug() << "Source path:" << m_src.absolutePath(); qDebug() << "Destination path:" << m_dst.absolutePath(); emit cloneFailed(m_src.absolutePath(), m_dst.absolutePath()); return false; } m_cloned = 0; // reset counter m_failedClones.clear(); auto src = PathCombine(m_src.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset); std::error_code err; // Function that'll do the actual cloneing auto cloneFile = [this, dryRun, dst, &err](QString src_path, QString relative_dst_path) { if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) return; auto dst_path = PathCombine(dst, relative_dst_path); if (!dryRun) { ensureFilePathExists(dst_path); clone_file(src_path, dst_path, err); } if (err) { qDebug() << "Failed to clone files: error" << err.value() << "message" << QString::fromStdString(err.message()); qDebug() << "Source file:" << src_path; qDebug() << "Destination file:" << dst_path; m_failedClones.append(qMakePair(src_path, dst_path)); emit cloneFailed(src_path, dst_path); return; } m_cloned++; emit fileCloned(src_path, dst_path); }; // We can't use copy_opts::recursive because we need to take into account the // blacklisted paths, so we iterate over the source directory, and if there's no blacklist // match, we copy the file. QDir src_dir(src); QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories); while (source_it.hasNext()) { auto src_path = source_it.next(); auto relative_path = src_dir.relativeFilePath(src_path); cloneFile(src_path, relative_path); } // If the root src is not a directory, the previous iterator won't run. if (!fs::is_directory(StringUtils::toStdString(src))) cloneFile(src, ""); return err.value() == 0; } /** * @brief clone/reflink file from src to dst * */ bool clone_file(const QString& src, const QString& dst, std::error_code& ec) { auto src_path = StringUtils::toStdString(QDir::toNativeSeparators(QFileInfo(src).absoluteFilePath())); auto dst_path = StringUtils::toStdString(QDir::toNativeSeparators(QFileInfo(dst).absoluteFilePath())); FilesystemInfo srcinfo = statFS(src); FilesystemInfo dstinfo = statFS(dst); if ((srcinfo.rootPath != dstinfo.rootPath) || (srcinfo.fsType != dstinfo.fsType)) { ec = std::make_error_code(std::errc::not_supported); qWarning() << "reflink/clone must be to the same device and filesystem! src and dst root filesystems do not match."; return false; } #if defined(Q_OS_WIN) if (!win_ioctl_clone(src_path, dst_path, ec)) { qDebug() << "failed win_ioctl_clone"; qWarning() << "clone/reflink not supported on windows outside of btrfs or ReFS!"; qWarning() << "check out https://github.com/maharmstone/btrfs for btrfs support!"; return false; } #elif defined(Q_OS_LINUX) if (!linux_ficlone(src_path, dst_path, ec)) { qDebug() << "failed linux_ficlone:"; return false; } #elif defined(Q_OS_MACOS) if (!macos_bsd_clonefile(src_path, dst_path, ec)) { qDebug() << "failed macos_bsd_clonefile:"; return false; } #else qWarning() << "clone/reflink not supported! unknown OS"; ec = std::make_error_code(std::errc::not_supported); return false; #endif return true; } #if defined(Q_OS_WIN) static long RoundUpToPowerOf2(long originalValue, long roundingMultiplePowerOf2) { long mask = roundingMultiplePowerOf2 - 1; return (originalValue + mask) & ~mask; } bool win_ioctl_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec) { /** * This algorithm inspired from https://github.com/0xbadfca11/reflink * LICENSE MIT * * Additional references * https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file * https://github.com/microsoft/CopyOnWrite/blob/main/lib/Windows/WindowsCopyOnWriteFilesystem.cs#L94 */ HANDLE hSourceFile = CreateFileW(src_path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr); if (hSourceFile == INVALID_HANDLE_VALUE) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to open source file" << src_path.c_str(); return false; } ULONG fs_flags; if (!GetVolumeInformationByHandleW(hSourceFile, nullptr, 0, nullptr, nullptr, &fs_flags, nullptr, 0)) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to get Filesystem information for" << src_path.c_str(); CloseHandle(hSourceFile); return false; } if (!(fs_flags & FILE_SUPPORTS_BLOCK_REFCOUNTING)) { SetLastError(ERROR_NOT_CAPABLE); ec = std::error_code(GetLastError(), std::system_category()); qWarning() << "Filesystem at" << src_path.c_str() << "does not support reflink"; CloseHandle(hSourceFile); return false; } FILE_END_OF_FILE_INFO sourceFileLength; if (!GetFileSizeEx(hSourceFile, &sourceFileLength.EndOfFile)) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to size of source file" << src_path.c_str(); CloseHandle(hSourceFile); return false; } FILE_BASIC_INFO sourceFileBasicInfo; if (!GetFileInformationByHandleEx(hSourceFile, FileBasicInfo, &sourceFileBasicInfo, sizeof(sourceFileBasicInfo))) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to source file info" << src_path.c_str(); CloseHandle(hSourceFile); return false; } ULONG junk; FSCTL_GET_INTEGRITY_INFORMATION_BUFFER sourceFileIntegrity; if (!DeviceIoControl(hSourceFile, FSCTL_GET_INTEGRITY_INFORMATION, nullptr, 0, &sourceFileIntegrity, sizeof(sourceFileIntegrity), &junk, nullptr)) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to source file integrity info" << src_path.c_str(); CloseHandle(hSourceFile); return false; } HANDLE hDestFile = CreateFileW(dst_path.c_str(), GENERIC_READ | GENERIC_WRITE | DELETE, 0, nullptr, CREATE_NEW, 0, hSourceFile); if (hDestFile == INVALID_HANDLE_VALUE) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to open dest file" << dst_path.c_str(); CloseHandle(hSourceFile); return false; } FILE_DISPOSITION_INFO destFileDispose = { TRUE }; if (!SetFileInformationByHandle(hDestFile, FileDispositionInfo, &destFileDispose, sizeof(destFileDispose))) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to set dest file info" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } if (!DeviceIoControl(hDestFile, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &junk, nullptr)) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to set dest sparseness" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } FSCTL_SET_INTEGRITY_INFORMATION_BUFFER setDestFileintegrity = { sourceFileIntegrity.ChecksumAlgorithm, sourceFileIntegrity.Reserved, sourceFileIntegrity.Flags }; if (!DeviceIoControl(hDestFile, FSCTL_SET_INTEGRITY_INFORMATION, &setDestFileintegrity, sizeof(setDestFileintegrity), nullptr, 0, nullptr, nullptr)) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to set dest file integrity info" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } if (!SetFileInformationByHandle(hDestFile, FileEndOfFileInfo, &sourceFileLength, sizeof(sourceFileLength))) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to set dest file size" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } const LONG64 splitThreshold = (1LL << 32) - sourceFileIntegrity.ClusterSizeInBytes; DUPLICATE_EXTENTS_DATA dupExtent; dupExtent.FileHandle = hSourceFile; for (LONG64 offset = 0, remain = RoundUpToPowerOf2(sourceFileLength.EndOfFile.QuadPart, sourceFileIntegrity.ClusterSizeInBytes); remain > 0; offset += splitThreshold, remain -= splitThreshold) { dupExtent.SourceFileOffset.QuadPart = dupExtent.TargetFileOffset.QuadPart = offset; dupExtent.ByteCount.QuadPart = std::min(splitThreshold, remain); if (!DeviceIoControl(hDestFile, FSCTL_DUPLICATE_EXTENTS_TO_FILE, &dupExtent, sizeof(dupExtent), nullptr, 0, &junk, nullptr)) { DWORD err = GetLastError(); QString additionalMessage; if (err == ERROR_BLOCK_TOO_MANY_REFERENCES) { static const int MaxClonesPerFile = 8175; additionalMessage = QString( " This is ERROR_BLOCK_TOO_MANY_REFERENCES and may mean you have surpassed the maximum " "allowed %1 references for a single file. " "See " "https://docs.microsoft.com/en-us/windows-server/storage/refs/block-cloning#functionality-restrictions-and-remarks") .arg(MaxClonesPerFile); } ec = std::error_code(err, std::system_category()); qDebug() << "Failed copy-on-write cloning of" << src_path.c_str() << "to" << dst_path.c_str() << "with error" << err << additionalMessage; CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } } if (!(sourceFileBasicInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE)) { FILE_SET_SPARSE_BUFFER setDestSparse = { FALSE }; if (!DeviceIoControl(hDestFile, FSCTL_SET_SPARSE, &setDestSparse, sizeof(setDestSparse), nullptr, 0, &junk, nullptr)) { qDebug() << "Failed to set dest file sparseness" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } } sourceFileBasicInfo.CreationTime.QuadPart = 0; if (!SetFileInformationByHandle(hDestFile, FileBasicInfo, &sourceFileBasicInfo, sizeof(sourceFileBasicInfo))) { qDebug() << "Failed to set dest file creation time" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } if (!FlushFileBuffers(hDestFile)) { qDebug() << "Failed to flush dest file buffer" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } destFileDispose = { FALSE }; bool result = !!SetFileInformationByHandle(hDestFile, FileDispositionInfo, &destFileDispose, sizeof(destFileDispose)); CloseHandle(hSourceFile); CloseHandle(hDestFile); return result; } #elif defined(Q_OS_LINUX) bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std::error_code& ec) { // https://man7.org/linux/man-pages/man2/ioctl_ficlone.2.html int src_fd = open(src_path.c_str(), O_RDONLY); if (src_fd == -1) { qDebug() << "Failed to open file:" << src_path.c_str(); qDebug() << "Error:" << strerror(errno); ec = std::make_error_code(static_cast(errno)); return false; } int dst_fd = open(dst_path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); if (dst_fd == -1) { qDebug() << "Failed to open file:" << dst_path.c_str(); qDebug() << "Error:" << strerror(errno); ec = std::make_error_code(static_cast(errno)); close(src_fd); return false; } // attempt to clone if (ioctl(dst_fd, FICLONE, src_fd) == -1) { qDebug() << "Failed to clone file:" << src_path.c_str() << "to" << dst_path.c_str(); qDebug() << "Error:" << strerror(errno); ec = std::make_error_code(static_cast(errno)); close(src_fd); close(dst_fd); return false; } if (close(src_fd)) { qDebug() << "Failed to close file:" << src_path.c_str(); qDebug() << "Error:" << strerror(errno); } if (close(dst_fd)) { qDebug() << "Failed to close file:" << dst_path.c_str(); qDebug() << "Error:" << strerror(errno); } return true; } #elif defined(Q_OS_MACOS) bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec) { // clonefile(const char * src, const char * dst, int flags); // https://www.manpagez.com/man/2/clonefile/ qDebug() << "attempting file clone via clonefile" << src_path.c_str() << "to" << dst_path.c_str(); if (clonefile(src_path.c_str(), dst_path.c_str(), 0) == -1) { qDebug() << "Failed to clone file:" << src_path.c_str() << "to" << dst_path.c_str(); qDebug() << "Error:" << strerror(errno); ec = std::make_error_code(static_cast(errno)); return false; } return true; } #endif /** * @brief if the Filesystem is symlink capable * */ bool canLinkOnFS(const QString& path) { FilesystemInfo info = statFS(path); return canLinkOnFS(info); } bool canLinkOnFS(const FilesystemInfo& info) { return canLinkOnFS(info.fsType); } bool canLinkOnFS(FilesystemType type) { return !s_non_link_filesystems.contains(type); } /** * @brief if the Filesystem is symlink capable on both ends * */ bool canLink(const QString& src, const QString& dst) { return canLinkOnFS(src) && canLinkOnFS(dst); } uintmax_t hardLinkCount(const QString& path) { std::error_code err; int count = fs::hard_link_count(StringUtils::toStdString(path), err); if (err) { qWarning() << "Failed to count hard links for" << path << ":" << QString::fromStdString(err.message()); count = 0; } return count; } #ifdef Q_OS_WIN // returns 8.3 file format from long path QString shortPathName(const QString& file) { auto input = file.toStdWString(); std::wstring output; long length = GetShortPathNameW(input.c_str(), NULL, 0); if (length == 0) return {}; // NOTE: this resizing might seem weird... // when GetShortPathNameW fails, it returns length including null character // when it succeeds, it returns length excluding null character // See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx output.resize(length); if (GetShortPathNameW(input.c_str(), (LPWSTR)output.c_str(), length) == 0) return {}; output.resize(length - 1); QString ret = QString::fromStdWString(output); return ret; } // if the string survives roundtrip through local 8bit encoding... bool fitsInLocal8bit(const QString& string) { return string == QString::fromLocal8Bit(string.toLocal8Bit()); } QString getPathNameInLocal8bit(const QString& file) { if (!fitsInLocal8bit(file)) { auto path = shortPathName(file); if (!path.isEmpty()) { return path; } // in case shortPathName fails just return the path as is } return file; } #endif QString getUniqueResourceName(const QString& filePath) { auto newFileName = filePath; if (!newFileName.endsWith(".disabled")) { return newFileName; // prioritize enabled mods } newFileName.chop(9); if (!QFile::exists(newFileName)) { return filePath; } QFileInfo fileInfo(filePath); auto baseName = fileInfo.completeBaseName(); auto path = fileInfo.absolutePath(); int counter = 1; do { if (counter == 1) { newFileName = FS::PathCombine(path, baseName + ".duplicate"); } else { newFileName = FS::PathCombine(path, baseName + ".duplicate" + QString::number(counter)); } counter++; } while (QFile::exists(newFileName)); return newFileName; } bool removeFiles(QStringList listFile) { bool ret = true; // For each file for (int i = 0; i < listFile.count(); i++) { // Remove ret = ret && QFile::remove(listFile.at(i)); } return ret; } } // namespace FS PrismLauncher-11.0.3/launcher/QVariantUtils.h0000644000175100017510000000415515224505336020510 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include namespace QVariantUtils { template inline QList toList(QVariant src) { QVariantList variantList = src.toList(); QList list_t; list_t.reserve(variantList.size()); for (const QVariant& v : variantList) { list_t.append(v.value()); } return list_t; } template inline QVariant fromList(QList val) { QVariantList variantList; variantList.reserve(val.size()); for (const T& v : val) { variantList.append(v); } return variantList; } } // namespace QVariantUtils PrismLauncher-11.0.3/launcher/MessageLevel.cpp0000644000175100017510000000446715224505336020657 0ustar runnerrunner#include "MessageLevel.h" MessageLevel MessageLevel::fromName(const QString& levelName) { QString name = levelName.toUpper(); if (name == "LAUNCHER") return MessageLevel::Launcher; else if (name == "TRACE") return MessageLevel::Trace; else if (name == "DEBUG") return MessageLevel::Debug; else if (name == "INFO") return MessageLevel::Info; else if (name == "MESSAGE") return MessageLevel::Message; else if (name == "WARNING" || name == "WARN") return MessageLevel::Warning; else if (name == "ERROR" || name == "CRITICAL") return MessageLevel::Error; else if (name == "FATAL") return MessageLevel::Fatal; // Skip PrePost, it's not exposed to !![]! // Also skip StdErr and StdOut else return MessageLevel::Unknown; } MessageLevel MessageLevel::fromQtMsgType(const QtMsgType& type) { switch (type) { case QtDebugMsg: return MessageLevel::Debug; case QtInfoMsg: return MessageLevel::Info; case QtWarningMsg: return MessageLevel::Warning; case QtCriticalMsg: return MessageLevel::Error; case QtFatalMsg: return MessageLevel::Fatal; default: return MessageLevel::Unknown; } } /* Get message level from a line. Line is modified if it was successful. */ MessageLevel MessageLevel::takeFromLine(QString& line) { // Level prefix int endmark = line.indexOf("]!"); if (line.startsWith("!![") && endmark != -1) { auto level = MessageLevel::fromName(line.left(endmark).mid(3)); line = line.mid(endmark + 2); return level; } return MessageLevel::Unknown; } /* Get message level from a line from the launcher log. Line is modified if it was successful. */ MessageLevel MessageLevel::takeFromLauncherLine(QString& line) { // Level prefix int startMark = 0; while (startMark < line.size() && (line[startMark].isDigit() || line[startMark].isSpace() || line[startMark] == '.')) ++startMark; int endmark = line.indexOf(":"); if (startMark < line.size() && endmark != -1) { auto level = MessageLevel::fromName(line.left(endmark).mid(startMark)); line = line.mid(endmark + 2); return level; } return MessageLevel::Unknown; } PrismLauncher-11.0.3/launcher/Launcher.in0000755000175100017510000000221015224505336017653 0ustar runnerrunner#!/usr/bin/env bash # Basic start script for running the launcher with the libs packaged with it. function printerror { printf "$1" if which zenity >/dev/null; then zenity --error --text="$1" &>/dev/null; elif which kdialog >/dev/null; then kdialog --error "$1" &>/dev/null; fi } if [[ $EUID -eq 0 ]]; then printerror "This program should not be run using sudo or as the root user!\n" exit 1 fi LAUNCHER_NAME=@Launcher_APP_BINARY_NAME@ LAUNCHER_ENVNAME=@Launcher_ENVName@ LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")" echo "Launcher Dir: ${LAUNCHER_DIR}" # Makes the launcher use portals for file picking export QT_QPA_PLATFORMTHEME=xdgdesktopportal # disable OpenGL and Vulkan launcher features on sharun until https://github.com/VHSgunzo/sharun/issues/35 if [[ -f "${LAUNCHER_DIR}/sharun" ]]; then export ${LAUNCHER_ENVNAME}_DISABLE_GLVULKAN=1 fi # Just to be sure... chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" ARGS=("${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}") if [ -f portable.txt ]; then ARGS+=("-d" "${LAUNCHER_DIR}") fi ARGS+=("$@") # Run the launcher exec -a "${ARGS[@]}" PrismLauncher-11.0.3/launcher/Commandline.h0000644000175100017510000000205415224505336020164 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan * * 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 #include /** * @file libutil/include/cmdutils.h * @brief commandline parsing and processing utilities */ namespace Commandline { /** * @brief split a string into argv items like a shell would do * @param args the argument string * @return a QStringList containing all arguments */ QStringList splitArgs(QString args); } // namespace Commandline PrismLauncher-11.0.3/launcher/InstanceTask.cpp0000644000175100017510000000705415224505336020665 0ustar runnerrunner#include "InstanceTask.h" #include #include "Application.h" #include "settings/SettingsObject.h" #include "ui/dialogs/CustomMessageBox.h" #include InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name) { auto dialog = CustomMessageBox::selectable(parent, QObject::tr("Change instance name"), QObject::tr("The instance's name seems to include the old version. Would you like to update it?\n\n" "Old name: %1\n" "New name: %2") .arg(old_name, new_name), QMessageBox::Question, QMessageBox::No | QMessageBox::Yes); auto result = dialog->exec(); if (result == QMessageBox::Yes) return InstanceNameChange::ShouldChange; return InstanceNameChange::ShouldKeep; } ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name) { if (APPLICATION->settings()->get("SkipModpackUpdatePrompt").toBool()) return ShouldUpdate::SkipUpdating; auto info = CustomMessageBox::selectable( parent, QObject::tr("Similar modpack was found!"), QObject::tr( "One or more of your instances are from this same modpack%1. Do you want to create a " "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before " "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") .arg(original_version_name), QMessageBox::Information, QMessageBox::Cancel); QAbstractButton* update = info->addButton(QObject::tr("Update existing instance"), QMessageBox::AcceptRole); QAbstractButton* skip = info->addButton(QObject::tr("Create new instance"), QMessageBox::ResetRole); info->exec(); if (info->clickedButton() == update) return ShouldUpdate::Update; if (info->clickedButton() == skip) return ShouldUpdate::SkipUpdating; return ShouldUpdate::Cancel; } QString InstanceName::name() const { if (!m_modified_name.isEmpty()) return modifiedName(); if (!m_original_version.isEmpty()) return QString("%1 %2").arg(m_original_name, m_original_version); return m_original_name; } QString InstanceName::originalName() const { return m_original_name; } QString InstanceName::modifiedName() const { if (!m_modified_name.isEmpty()) return m_modified_name; return m_original_name; } QString InstanceName::version() const { return m_original_version; } void InstanceName::setName(InstanceName& other) { m_original_name = other.m_original_name; m_original_version = other.m_original_version; m_modified_name = other.m_modified_name; } InstanceTask::InstanceTask() : Task(), InstanceName() {} ShouldDeleteSaves askIfShouldDeleteSaves(QWidget* parent) { auto dialog = CustomMessageBox::selectable(parent, QObject::tr("Delete Existing Save Files"), QObject::tr("An earlier version of this mod pack installed save files.\n" "Would you like to remove those existing saves as part of this update?"), QMessageBox::Question, QMessageBox::No | QMessageBox::Yes); auto result = dialog->exec(); return result == QMessageBox::Yes ? ShouldDeleteSaves::Yes : ShouldDeleteSaves::No; } PrismLauncher-11.0.3/launcher/KonamiCode.h0000644000175100017510000000035715224505336017753 0ustar runnerrunner#pragma once #include class KonamiCode : public QObject { Q_OBJECT public: KonamiCode(QObject* parent = 0); void input(QEvent* event); signals: void triggered(); private: int m_progress = 0; }; PrismLauncher-11.0.3/launcher/InstanceDirUpdate.cpp0000644000175100017510000001317115224505336021641 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "InstanceDirUpdate.h" #include #include "Application.h" #include "FileSystem.h" #include "InstanceList.h" #include "ui/dialogs/CustomMessageBox.h" QString askToUpdateInstanceDirName(BaseInstance* instance, const QString& oldName, const QString& newName, QWidget* parent) { if (oldName == newName) return QString(); QString renamingMode = APPLICATION->settings()->get("InstRenamingMode").toString(); if (renamingMode == "MetadataOnly") return QString(); auto oldRoot = instance->instanceRoot(); auto newDirName = FS::DirNameFromString(newName, QFileInfo(oldRoot).dir().absolutePath()); auto newRoot = FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newDirName); if (oldRoot == newRoot) return QString(); if (oldRoot == FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newName)) return QString(); // Check for conflict if (QDir(newRoot).exists()) { QMessageBox::warning(parent, QObject::tr("Cannot rename instance"), QObject::tr("New instance root (%1) already exists.
    Only the metadata will be renamed.").arg(newRoot)); return QString(); } // Ask if we should rename if (renamingMode == "AskEverytime") { auto checkBox = new QCheckBox(QObject::tr("&Remember my choice"), parent); auto dialog = CustomMessageBox::selectable(parent, QObject::tr("Rename instance folder"), QObject::tr("Would you also like to rename the instance folder?\n\n" "Old name: %1\n" "New name: %2") .arg(oldName, newName), QMessageBox::Question, QMessageBox::No | QMessageBox::Yes, QMessageBox::NoButton, checkBox); auto res = dialog->exec(); if (checkBox->isChecked()) { if (res == QMessageBox::Yes) APPLICATION->settings()->set("InstRenamingMode", "PhysicalDir"); else APPLICATION->settings()->set("InstRenamingMode", "MetadataOnly"); } if (res == QMessageBox::No) return QString(); } // Check for linked instances if (!checkLinkedInstances(instance->id(), parent, QObject::tr("Renaming"))) return QString(); // Now we can confirm that a renaming is happening if (!instance->syncInstanceDirName(newRoot)) { QMessageBox::warning(parent, QObject::tr("Cannot rename instance"), QObject::tr("An error occurred when performing the following renaming operation:
    " " - Old instance root: %1
    " " - New instance root: %2
    " "Only the metadata is renamed.") .arg(oldRoot, newRoot)); return QString(); } return newRoot; } bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb) { auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); if (!linkedInstances.empty()) { auto response = CustomMessageBox::selectable(parent, QObject::tr("There are linked instances"), QObject::tr("The following instance(s) might reference files in this instance:\n\n" "%1\n\n" "%2 it could break the other instance(s), \n\n" "Do you wish to proceed?", nullptr, linkedInstances.count()) .arg(linkedInstances.join("\n")) .arg(verb), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return false; } return true; } PrismLauncher-11.0.3/launcher/LaunchMode.h0000644000175100017510000000146715224505336017764 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2026 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once enum class LaunchMode { Normal, Offline, Demo, }; PrismLauncher-11.0.3/launcher/Application.cpp0000644000175100017510000023135615224505336020545 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Lenny McLennington * Copyright (C) 2022 Tayou * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Application.h" #include "BuildConfig.h" #include "DataMigrationTask.h" #include "java/JavaInstallList.h" #include "net/PasteUpload.h" #include "tasks/Task.h" #include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" #include "ui/ToolTipFilter.h" #include "ui/ViewLogWindow.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/instanceview/AccessibleInstanceView.h" #include "ui/pages/BasePageProvider.h" #include "ui/pages/global/APIPage.h" #include "ui/pages/global/AccountListPage.h" #include "ui/pages/global/AppearancePage.h" #include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/JavaPage.h" #include "ui/pages/global/LanguagePage.h" #include "ui/pages/global/LauncherPage.h" #include "ui/pages/global/MinecraftPage.h" #include "ui/pages/global/ProxyPage.h" #include "ui/setupwizard/AutoJavaWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h" #include "ui/setupwizard/LanguageWizardPage.h" #include "ui/setupwizard/LoginWizardPage.h" #include "ui/setupwizard/PasteWizardPage.h" #include "ui/setupwizard/SetupWizard.h" #include "ui/setupwizard/ThemeWizardPage.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/pagedialog/PageDialog.h" #include "ui/themes/ThemeManager.h" #include "ApplicationMessage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "InstanceList.h" #include "MTPixmapCache.h" #include #include "icons/IconList.h" #include "net/HttpMetaCache.h" #include "updater/ExternalUpdater.h" #include "tools/JProfiler.h" #include "tools/JVisualVM.h" #include "tools/MCEditTool.h" #include "settings/INISettingsObject.h" #include "settings/Setting.h" #include "meta/Index.h" #include "translations/TranslationsModel.h" #include #include #include #include #include "SysInfo.h" #ifdef Q_OS_LINUX #include #include "LibraryUtils.h" #include "gamemode_client.h" #endif #if defined(Q_OS_LINUX) #include #endif #if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) #include #include #endif #if defined(Q_OS_MAC) #if defined(SPARKLE_ENABLED) #include "updater/MacSparkleUpdater.h" #endif #else #include "updater/PrismExternalUpdater.h" #endif #if defined Q_OS_WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include #endif #include "console/Console.h" #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) static const QLatin1String liveCheckFile("live.check"); PixmapCache* PixmapCache::s_instance = nullptr; static bool isANSIColorConsole; static QString defaultLogFormat = QStringLiteral( "%{time process}" " " "%{if-debug}Debug:%{endif}" "%{if-info}Info:%{endif}" "%{if-warning}Warning:%{endif}" "%{if-critical}Critical:%{endif}" "%{if-fatal}Fatal:%{endif}" " " "%{if-category}[%{category}] %{endif}" "%{message}" " " "(%{function}:%{line})"); #define ansi_reset "\x1b[0m" #define ansi_bold "\x1b[1m" #define ansi_reset_bold "\x1b[22m" #define ansi_faint "\x1b[2m" #define ansi_italic "\x1b[3m" #define ansi_red_fg "\x1b[31m" #define ansi_green_fg "\x1b[32m" #define ansi_yellow_fg "\x1b[33m" #define ansi_blue_fg "\x1b[34m" #define ansi_purple_fg "\x1b[35m" #define ansi_inverse "\x1b[7m" // clang-format off static QString ansiLogFormat = QStringLiteral( ansi_faint "%{time process}" ansi_reset " " "%{if-debug}" ansi_bold ansi_green_fg "D:" ansi_reset "%{endif}" "%{if-info}" ansi_bold ansi_blue_fg "I:" ansi_reset "%{endif}" "%{if-warning}" ansi_bold ansi_yellow_fg "W:" ansi_reset_bold "%{endif}" "%{if-critical}" ansi_bold ansi_red_fg "C:" ansi_reset_bold "%{endif}" "%{if-fatal}" ansi_bold ansi_inverse ansi_red_fg "F:" ansi_reset_bold "%{endif}" " " "%{if-category}" ansi_bold "[%{category}]" ansi_reset_bold " %{endif}" "%{message}" " " ansi_reset ansi_faint "(%{function}:%{line})" ansi_reset ); // clang-format on #undef ansi_inverse #undef ansi_purple_fg #undef ansi_blue_fg #undef ansi_yellow_fg #undef ansi_green_fg #undef ansi_red_fg #undef ansi_italic #undef ansi_faint #undef ansi_bold #undef ansi_reset_bold #undef ansi_reset namespace { /** This is used so that we can output to the log file in addition to the CLI. */ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) { static std::mutex loggerMutex; const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe if (isANSIColorConsole) { // ensure default is set for log file qSetMessagePattern(defaultLogFormat); } QString out = qFormatLogMessage(type, context, msg); if (APPLICATION->logModel) { APPLICATION->logModel->append(MessageLevel::fromQtMsgType(type), out); } out += QChar::LineFeed; APPLICATION->logFile->write(out.toUtf8()); APPLICATION->logFile->flush(); if (isANSIColorConsole) { // format ansi for console; qSetMessagePattern(ansiLogFormat); out = qFormatLogMessage(type, context, msg); out += QChar::LineFeed; } QTextStream(stderr) << out.toLocal8Bit(); fflush(stderr); } } // namespace std::tuple read_lock_File(const QString& path) { auto contents = QString(FS::read(path)); auto lines = contents.split('\n'); QDateTime timestamp; QString from, to, target, data_path; for (auto line : lines) { auto index = line.indexOf("="); if (index < 0) continue; auto left = line.left(index); auto right = line.mid(index + 1); if (left.toLower() == "timestamp") { timestamp = QDateTime::fromString(right, Qt::ISODate); } else if (left.toLower() == "from") { from = right; } else if (left.toLower() == "to") { to = right; } else if (left.toLower() == "target") { target = right; } else if (left.toLower() == "data_path") { data_path = right; } } return std::make_tuple(timestamp, from, to, target, data_path); } Application::Application(int& argc, char** argv) : QApplication(argc, argv) { if (console::isConsole()) { isANSIColorConsole = true; } setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME); setApplicationDisplayName(QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString())); setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); setDesktopFileName(BuildConfig.LAUNCHER_APPID); m_startTime = QDateTime::currentDateTime(); // Don't quit on hiding the last window this->setQuitOnLastWindowClosed(false); this->setQuitLockEnabled(false); // Commandline parsing QCommandLineParser parser; parser.setApplicationDescription(BuildConfig.LAUNCHER_DISPLAYNAME); parser.addOptions( { { { "d", "dir" }, "Use a custom path as application root (use '.' for current directory)", "directory" }, { { "l", "launch" }, "Launch the specified instance (by instance ID)", "instance" }, { { "s", "server" }, "Join the specified server on launch (only valid in combination with --launch)", "address" }, { { "w", "world" }, "Join the specified world on launch (only valid in combination with --launch)", "world" }, { { "a", "profile" }, "Use the account specified by its profile name (only valid in combination with --launch)", "profile" }, { { "o", "offline" }, "Launch offline, with given player name (only valid in combination with --launch)", "offline" }, { "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" }, { "show-window", "Show the main launcher window (useful in combination with --launch)" }, { { "I", "import" }, "Import instance or resource from specified local path or URL", "url" }, { "show", "Opens the window for the specified instance (by instance ID)", "show" } }); // Has to be positional for some OS to handle that properly parser.addPositionalArgument("URL", "Import the resource(s) at the given URL(s) (same as -I / --import)", "[URL...]"); parser.addHelpOption(); parser.addVersionOption(); parser.process(arguments()); m_instanceIdToLaunch = parser.value("launch"); m_serverToJoin = parser.value("server"); m_worldToJoin = parser.value("world"); m_profileToUse = parser.value("profile"); if (parser.isSet("offline")) { m_launchOffline = true; m_offlineName = parser.value("offline"); } m_liveCheck = parser.isSet("alive"); m_instanceIdToShowWindowOf = parser.value("show"); m_showMainWindow = parser.isSet("show-window"); for (auto url : parser.values("import")) { m_urlsToImport.append(normalizeImportUrl(url)); } // treat unspecified positional arguments as import urls for (auto url : parser.positionalArguments()) { m_urlsToImport.append(normalizeImportUrl(url)); } // error if --launch is missing with --server or --profile if ((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty() || !m_profileToUse.isEmpty() || m_launchOffline) && m_instanceIdToLaunch.isEmpty()) { std::cerr << "--server, --profile and --offline can only be used in combination with --launch!" << std::endl; m_status = Application::Failed; return; } QString origcwdPath = QDir::currentPath(); QString binPath = applicationDirPath(); { // Root path is used for updates and portable data #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr m_rootPath = foo.absolutePath(); #elif defined(Q_OS_WIN32) m_rootPath = binPath; #elif defined(Q_OS_MAC) QDir foo(FS::PathCombine(binPath, "../..")); m_rootPath = foo.absolutePath(); // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues) FS::updateTimestamp(m_rootPath); #endif } QString adjustedBy; QString dataPath; // change folder QString dataDirEnv; QString dirParam = parser.value("dir"); if (!dirParam.isEmpty()) { // the dir param. it makes multimc data path point to whatever the user specified // on command line adjustedBy = "Command line"; dataPath = dirParam; } else if (dataDirEnv = QProcessEnvironment::systemEnvironment().value(QString("%1_DATA_DIR").arg(BuildConfig.LAUNCHER_NAME.toUpper())); !dataDirEnv.isEmpty()) { adjustedBy = "System environment"; dataPath = dataDirEnv; } else { QDir foo; if (DesktopServices::isSnap()) { foo = QDir(qEnvironmentVariable("SNAP_USER_COMMON")); } else { foo = QDir(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); } dataPath = foo.absolutePath(); adjustedBy = "Persistent data path"; #ifndef Q_OS_MACOS if (auto portableUserData = FS::PathCombine(m_rootPath, "UserData"); QDir(portableUserData).exists()) { dataPath = portableUserData; adjustedBy = "Portable user data path"; m_portable = true; } else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { dataPath = m_rootPath; adjustedBy = "Portable data path"; m_portable = true; } #endif } if (!FS::ensureFolderPathExists(dataPath)) { showFatalErrorMessage( "The launcher data folder could not be created.", QString("The launcher data folder could not be created.\n" "\n" "Make sure you have the right permissions to the launcher data folder and any folder needed to access it.\n" "(%1)\n" "\n" "The launcher cannot continue until you fix this problem.") .arg(dataPath)); return; } if (!QDir::setCurrent(dataPath)) { showFatalErrorMessage("The launcher data folder could not be opened.", QString("The launcher data folder could not be opened.\n" "\n" "Make sure you have the right permissions to the launcher data folder.\n" "(%1)\n" "\n" "The launcher cannot continue until you fix this problem.") .arg(dataPath)); return; } m_dataPath = dataPath; /* * Establish the mechanism for communication with an already running PrismLauncher that uses the same data path. * If there is one, tell it what the user actually wanted to do and exit. * We want to initialize this before logging to avoid messing with the log of a potential already running copy. */ auto appID = ApplicationId::fromPathAndVersion(QDir::currentPath(), BuildConfig.printableVersionString()); { // FIXME: you can run the same binaries with multiple data dirs and they won't clash. This could cause issues for updates. m_peerInstance = new LocalPeer(this, appID); connect(m_peerInstance, &LocalPeer::messageReceived, this, &Application::messageReceived); if (m_peerInstance->isClient()) { bool sentMessage = false; int timeout = 2000; if (m_instanceIdToLaunch.isEmpty()) { ApplicationMessage activate; activate.command = "activate"; sentMessage = m_peerInstance->sendMessage(activate.serialize(), timeout); if (!m_urlsToImport.isEmpty()) { for (auto url : m_urlsToImport) { ApplicationMessage import; import.command = "import"; import.args.insert("url", url.toString()); sentMessage = m_peerInstance->sendMessage(import.serialize(), timeout); } } } else { ApplicationMessage launch; launch.command = "launch"; launch.args["id"] = m_instanceIdToLaunch; if (!m_serverToJoin.isEmpty()) { launch.args["server"] = m_serverToJoin; } else if (!m_worldToJoin.isEmpty()) { launch.args["world"] = m_worldToJoin; } if (!m_profileToUse.isEmpty()) { launch.args["profile"] = m_profileToUse; } if (m_launchOffline) { launch.args["offline_enabled"] = "true"; launch.args["offline_name"] = m_offlineName; } sentMessage = m_peerInstance->sendMessage(launch.serialize(), timeout); } if (sentMessage) { m_status = Application::Succeeded; return; } else { std::cerr << "Unable to redirect command to already running instance\n"; // C function not Qt function - event loop not started yet ::exit(1); } } } // init the logger { static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log"; static const QString logBase = FS::PathCombine("logs", baseLogFile); if (FS::ensureFolderPathExists("logs")) { // if this did not fail for (auto i = 0; i <= 4; i++) if (auto oldName = baseLogFile.arg(i); QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there FS::move(oldName, logBase.arg(i)); } for (auto i = 4; i > 0; i--) FS::move(logBase.arg(i - 1), logBase.arg(i)); logFile = std::unique_ptr(new QFile(logBase.arg(0))); if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { showFatalErrorMessage("The launcher data folder is not writable!", QString("The launcher couldn't create a log file - %1.\n" "\n" "Make sure you have write permissions to the data folder.\n" "(%2)\n" "\n" "The launcher cannot continue until you fix this problem.") .arg(logFile->errorString()) .arg(dataPath)); return; } qInstallMessageHandler(appDebugOutput); qSetMessagePattern(defaultLogFormat); logModel.reset(new LogModel(this)); bool foundLoggingRules = false; auto logRulesFile = QStringLiteral("qtlogging.ini"); auto logRulesPath = FS::PathCombine(dataPath, logRulesFile); qInfo() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); // search the dataPath() // seach app data standard path if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) { logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); if (!logRulesPath.isEmpty()) { qInfo() << "Found" << logRulesPath << "..."; foundLoggingRules = true; } } // seach root path if (!foundLoggingRules) { #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) logRulesPath = FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME, logRulesFile); #else logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); #endif qInfo() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); } if (foundLoggingRules) { // load and set logging rules qInfo() << "Loading logging rules from:" << logRulesPath; QSettings loggingRules(logRulesPath, QSettings::IniFormat); loggingRules.beginGroup("Rules"); QStringList rule_names = loggingRules.childKeys(); QStringList rules; qInfo() << "Setting log rules:"; for (auto rule_name : rule_names) { auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString()); rules.append(rule); qInfo() << " " << rule; } auto rules_str = rules.join("\n"); QLoggingCategory::setFilterRules(rules_str); } qInfo() << "<> Log initialized."; } { bool migrated = false; if (!migrated) migrated = handleDataMigration( dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", "polymc.cfg"); if (!migrated) migrated = handleDataMigration( dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), "MultiMC", "multimc.cfg"); } { qInfo() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); qInfo() << "Version :" << BuildConfig.printableVersionString(); qInfo() << "Platform :" << BuildConfig.BUILD_PLATFORM; qInfo() << "Git commit :" << BuildConfig.GIT_COMMIT; qInfo() << "Git refspec :" << BuildConfig.GIT_REFSPEC; qInfo() << "Compiled for :" << BuildConfig.systemID(); qInfo() << "Compiled by :" << BuildConfig.compilerID(); qInfo() << "Build Artifact :" << BuildConfig.BUILD_ARTIFACT; qInfo() << "Updates Enabled :" << (updaterEnabled() ? "Yes" : "No"); if (adjustedBy.size()) { qInfo() << "Work dir before adjustment :" << origcwdPath; qInfo() << "Work dir after adjustment :" << QDir::currentPath(); qInfo() << "Adjusted by :" << adjustedBy; } else { qInfo() << "Work dir :" << QDir::currentPath(); } qInfo() << "Binary path :" << binPath; qInfo() << "Application root path :" << m_rootPath; if (!m_instanceIdToLaunch.isEmpty()) { qInfo() << "ID of instance to launch :" << m_instanceIdToLaunch; } if (!m_serverToJoin.isEmpty()) { qInfo() << "Address of server to join :" << m_serverToJoin; } else if (!m_worldToJoin.isEmpty()) { qInfo() << "Name of the world to join :" << m_worldToJoin; } qInfo() << "<> Paths set."; } if (m_liveCheck) { QFile check(liveCheckFile); if (check.open(QIODevice::WriteOnly | QIODevice::Truncate)) { auto payload = appID.toString().toUtf8(); if (check.write(payload) == payload.size()) { check.close(); } else { qWarning() << "Could not write into" << liveCheckFile << "error:" << check.errorString(); check.remove(); // also closes file! } } else { qWarning() << "Could not open" << liveCheckFile << "for writing:" << check.errorString(); } } // Initialize application settings { // Provide a fallback for migration from PolyMC m_settings.reset(new INISettingsObject({ BuildConfig.LAUNCHER_CONFIGFILE, "polymc.cfg", "multimc.cfg" }, this)); // Theming m_settings->registerSetting("IconTheme", QString()); m_settings->registerSetting("ApplicationTheme", QString()); m_settings->registerSetting("BackgroundCat", QString("kitteh")); // Remembered state m_settings->registerSetting("LastUsedGroupForNewInstance", QString()); m_settings->registerSetting("MenuBarInsteadOfToolBar", false); m_settings->registerSetting("NumberOfConcurrentTasks", 10); m_settings->registerSetting("NumberOfConcurrentDownloads", 6); m_settings->registerSetting("NumberOfManualRetries", 1); m_settings->registerSetting("RequestTimeout", 60); QString defaultMonospace; int defaultSize = 11; #ifdef Q_OS_WIN32 defaultMonospace = "Courier"; defaultSize = 10; #elif defined(Q_OS_MAC) defaultMonospace = "Menlo"; #else defaultMonospace = "Monospace"; #endif // resolve the font so the default actually matches QFont consoleFont; consoleFont.setFamily(defaultMonospace); consoleFont.setStyleHint(QFont::Monospace); consoleFont.setFixedPitch(true); QFontInfo consoleFontInfo(consoleFont); QString resolvedDefaultMonospace = consoleFontInfo.family(); QFont resolvedFont(resolvedDefaultMonospace); qDebug().nospace() << "Detected default console font: " << resolvedDefaultMonospace << ", substitutions: " << resolvedFont.substitutions().join(','); m_settings->registerSetting("ConsoleFont", resolvedDefaultMonospace); m_settings->registerSetting("ConsoleFontSize", defaultSize); m_settings->registerSetting("ConsoleMaxLines", 100000); m_settings->registerSetting("ConsoleOverflowStop", true); logModel->setMaxLines(getConsoleMaxLines(settings())); logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(settings())); logModel->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(logModel->getMaxLines())); // Folders m_settings->registerSetting("InstanceDir", "instances"); m_settings->registerSetting({ "CentralModsDir", "ModsDir" }, "mods"); m_settings->registerSetting("IconsDir", "icons"); m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); m_settings->registerSetting("DownloadsDirWatchRecursive", false); m_settings->registerSetting("MoveModsFromDownloadsDir", false); m_settings->registerSetting("SkinsDir", "skins"); m_settings->registerSetting("JavaDir", "java"); #ifdef Q_OS_MACOS // Folder security-scoped bookmarks m_settings->registerSetting("InstanceDirBookmark", ""); m_settings->registerSetting("CentralModsDirBookmark", ""); m_settings->registerSetting("IconsDirBookmark", ""); m_settings->registerSetting("DownloadsDirBookmark", ""); m_settings->registerSetting("SkinsDirBookmark", ""); m_settings->registerSetting("JavaDirBookmark", ""); #endif // Editors m_settings->registerSetting("JsonEditor", QString()); // Language m_settings->registerSetting("Language", QString()); m_settings->registerSetting("UseSystemLocale", false); // Console m_settings->registerSetting("ShowConsole", false); m_settings->registerSetting("AutoCloseConsole", false); m_settings->registerSetting("ShowConsoleOnError", true); m_settings->registerSetting("LogPrePostOutput", true); // Window Size m_settings->registerSetting({ "LaunchMaximized", "MCWindowMaximize" }, false); m_settings->registerSetting({ "MinecraftWinWidth", "MCWindowWidth" }, 854); m_settings->registerSetting({ "MinecraftWinHeight", "MCWindowHeight" }, 480); // Proxy Settings m_settings->registerSetting("ProxyType", "None"); m_settings->registerSetting({ "ProxyAddr", "ProxyHostName" }, "127.0.0.1"); m_settings->registerSetting("ProxyPort", 8080); m_settings->registerSetting({ "ProxyUser", "ProxyUsername" }, ""); m_settings->registerSetting({ "ProxyPass", "ProxyPassword" }, ""); // Memory m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512); m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::defaultMaxJvmMem()); m_settings->registerSetting("PermGen", 128); m_settings->registerSetting("LowMemWarning", true); // Java Settings m_settings->registerSetting("JavaPath", ""); m_settings->registerSetting("JavaSignature", ""); m_settings->registerSetting("JavaArchitecture", ""); m_settings->registerSetting("JavaRealArchitecture", ""); m_settings->registerSetting("JavaVersion", ""); m_settings->registerSetting("JavaVendor", ""); m_settings->registerSetting("LastHostname", ""); m_settings->registerSetting("JvmArgs", ""); m_settings->registerSetting("IgnoreJavaCompatibility", false); m_settings->registerSetting("IgnoreJavaWizard", false); auto defaultEnableAutoJava = m_settings->get("JavaPath").toString().isEmpty(); m_settings->registerSetting("AutomaticJavaSwitch", defaultEnableAutoJava); m_settings->registerSetting("AutomaticJavaDownload", defaultEnableAutoJava); m_settings->registerSetting("UserAskedAboutAutomaticJavaDownload", false); // Legacy settings m_settings->registerSetting("OnlineFixes", false); // Native library workarounds m_settings->registerSetting("UseNativeOpenAL", false); m_settings->registerSetting("CustomOpenALPath", ""); m_settings->registerSetting("UseNativeGLFW", false); m_settings->registerSetting("CustomGLFWPath", ""); // Performance related options m_settings->registerSetting("EnableFeralGamemode", false); m_settings->registerSetting("EnableMangoHud", false); m_settings->registerSetting("UseDiscreteGpu", false); m_settings->registerSetting("UseZink", false); // Game time m_settings->registerSetting("ShowGameTime", true); m_settings->registerSetting("ShowGlobalGameTime", true); m_settings->registerSetting("RecordGameTime", true); m_settings->registerSetting("ShowGameTimeWithoutDays", false); // Minecraft mods m_settings->registerSetting("ModMetadataDisabled", false); m_settings->registerSetting("ModDependenciesDisabled", false); m_settings->registerSetting("SkipModpackUpdatePrompt", false); m_settings->registerSetting("ShowModIncompat", false); // Minecraft offline player name m_settings->registerSetting("LastOfflinePlayerName", ""); // Wrapper command for launch m_settings->registerSetting("WrapperCommand", ""); // Custom Commands m_settings->registerSetting({ "PreLaunchCommand", "PreLaunchCmd" }, ""); m_settings->registerSetting({ "PostExitCommand", "PostExitCmd" }, ""); // The cat m_settings->registerSetting("TheCat", false); m_settings->registerSetting("CatOpacity", 100); m_settings->registerSetting("CatFit", "fit"); m_settings->registerSetting("StatusBarVisible", true); m_settings->registerSetting("ToolbarsLocked", false); // Instance m_settings->registerSetting("InstSortMode", "Name"); m_settings->registerSetting("InstRenamingMode", "AskEverytime"); m_settings->registerSetting("SelectedInstance", QString()); // Window state and geometry m_settings->registerSetting("MainWindowState", ""); m_settings->registerSetting("MainWindowGeometry", ""); m_settings->registerSetting("ConsoleWindowState", ""); m_settings->registerSetting("ConsoleWindowGeometry", ""); m_settings->registerSetting("SettingsGeometry", ""); m_settings->registerSetting("PagedGeometry", ""); m_settings->registerSetting("NewInstanceGeometry", ""); m_settings->registerSetting("UpdateDialogGeometry", ""); m_settings->registerSetting("NewsGeometry", ""); m_settings->registerSetting("ModDownloadGeometry", ""); m_settings->registerSetting("RPDownloadGeometry", ""); m_settings->registerSetting("TPDownloadGeometry", ""); m_settings->registerSetting("ShaderDownloadGeometry", ""); m_settings->registerSetting("DataPackDownloadGeometry", ""); // data pack window // in future, more pages may be added - so this name is chosen to avoid needing migration m_settings->registerSetting("WorldManagementGeometry", ""); // HACK: This code feels so stupid is there a less stupid way of doing this? { m_settings->registerSetting("PastebinURL", ""); m_settings->registerSetting("PastebinType", PasteUpload::PasteType::Mclogs); m_settings->registerSetting("PastebinCustomAPIBase", ""); QString pastebinURL = m_settings->get("PastebinURL").toString(); bool userHadDefaultPastebin = pastebinURL == "https://0x0.st"; if (!pastebinURL.isEmpty() && !userHadDefaultPastebin) { m_settings->set("PastebinType", PasteUpload::PasteType::NullPointer); m_settings->set("PastebinCustomAPIBase", pastebinURL); m_settings->reset("PastebinURL"); } bool ok; int pasteType = m_settings->get("PastebinType").toInt(&ok); // If PastebinType is invalid then reset the related settings. if (!ok || !(PasteUpload::PasteType::First <= pasteType && pasteType <= PasteUpload::PasteType::Last)) { m_settings->reset("PastebinType"); m_settings->reset("PastebinCustomAPIBase"); } } { auto resetIfInvalid = [this](const Setting* setting) { if (const QUrl url(setting->get().toString()); !url.isValid() || (url.scheme() != "http" && url.scheme() != "https")) { m_settings->reset(setting->id()); } }; // Meta URL resetIfInvalid(m_settings->registerSetting("MetaURLOverride", "").get()); // Resource URL resetIfInvalid(m_settings->registerSetting({ "ResourceURLOverride", "ResourceURL" }, "").get()); // Legacy FML libs URL resetIfInvalid(m_settings->registerSetting("LegacyFMLLibsURLOverride", "").get()); } m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); m_settings->registerSetting("Env", "{}"); // Custom Microsoft Authentication Client ID m_settings->registerSetting("MSAClientIDOverride", ""); // Custom Flame API Key { m_settings->registerSetting("CFKeyOverride", ""); m_settings->registerSetting("FlameKeyOverride", ""); QString flameKey = m_settings->get("CFKeyOverride").toString(); if (!flameKey.isEmpty()) m_settings->set("FlameKeyOverride", flameKey); m_settings->reset("CFKeyOverride"); } m_settings->registerSetting("FallbackMRBlockedMods", true); m_settings->registerSetting("ModrinthToken", ""); m_settings->registerSetting("UserAgentOverride", ""); // FTBApp instances m_settings->registerSetting("FTBAppInstancesPath", ""); // Custom Technic Client ID m_settings->registerSetting("TechnicClientID", ""); // Init page provider { m_globalSettingsProvider = std::make_unique(tr("Settings")); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); } PixmapCache::setInstance(new PixmapCache(this)); qInfo() << "<> Settings loaded."; } #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(groupViewAccessibleFactory); #endif /* !QT_NO_ACCESSIBILITY */ // initialize network access and proxy setup { m_network.reset(new QNetworkAccessManager()); QString proxyTypeStr = settings()->get("ProxyType").toString(); QString addr = settings()->get("ProxyAddr").toString(); int port = settings()->get("ProxyPort").value(); QString user = settings()->get("ProxyUser").toString(); QString pass = settings()->get("ProxyPass").toString(); updateProxySettings(proxyTypeStr, addr, port, user, pass); qInfo() << "<> Network done."; } // Instance icons { auto setting = APPLICATION->settings()->getSetting("IconsDir"); QStringList instFolders = { ":/icons/multimc/32x32/instances/", ":/icons/multimc/50x50/instances/", ":/icons/multimc/128x128/instances/", ":/icons/multimc/scalable/instances/" }; m_icons.reset(new IconList(instFolders, setting->get().toString())); connect(setting.get(), &Setting::SettingChanged, [this](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); qInfo() << "<> Instance icons initialized."; } // Themes m_themeManager = std::make_unique(); #ifdef Q_OS_MACOS // for macOS: getting directory settings will generate URL security-scoped bookmarks if needed and not present // this facilitates a smooth transition from a non-sandboxed version of the launcher, that likely can access the directory, // and a sandboxed version that can't access the directory without a bookmark // this section can likely be removed once the sandboxed version has been released for a while and migrations aren't done anymore { m_settings->get("InstanceDir"); m_settings->get("CentralModsDir"); m_settings->get("IconsDir"); m_settings->get("DownloadsDir"); m_settings->get("SkinsDir"); m_settings->get("JavaDir"); } #endif // initialize and load all instances { auto InstDirSetting = m_settings->getSetting("InstanceDir"); // instance path: check for problems with '!' in instance path and warn the user in the log // and remember that we have to show him a dialog when the gui starts (if it does so) QString instDir = m_settings->get("InstanceDir").toString(); qInfo() << "Instance path :" << instDir; if (FS::checkProblemticPathJava(QDir(instDir))) { qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!"; } m_instances.reset(new InstanceList(m_settings.get(), instDir, this)); connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged); qInfo() << "Loading Instances..."; m_instances->loadList(); qInfo() << "<> Instances loaded."; } // and accounts { m_accounts.reset(new AccountList(this)); qInfo() << "Loading accounts..."; m_accounts->setListFilePath("accounts.json", true); m_accounts->loadList(); m_accounts->fillQueue(); qInfo() << "<> Accounts loaded."; } // init the http meta cache { m_metacache.reset(new HttpMetaCache("metacache")); m_metacache->addBase("asset_indexes", QDir("assets/indexes").absolutePath()); m_metacache->addBase("libraries", QDir("libraries").absolutePath()); m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath()); m_metacache->addBase("general", QDir("cache").absolutePath()); m_metacache->addBase("ATLauncherPacks", QDir("cache/ATLauncherPacks").absolutePath()); m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath()); m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath()); m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath()); m_metacache->addBase("FlameMods", QDir("cache/FlameMods").absolutePath()); m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath()); m_metacache->addBase("ModrinthModpacks", QDir("cache/ModrinthModpacks").absolutePath()); m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("meta", QDir("meta").absolutePath()); m_metacache->addBase("java", QDir("cache/java").absolutePath()); m_metacache->addBase("feed", QDir("cache/feed").absolutePath()); m_metacache->Load(); qInfo() << "<> Cache initialized."; } // load translations { m_translations.reset(new TranslationsModel("translations")); m_translations->downloadIndex(); qInfo() << "Your language is" << m_translations->selectedLanguage(); qInfo() << "<> Translations loaded."; } // FIXME: what to do with these? m_profilers.insert("jprofiler", std::shared_ptr(new JProfilerFactory())); m_profilers.insert("jvisualvm", std::shared_ptr(new JVisualVMFactory())); m_profilers.insert("generic", std::shared_ptr(new GenericProfilerFactory())); for (auto profiler : m_profilers.values()) { profiler->registerSettings(m_settings.get()); } // Create the MCEdit thing... why is this here? { m_mcedit.reset(new MCEditTool(m_settings.get())); } #ifdef Q_OS_MACOS connect(this, &Application::clickedOnDock, [this]() { this->showMainWindow(); }); #endif connect(this, &Application::aboutToQuit, [this]() { if (m_instances) { // save any remaining instance state m_instances->saveNow(); } if (logFile) { logFile->flush(); logFile->close(); } }); updateCapabilities(); detectLibraries(); // check update locks { auto update_log_path = FS::PathCombine(m_dataPath, "logs", "prism_launcher_update.log"); auto update_lock = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.lock")); if (update_lock.exists()) { auto [timestamp, from, to, target, data_path] = read_lock_File(update_lock.absoluteFilePath()); auto infoMsg = tr("This installation has a update lock file present at: %1\n" "\n" "Timestamp: %2\n" "Updating from version %3 to %4\n" "Target install path: %5\n" "Data Path: %6" "\n" "This likely means that a update attempt failed. Please ensure your installation is in working order before " "proceeding.\n" "Check the Prism Launcher updater log at: \n" "%7\n" "for details on the last update attempt.\n" "\n" "To delete this lock and proceed select \"Ignore\" below.") .arg(update_lock.absoluteFilePath()) .arg(timestamp.toString(Qt::ISODate), from, to, target, data_path) .arg(update_log_path); auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update In Progress"), infoMsg, QMessageBox::Ignore | QMessageBox::Abort); msgBox.setDefaultButton(QMessageBox::Abort); msgBox.setModal(true); msgBox.setDetailedText(FS::read(update_log_path)); msgBox.setMinimumWidth(460); msgBox.adjustSize(); auto res = msgBox.exec(); switch (res) { case QMessageBox::Ignore: { FS::deletePath(update_lock.absoluteFilePath()); break; } case QMessageBox::Abort: [[fallthrough]]; default: { qDebug() << "Exiting because update lockfile is present"; QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection); return; } } } auto update_fail_marker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.fail")); if (update_fail_marker.exists()) { auto infoMsg = tr("An update attempt failed\n" "\n" "Please ensure your installation is in working order before " "proceeding.\n" "Check the Prism Launcher updater log at: \n" "%1\n" "for details on the last update attempt.") .arg(update_log_path); auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update Failed"), infoMsg, QMessageBox::Ignore | QMessageBox::Abort); msgBox.setDefaultButton(QMessageBox::Abort); msgBox.setModal(true); msgBox.setDetailedText(FS::read(update_log_path)); msgBox.setMinimumWidth(460); msgBox.adjustSize(); auto res = msgBox.exec(); switch (res) { case QMessageBox::Ignore: { FS::deletePath(update_fail_marker.absoluteFilePath()); break; } case QMessageBox::Abort: [[fallthrough]]; default: { qDebug() << "Exiting because update lockfile is present"; QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection); return; } } } auto update_success_marker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.success")); if (update_success_marker.exists()) { auto infoMsg = tr("Update succeeded\n" "\n" "You are now running %1 .\n" "Check the Prism Launcher updater log at: \n" "%2\n" "for details.") .arg(BuildConfig.printableVersionString()) .arg(update_log_path); auto msgBox = new QMessageBox(QMessageBox::Information, tr("Update Succeeded"), infoMsg, QMessageBox::Ok); msgBox->setDefaultButton(QMessageBox::Ok); msgBox->setDetailedText(FS::read(update_log_path)); msgBox->setAttribute(Qt::WA_DeleteOnClose); msgBox->setMinimumWidth(460); msgBox->adjustSize(); msgBox->open(); FS::deletePath(update_success_marker.absoluteFilePath()); } } // notify user if /tmp is mounted with `noexec` (#1693) QString jvmArgs = m_settings->get("JvmArgs").toString(); if (jvmArgs.indexOf("java.io.tmpdir") == -1) { /* java.io.tmpdir is a valid workaround, so don't annoy */ bool is_tmp_noexec = false; #if defined(Q_OS_LINUX) struct statvfs tmp_stat; statvfs("/tmp", &tmp_stat); is_tmp_noexec = tmp_stat.f_flag & ST_NOEXEC; #elif defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) struct statfs tmp_stat; statfs("/tmp", &tmp_stat); is_tmp_noexec = tmp_stat.f_flags & MNT_NOEXEC; #endif if (is_tmp_noexec) { auto infoMsg = tr("Your /tmp directory is currently mounted with the 'noexec' flag enabled.\n" "Some versions of Minecraft may not launch.\n" "\n" "You may solve this issue by remounting /tmp as 'exec' or setting " "the java.io.tmpdir JVM argument to a writeable directory in a " "filesystem where the 'exec' flag is set (e.g., /home/user/.local/tmp)\n"); auto msgBox = new QMessageBox(QMessageBox::Information, tr("Incompatible system configuration"), infoMsg, QMessageBox::Ok); msgBox->setDefaultButton(QMessageBox::Ok); msgBox->setAttribute(Qt::WA_DeleteOnClose); msgBox->setMinimumWidth(460); msgBox->adjustSize(); msgBox->open(); } } if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") { installEventFilter(new ToolTipFilter); } if (createSetupWizard()) { return; } m_themeManager->applyCurrentlySelectedTheme(true); performMainStartupAction(); } bool Application::createSetupWizard() { bool javaRequired = [this]() { if (BuildConfig.JAVA_DOWNLOADER_ENABLED && settings()->get("AutomaticJavaDownload").toBool()) { return false; } bool ignoreJavaWizard = settings()->get("IgnoreJavaWizard").toBool(); if (ignoreJavaWizard) { return false; } QString currentHostName = QHostInfo::localHostName(); QString oldHostName = settings()->get("LastHostname").toString(); if (currentHostName != oldHostName) { settings()->set("LastHostname", currentHostName); return true; } QString currentJavaPath = settings()->get("JavaPath").toString(); QString actualPath = FS::ResolveExecutable(currentJavaPath); return actualPath.isNull(); }(); bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired && !settings()->get("AutomaticJavaDownload").toBool() && !settings()->get("AutomaticJavaSwitch").toBool() && !settings()->get("UserAskedAboutAutomaticJavaDownload").toBool(); bool languageRequired = settings()->get("Language").toString().isEmpty(); bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; bool validWidgets = m_themeManager->isValidApplicationTheme(settings()->get("ApplicationTheme").toString()); bool validIcons = m_themeManager->isValidIconTheme(settings()->get("IconTheme").toString()); bool login = !m_accounts->anyAccountIsValid() && capabilities() & Application::SupportsMSA; bool themeInterventionRequired = !validWidgets || !validIcons; bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired || askjava || login; if (wizardRequired) { // set default theme after going into theme wizard if (!validIcons) settings()->set("IconTheme", QString("pe_colored")); 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); m_setupWizard = new SetupWizard(nullptr); if (languageRequired) { m_setupWizard->addPage(new LanguageWizardPage(m_setupWizard)); } if (javaRequired) { m_setupWizard->addPage(new JavaWizardPage(m_setupWizard)); } else if (askjava) { m_setupWizard->addPage(new AutoJavaWizardPage(m_setupWizard)); } if (pasteInterventionRequired) { m_setupWizard->addPage(new PasteWizardPage(m_setupWizard)); } if (themeInterventionRequired) { m_setupWizard->addPage(new ThemeWizardPage(m_setupWizard)); } if (login) { m_setupWizard->addPage(new LoginWizardPage(m_setupWizard)); } connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished); m_setupWizard->show(); } return wizardRequired || login; } bool Application::updaterEnabled() { #if defined(Q_OS_MAC) return BuildConfig.UPDATER_ENABLED; #else return BuildConfig.UPDATER_ENABLED && QFileInfo(FS::PathCombine(m_rootPath, updaterBinaryName())).isFile(); #endif } QString Application::updaterBinaryName() { auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); #if defined Q_OS_WIN32 exe_name.append(".exe"); #else exe_name.prepend("bin/"); #endif return exe_name; } bool Application::event(QEvent* event) { #ifdef Q_OS_MACOS if (event->type() == QEvent::ApplicationStateChange) { auto ev = static_cast(event); if (m_prevAppState == Qt::ApplicationActive && ev->applicationState() == Qt::ApplicationActive) { emit clickedOnDock(); } m_prevAppState = ev->applicationState(); } #endif if (event->type() == QEvent::FileOpen) { if (!m_mainWindow) { showMainWindow(false); } auto ev = static_cast(event); m_mainWindow->processURLs({ ev->url() }); } return QApplication::event(event); } void Application::setupWizardFinished(int status) { qDebug() << "Wizard result =" << status; performMainStartupAction(); } void Application::performMainStartupAction() { m_status = Application::Initialized; if (!m_instanceIdToLaunch.isEmpty()) { auto inst = instances()->getInstanceById(m_instanceIdToLaunch); if (inst) { MinecraftTarget::Ptr targetToJoin = nullptr; MinecraftAccountPtr accountToUse = nullptr; qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching"; if (!m_serverToJoin.isEmpty()) { // FIXME: validate the server string targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(m_serverToJoin, false))); qDebug() << " Launching with server" << m_serverToJoin; } else if (!m_worldToJoin.isEmpty()) { targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(m_worldToJoin, true))); qDebug() << " Launching with world" << m_worldToJoin; } if (!m_profileToUse.isEmpty()) { accountToUse = accounts()->getAccountByProfileName(m_profileToUse); if (!accountToUse) { return; } qDebug() << " Launching with account" << m_profileToUse; } launch(inst, m_launchOffline ? LaunchMode::Offline : LaunchMode::Normal, targetToJoin, accountToUse, m_offlineName); if (!m_showMainWindow) { return; } } } if (!m_instanceIdToShowWindowOf.isEmpty()) { auto inst = instances()->getInstanceById(m_instanceIdToShowWindowOf); if (inst) { qDebug() << "<> Showing window of instance " << m_instanceIdToShowWindowOf; showInstanceWindow(inst); return; } } if (!m_mainWindow) { // normal main window showMainWindow(false); qDebug() << "<> Main window shown."; } // initialize the updater if (updaterEnabled()) { qDebug() << "Initializing updater"; #ifdef Q_OS_MAC #if defined(SPARKLE_ENABLED) m_updater.reset(new MacSparkleUpdater()); #endif #else m_updater.reset(new PrismExternalUpdater(m_mainWindow, m_rootPath, m_dataPath)); #endif qDebug() << "<> Updater started."; } { // delete instances tmp dirctory auto instDir = m_settings->get("InstanceDir").toString(); const QString tempRoot = FS::PathCombine(instDir, ".tmp"); FS::deletePath(tempRoot); } if (!m_urlsToImport.isEmpty()) { qDebug() << "<> Importing from url:" << m_urlsToImport; m_mainWindow->processURLs(m_urlsToImport); } } void Application::showFatalErrorMessage(const QString& title, const QString& content) { m_status = Application::Failed; auto dialog = CustomMessageBox::selectable(nullptr, title, content, QMessageBox::Critical); dialog->exec(); } Application::~Application() { // Shut down logger by setting the logger function to nothing qInstallMessageHandler(nullptr); } void Application::messageReceived(const QByteArray& message) { ApplicationMessage received; received.parse(message); auto& command = received.command; if (status() != Initialized) { bool isLoginAtempt = false; if (command == "import") { QString url = received.args["url"]; isLoginAtempt = !url.isEmpty() && normalizeImportUrl(url).scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME; } if (!isLoginAtempt) { qDebug() << "Received message" << message << "while still initializing. It will be ignored."; return; } } if (command == "activate") { showMainWindow(); } else if (command == "import") { QString url = received.args["url"]; if (url.isEmpty()) { 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"]; QString server = received.args["server"]; QString world = received.args["world"]; QString profile = received.args["profile"]; bool offline = received.args["offline_enabled"] == "true"; QString offlineName = received.args["offline_name"]; BaseInstance* instance; if (!id.isEmpty()) { instance = instances()->getInstanceById(id); if (!instance) { qWarning() << "Launch command requires an valid instance ID. " << id << "resolves to nothing."; return; } } else { qWarning() << "Launch command called without an instance ID..."; return; } MinecraftTarget::Ptr serverObject = nullptr; if (!server.isEmpty()) { serverObject = std::make_shared(MinecraftTarget::parse(server, false)); } else if (!world.isEmpty()) { serverObject = std::make_shared(MinecraftTarget::parse(world, true)); } MinecraftAccountPtr accountObject; if (!profile.isEmpty()) { accountObject = accounts()->getAccountByProfileName(profile); if (!accountObject) { qWarning() << "Launch command requires the specified profile to be valid. " << profile << "does not resolve to any account."; return; } } launch(instance, offline ? LaunchMode::Offline : LaunchMode::Normal, serverObject, accountObject, offlineName); } else { qWarning() << "Received invalid message" << message; } } TranslationsModel* Application::translations() { return m_translations.get(); } JavaInstallList* Application::javalist() { if (!m_javalist) { m_javalist.reset(new JavaInstallList()); } return m_javalist.get(); } QIcon Application::logo() { return QIcon(":/" + BuildConfig.LAUNCHER_SVGFILENAME); } bool Application::openJsonEditor(const QString& filename) { const QString file = QDir::current().absoluteFilePath(filename); if (m_settings->get("JsonEditor").toString().isEmpty()) { return DesktopServices::openUrl(QUrl::fromLocalFile(file)); } else { // return DesktopServices::openFile(m_settings->get("JsonEditor").toString(), file); return DesktopServices::run(m_settings->get("JsonEditor").toString(), { file }); } } bool Application::launch(BaseInstance* instance, LaunchMode mode, MinecraftTarget::Ptr targetToJoin, MinecraftAccountPtr accountToUse, const QString& offlineName) { if (m_updateRunning) { qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed."; } else if (instance->canLaunch()) { QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instance->id()]; auto window = extras.window; if (window) { if (!window->saveAll()) { return false; } } auto& controller = extras.controller; controller.reset(new LaunchController()); controller->setInstance(instance); controller->setLaunchMode(mode); controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get()); controller->setTargetToJoin(targetToJoin); controller->setAccountToUse(accountToUse); controller->setOfflineName(offlineName); if (window) { controller->setParentWidget(window); } else if (m_mainWindow) { controller->setParentWidget(m_mainWindow); } connect(controller.get(), &LaunchController::finished, this, &Application::controllerFinished); addRunningInstance(); QMetaObject::invokeMethod(controller.get(), &Task::start, Qt::QueuedConnection); return true; } else if (instance->isRunning()) { showInstanceWindow(instance, "console"); return true; } else if (instance->canEdit()) { showInstanceWindow(instance); return true; } return false; } bool Application::kill(BaseInstance* instance) { if (!instance->isRunning()) { qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running."; return false; } QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instance->id()]; // NOTE: copy of the shared pointer keeps it alive auto& controller = extras.controller; locker.unlock(); if (controller) { return controller->abort(); } return true; } void Application::closeCurrentWindow() { if (focusWindow()) focusWindow()->close(); } void Application::addRunningInstance() { m_runningInstances++; if (m_runningInstances == 1) { emit updateAllowedChanged(false); } } void Application::subRunningInstance() { if (m_runningInstances == 0) { qCritical() << "Something went really wrong and we now have less than 0 running instances... WTF"; return; } m_runningInstances--; if (m_runningInstances == 0) { emit updateAllowedChanged(true); } } bool Application::shouldExitNow() const { return m_runningInstances == 0 && m_openWindows == 0; } bool Application::updatesAreAllowed() { return m_runningInstances == 0; } void Application::updateIsRunning(bool running) { m_updateRunning = running; } void Application::controllerFinished() { auto controller = qobject_cast(sender()); if (!controller) return; auto id = controller->id(); QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras.at(id); const bool wasSuccessful = controller->wasSuccessful(); // on success, do... if (wasSuccessful && controller->instance()->settings()->get("AutoCloseConsole").toBool()) { if (extras.window) { QMetaObject::invokeMethod(extras.window, &QWidget::close, Qt::QueuedConnection); } } extras.controller.reset(); subRunningInstance(); // quit when there are no more windows. if (shouldExitNow()) { m_status = wasSuccessful ? Succeeded : Failed; exit(wasSuccessful ? 0 : 1); } } void Application::ShowGlobalSettings(class QWidget* parent, QString open_page) { if (!m_globalSettingsProvider) { return; } emit globalSettingsAboutToOpen(); { SettingsObject::Lock lock(APPLICATION->settings()); PageDialog dlg(m_globalSettingsProvider.get(), open_page, parent); connect(&dlg, &PageDialog::applied, this, &Application::globalSettingsApplied); dlg.exec(); } } MainWindow* Application::showMainWindow(bool minimized) { if (m_mainWindow) { m_mainWindow->setWindowState(m_mainWindow->windowState() & ~Qt::WindowMinimized); m_mainWindow->raise(); m_mainWindow->activateWindow(); } else { m_mainWindow = new MainWindow(); m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toString().toUtf8())); m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toString().toUtf8())); if (minimized) { m_mainWindow->showMinimized(); } else { m_mainWindow->show(); } m_mainWindow->checkInstancePathForProblems(); connect(this, &Application::updateAllowedChanged, m_mainWindow, &MainWindow::updatesAllowedChanged); connect(m_mainWindow, &MainWindow::isClosing, this, &Application::on_windowClose); m_openWindows++; } return m_mainWindow; } ViewLogWindow* Application::showLogWindow() { if (m_viewLogWindow) { m_viewLogWindow->setWindowState(m_viewLogWindow->windowState() & ~Qt::WindowMinimized); m_viewLogWindow->raise(); m_viewLogWindow->activateWindow(); } else { m_viewLogWindow = new ViewLogWindow(); connect(m_viewLogWindow, &ViewLogWindow::isClosing, this, &Application::on_windowClose); m_openWindows++; } return m_viewLogWindow; } InstanceWindow* Application::showInstanceWindow(BaseInstance* instance, QString page) { if (!instance) return nullptr; auto id = instance->id(); QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[id]; auto& window = extras.window; if (window) { // If the window is minimized on macOS or Windows, activate and bring it up #ifdef Q_OS_MACOS if (window->isMinimized()) { window->setWindowState(window->windowState() & ~Qt::WindowMinimized); } #elif defined(Q_OS_WIN) if (window->isMinimized()) { window->showNormal(); } #endif window->raise(); window->activateWindow(); } else { window = new InstanceWindow(instance); m_openWindows++; connect(window, &InstanceWindow::isClosing, this, &Application::on_windowClose); } if (!page.isEmpty()) { window->selectPage(page); } if (extras.controller) { extras.controller->setParentWidget(window); } return window; } void Application::on_windowClose() { m_openWindows--; auto instWindow = qobject_cast(sender()); if (instWindow) { QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instWindow->instanceId()]; extras.window = nullptr; if (extras.controller) { extras.controller->setParentWidget(m_mainWindow); } } auto mainWindow = qobject_cast(sender()); if (mainWindow) { m_mainWindow = nullptr; } auto logWindow = qobject_cast(sender()); if (logWindow) { m_viewLogWindow = nullptr; } // quit when there are no more windows. if (shouldExitNow()) { exit(0); } } void Application::updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password) { // Set the application proxy settings. if (proxyTypeStr == "SOCKS5") { QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::Socks5Proxy, addr, port, user, password)); } else if (proxyTypeStr == "HTTP") { QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::HttpProxy, addr, port, user, password)); } else if (proxyTypeStr == "None") { // If we have no proxy set, set no proxy and return. QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::NoProxy)); } else { // If we have "Default" selected, set Qt to use the system proxy settings. QNetworkProxyFactory::setUseSystemConfiguration(true); } qDebug() << "Detecting proxy settings..."; QNetworkProxy proxy = QNetworkProxy::applicationProxy(); m_network->setProxy(proxy); QString proxyDesc; if (proxy.type() == QNetworkProxy::NoProxy) { qDebug() << "Using no proxy is an option!"; return; } switch (proxy.type()) { case QNetworkProxy::DefaultProxy: proxyDesc = "Default proxy: "; break; case QNetworkProxy::Socks5Proxy: proxyDesc = "Socks5 proxy: "; break; case QNetworkProxy::HttpProxy: proxyDesc = "HTTP proxy: "; break; case QNetworkProxy::HttpCachingProxy: proxyDesc = "HTTP caching: "; break; case QNetworkProxy::FtpCachingProxy: proxyDesc = "FTP caching: "; break; default: proxyDesc = "DERP proxy: "; break; } proxyDesc += QString("%1:%2").arg(proxy.hostName()).arg(proxy.port()); qDebug() << proxyDesc; } HttpMetaCache* Application::metacache() { return m_metacache.get(); } QNetworkAccessManager* Application::network() { return m_network.get(); } Meta::Index* Application::metadataIndex() { if (!m_metadataIndex) { m_metadataIndex.reset(new Meta::Index()); } return m_metadataIndex.get(); } void Application::updateCapabilities() { m_capabilities = None; if (!getMSAClientID().isEmpty()) m_capabilities |= SupportsMSA; if (!getFlameAPIKey().isEmpty()) m_capabilities |= SupportsFlame; #ifdef Q_OS_LINUX if (gamemode_query_status() >= 0) m_capabilities |= SupportsGameMode; if (!LibraryUtils::findMangoHud().isEmpty()) m_capabilities |= SupportsMangoHud; #endif } void Application::detectLibraries() { #ifdef Q_OS_LINUX m_detectedGLFWPath = LibraryUtils::find(BuildConfig.GLFW_LIBRARY_NAME); m_detectedOpenALPath = LibraryUtils::find(BuildConfig.OPENAL_LIBRARY_NAME); qDebug() << "Detected native libraries:" << m_detectedGLFWPath << m_detectedOpenALPath; #endif } QString Application::getJarPath(QString jarFile) { QStringList potentialPaths = { #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME), #endif FS::PathCombine(m_rootPath, "jars"), FS::PathCombine(applicationDirPath(), "jars"), FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir, for debuging }; for (QString p : potentialPaths) { QString jarPath = FS::PathCombine(p, jarFile); if (QFileInfo(jarPath).isFile()) return jarPath; } return {}; } QString Application::getMSAClientID() { QString clientIDOverride = m_settings->get("MSAClientIDOverride").toString(); if (!clientIDOverride.isEmpty()) { return clientIDOverride; } return BuildConfig.MSA_CLIENT_ID; } QString Application::getFlameAPIKey() { QString keyOverride = m_settings->get("FlameKeyOverride").toString(); if (!keyOverride.isEmpty()) { return keyOverride; } return BuildConfig.FLAME_API_KEY; } QString Application::getModrinthAPIToken() { QString tokenOverride = m_settings->get("ModrinthToken").toString(); if (!tokenOverride.isEmpty()) return tokenOverride; return QString(); } QString Application::getUserAgent() { QString uaOverride = m_settings->get("UserAgentOverride").toString(); if (!uaOverride.isEmpty()) { return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); } return BuildConfig.USER_AGENT; } bool Application::handleDataMigration(const QString& currentData, const QString& oldData, const QString& name, const QString& configFile) const { QString nomigratePath = FS::PathCombine(currentData, name + "_nomigrate.txt"); QStringList configPaths = { FS::PathCombine(oldData, configFile), FS::PathCombine(oldData, BuildConfig.LAUNCHER_CONFIGFILE) }; QLocale locale; // Is there a valid config at the old location? bool configExists = false; for (QString configPath : configPaths) { configExists |= QFileInfo::exists(configPath); } if (!configExists || QFileInfo::exists(nomigratePath)) { qDebug() << "<> No migration needed from" << name; return false; } QString message; bool currentExists = QFileInfo::exists(FS::PathCombine(currentData, BuildConfig.LAUNCHER_CONFIGFILE)); if (currentExists) { message = tr("Old data from %1 was found, but you already have existing data for %2. Sadly you will need to migrate yourself. Do " "you want to be reminded of the pending data migration next time you start %2?") .arg(name, BuildConfig.LAUNCHER_DISPLAYNAME); } else { message = tr("It looks like you used %1 before. Do you want to migrate your data to the new location of %2?") .arg(name, BuildConfig.LAUNCHER_DISPLAYNAME); QFileInfo logInfo(FS::PathCombine(oldData, name + "-0.log")); if (logInfo.exists()) { QString lastModified = logInfo.lastModified().toString(locale.dateFormat()); message = tr("It looks like you used %1 on %2 before. Do you want to migrate your data to the new location of %3?") .arg(name, lastModified, BuildConfig.LAUNCHER_DISPLAYNAME); } } QMessageBox::StandardButton askMoveDialogue = QMessageBox::question(nullptr, BuildConfig.LAUNCHER_DISPLAYNAME, message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); auto setDoNotMigrate = [&nomigratePath] { QFile file(nomigratePath); if (!file.open(QIODevice::WriteOnly)) { qWarning() << "setDoNotMigrate failed; Failed to open file" << file.fileName() << "for writing:" << file.errorString(); } }; // create no-migrate file if user doesn't want to migrate if (askMoveDialogue != QMessageBox::Yes) { qDebug() << "<> Migration declined for" << name; setDoNotMigrate(); return currentExists; // cancel further migrations, if we already have a data directory } if (!currentExists) { // Migrate! using namespace Filters; QList filters; filters.append(equals(configFile)); filters.append(equals(BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before filters.append(startsWith("logs/")); filters.append(equals("accounts.json")); filters.append(startsWith("accounts/")); filters.append(startsWith("assets/")); filters.append(startsWith("icons/")); filters.append(startsWith("instances/")); filters.append(startsWith("libraries/")); filters.append(startsWith("mods/")); filters.append(startsWith("themes/")); ProgressDialog diag; DataMigrationTask task(oldData, currentData, any(std::move(filters))); if (diag.execWithTask(&task)) { qDebug() << "<> Migration succeeded"; setDoNotMigrate(); } else { QString reason = task.failReason(); QMessageBox::critical(nullptr, BuildConfig.LAUNCHER_DISPLAYNAME, tr("Migration failed! Reason: %1").arg(reason)); } } else { qWarning() << "<> Migration was skipped, due to existing data"; } return true; } void Application::triggerUpdateCheck() { if (m_updater) { qDebug() << "Checking for updates."; m_updater->setBetaAllowed(false); // There are no other channels than stable m_updater->checkForUpdates(); } else { qDebug() << "Updater not available."; } } QUrl Application::normalizeImportUrl(const QString& url) { auto local_file = QFileInfo(url); if (local_file.exists()) { return QUrl::fromLocalFile(local_file.absoluteFilePath()); } else { return QUrl::fromUserInput(url); } } const QString Application::javaPath() { return m_settings->get("JavaDir").toString(); } void Application::addQSavePath(QString path) { QMutexLocker locker(&m_qsaveResourcesMutex); m_qsaveResources[path] = m_qsaveResources.value(path, 0) + 1; } void Application::removeQSavePath(QString path) { QMutexLocker locker(&m_qsaveResourcesMutex); auto count = m_qsaveResources.value(path, 0) - 1; if (count <= 0) { m_qsaveResources.remove(path); } else { m_qsaveResources[path] = count; } } bool Application::checkQSavePath(QString path) { QMutexLocker locker(&m_qsaveResourcesMutex); for (auto partialPath : m_qsaveResources.keys()) { if (path.startsWith(partialPath) && m_qsaveResources.value(partialPath, 0) > 0) { return true; } } return false; } PrismLauncher-11.0.3/launcher/tasks/0000755000175100017510000000000015224505336016711 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/tasks/SequentialTask.cpp0000644000175100017510000000410315224505336022350 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "SequentialTask.h" #include #include "tasks/ConcurrentTask.h" SequentialTask::SequentialTask(QString task_name) : ConcurrentTask(task_name, 1) {} void SequentialTask::subTaskFailed(Task::Ptr task, const QString& msg) { qWarning() << msg; ConcurrentTask::subTaskFailed(task, msg); emitFailed(msg); } void SequentialTask::updateState() { setProgress(m_done.count(), totalSize()); setStatus(tr("Executing task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(totalSize()))); } PrismLauncher-11.0.3/launcher/tasks/SequentialTask.h0000644000175100017510000000424615224505336022025 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ConcurrentTask.h" /** A concurrent task that only allows one concurrent task :) * * This should be used when there's a need to maintain a strict ordering of task executions, and * the starting of a task is contingent on the success of the previous one. * * See MultipleOptionsTask if that's not the case. */ class SequentialTask : public ConcurrentTask { Q_OBJECT public: explicit SequentialTask(QString task_name = ""); ~SequentialTask() override = default; protected slots: virtual void subTaskFailed(Task::Ptr, const QString& msg) override; protected: void updateState() override; }; PrismLauncher-11.0.3/launcher/tasks/Task.h0000644000175100017510000001555615224505336020000 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * PrismLauncher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include "QObjectPtr.h" Q_DECLARE_LOGGING_CATEGORY(taskLogC) enum class TaskStepState { Waiting, Running, Failed, Succeeded }; Q_DECLARE_METATYPE(TaskStepState) struct TaskStepProgress { QUuid uid; qint64 current = 0; qint64 total = -1; qint64 old_current = 0; qint64 old_total = -1; QString status = ""; QString details = ""; TaskStepState state = TaskStepState::Waiting; TaskStepProgress() { this->uid = QUuid::createUuid(); } TaskStepProgress(QUuid uid_) : uid(uid_) {} bool isDone() const { return (state == TaskStepState::Failed) || (state == TaskStepState::Succeeded); } void update(qint64 new_current, qint64 new_total) { this->old_current = this->current; this->old_total = this->total; this->current = new_current; this->total = new_total; this->state = TaskStepState::Running; } }; Q_DECLARE_METATYPE(TaskStepProgress) using TaskStepProgressList = QList>; /*! * Represents a task that has to be done. * To create a task, you need to subclass this class, implement the executeTask() method and call * emitSucceeded() or emitFailed() when the task is done. * the caller needs to call start() to start the task. */ class Task : public QObject, public QRunnable { Q_OBJECT public: using Ptr = shared_qobject_ptr; enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser }; public: explicit Task(bool show_debug_log = true); ~Task() override; bool isRunning() const; bool isFinished() const; bool wasSuccessful() const; /*! * MultiStep tasks are combinations of multiple tasks into a single logical task. * The main usage of this is in SequencialTask. */ virtual auto isMultiStep() const -> bool { return false; } /*! * Returns the string that was passed to emitFailed as the error message when the task failed. * If the task hasn't failed, returns an empty string. */ QString failReason() const; virtual QStringList warnings() const; virtual bool canAbort() const { return m_can_abort; } auto getState() const -> State { return m_state; } QString getStatus() { return m_status; } QString getDetails() { return m_details; } qint64 getProgress() { return m_progress; } qint64 getTotalProgress() { return m_progressTotal; } virtual auto getStepProgress() const -> TaskStepProgressList { return {}; } QUuid getUid() { return m_uid; } // Copies the other task's status, details, progress, and step progress to this task; and sets up connections for future propagation void propagateFromOther(Task* other); protected: void logWarning(const QString& line); private: QString describe(); signals: void started(); void progress(qint64 current, qint64 total); //! called when a task has either succeeded, aborted or failed. void finished(); //! called when a task has succeeded void succeeded(); //! called when a task has been aborted by calling abort() void aborted(); void failed(QString reason); void status(QString status); void details(QString details); void warningLogged(const QString& warning); void stepProgress(TaskStepProgress const& task_progress); //! Emitted when the canAbort() status has changed. */ void abortStatusChanged(bool can_abort); void abortButtonTextChanged(QString text); public slots: // QRunnable's interface void run() override { start(); } //! used by the task caller to start the task virtual void start(); //! used by external code to ask the task to abort virtual bool abort() { if (canAbort() && isRunning()) emitAborted(); return canAbort(); } void setAbortable(bool can_abort) { m_can_abort = can_abort; emit abortStatusChanged(can_abort); } void setAbortButtonText(QString text) { emit abortButtonTextChanged(text); } protected: //! The task subclass must implement this method. This method is called to start to run the task. //! The task is not finished when this method returns. the subclass must manually call emitSucceeded() or emitFailed() instead. virtual void executeTask() = 0; protected slots: //! The Task subclass must call this method when the task has succeeded virtual void emitSucceeded(); //! **The Task subclass** must call this method when the task has aborted. External code should call abort() instead. virtual void emitAborted(); //! The Task subclass must call this method when the task has failed virtual void emitFailed(QString reason = ""); virtual void propagateStepProgress(TaskStepProgress const& task_progress); public slots: void setStatus(const QString& status); void setDetails(const QString& details); void setProgress(qint64 current, qint64 total); protected: State m_state = State::Inactive; QStringList m_Warnings; QString m_failReason = ""; QString m_status; QString m_details; int m_progress = 0; int m_progressTotal = 100; // TODO: Nuke in favor of QLoggingCategory bool m_show_debug = true; private: // Change using setAbortStatus bool m_can_abort = false; QUuid m_uid; }; PrismLauncher-11.0.3/launcher/tasks/ConcurrentTask.h0000644000175100017510000000707615224505336022041 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include "tasks/Task.h" /*! * Runs a list of tasks concurrently (according to `max_concurrent` parameter). * Behaviour is the same as regular Task (e.g. starts using start()) */ class ConcurrentTask : public Task { Q_OBJECT public: using Ptr = shared_qobject_ptr; explicit ConcurrentTask(QString task_name = "", int max_concurrent = 6); ~ConcurrentTask() override; // safe to call before starting the task void setMaxConcurrent(int max_concurrent) { m_total_max_size = max_concurrent; } bool canAbort() const override { return true; } inline auto isMultiStep() const -> bool override { return totalSize() > 1; } auto getStepProgress() const -> TaskStepProgressList override; //! Adds a task to execute in this ConcurrentTask void addTask(Task::Ptr task); public slots: bool abort() override; /** Resets the internal state of the task. * This allows the same task to be re-used. */ void clear(); protected slots: void executeTask() override; virtual void executeNextSubTask(); void subTaskSucceeded(Task::Ptr); virtual void subTaskFailed(Task::Ptr, const QString& msg); void subTaskFinished(Task::Ptr, TaskStepState); void subTaskStatus(Task::Ptr task, const QString& msg); void subTaskDetails(Task::Ptr task, const QString& msg); void subTaskProgress(Task::Ptr task, qint64 current, qint64 total); protected: // NOTE: This is not thread-safe. unsigned int totalSize() const { return static_cast(m_queue.size() + m_doing.size() + m_done.size()); } virtual void updateState(); void startSubTask(Task::Ptr task); protected: QQueue m_queue; QHash m_doing; QHash m_done; QHash m_failed; QHash m_succeeded; QHash> m_task_progress; int m_total_max_size; }; PrismLauncher-11.0.3/launcher/tasks/MultipleOptionsTask.h0000644000175100017510000000367215224505336023064 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ConcurrentTask.h" /* This task type will attempt to do run each of it's subtasks in sequence, * until one of them succeeds. When that happens, the remaining tasks will not run. * */ class MultipleOptionsTask : public ConcurrentTask { Q_OBJECT public: explicit MultipleOptionsTask(const QString& task_name = ""); ~MultipleOptionsTask() override = default; private slots: void executeNextSubTask() override; void updateState() override; }; PrismLauncher-11.0.3/launcher/tasks/ConcurrentTask.cpp0000644000175100017510000001752715224505336022376 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "ConcurrentTask.h" #include #include "tasks/Task.h" ConcurrentTask::ConcurrentTask(QString task_name, int max_concurrent) : Task(), m_total_max_size(max_concurrent) { setObjectName(task_name); } ConcurrentTask::~ConcurrentTask() { for (auto task : m_doing) { if (task) task->disconnect(this); } } auto ConcurrentTask::getStepProgress() const -> TaskStepProgressList { return m_task_progress.values(); } void ConcurrentTask::addTask(Task::Ptr task) { m_queue.append(task); } void ConcurrentTask::executeTask() { for (auto i = 0; i < m_total_max_size; i++) QMetaObject::invokeMethod(this, &ConcurrentTask::executeNextSubTask, Qt::QueuedConnection); } bool ConcurrentTask::abort() { m_queue.clear(); if (m_doing.isEmpty()) { // Don't call emitAborted() here, we want to bypass the 'is the task running' check emit aborted(); emit finished(); return true; } bool suceedeed = true; QMutableHashIterator doing_iter(m_doing); while (doing_iter.hasNext()) { auto task = doing_iter.next(); disconnect(task->get(), &Task::aborted, this, 0); suceedeed &= (task.value())->abort(); } if (suceedeed) emitAborted(); else emitFailed(tr("Failed to abort all running tasks.")); return suceedeed; } void ConcurrentTask::clear() { Q_ASSERT(!isRunning()); m_done.clear(); m_failed.clear(); m_queue.clear(); m_task_progress.clear(); m_progress = 0; } void ConcurrentTask::executeNextSubTask() { if (!isRunning()) { return; } if (m_doing.count() >= m_total_max_size) { return; } if (m_queue.isEmpty()) { if (m_doing.isEmpty()) { if (m_failed.isEmpty()) { emitSucceeded(); } else if (m_failed.count() == 1) { auto task = m_failed.keys().first(); auto reason = task->failReason(); if (reason.isEmpty()) { // clearly a bug somewhere reason = tr("Task failed"); } emitFailed(reason); } else { QStringList failReason; for (auto t : m_failed) { auto reason = t->failReason(); if (!reason.isEmpty()) { failReason << reason; } } if (failReason.isEmpty()) { emitFailed(tr("Multiple subtasks failed")); } else { emitFailed(tr("Multiple subtasks failed\n%1").arg(failReason.join("\n"))); } } } return; } startSubTask(m_queue.dequeue()); } void ConcurrentTask::startSubTask(Task::Ptr next) { connect(next.get(), &Task::succeeded, this, [this, next]() { subTaskSucceeded(next); }); connect(next.get(), &Task::failed, this, [this, next](QString msg) { subTaskFailed(next, msg); }); // this should never happen but if it does, it's better to fail the task than get stuck connect(next.get(), &Task::aborted, this, [this, next] { subTaskFailed(next, "Aborted"); }); connect(next.get(), &Task::status, this, [this, next](QString msg) { subTaskStatus(next, msg); }); connect(next.get(), &Task::details, this, [this, next](QString msg) { subTaskDetails(next, msg); }); connect(next.get(), &Task::stepProgress, this, &ConcurrentTask::stepProgress); connect(next.get(), &Task::progress, this, [this, next](qint64 current, qint64 total) { subTaskProgress(next, current, total); }); m_doing.insert(next.get(), next); auto task_progress = std::make_shared(next->getUid()); m_task_progress.insert(next->getUid(), task_progress); updateState(); QMetaObject::invokeMethod(next.get(), &Task::start, Qt::QueuedConnection); } void ConcurrentTask::subTaskFinished(Task::Ptr task, TaskStepState state) { m_done.insert(task.get(), task); (state == TaskStepState::Succeeded ? m_succeeded : m_failed).insert(task.get(), task); m_doing.remove(task.get()); auto task_progress = *m_task_progress.value(task->getUid()); task_progress.state = state; m_task_progress.remove(task->getUid()); disconnect(task.get(), 0, this, 0); emit stepProgress(task_progress); updateState(); QMetaObject::invokeMethod(this, &ConcurrentTask::executeNextSubTask, Qt::QueuedConnection); } void ConcurrentTask::subTaskSucceeded(Task::Ptr task) { subTaskFinished(task, TaskStepState::Succeeded); } void ConcurrentTask::subTaskFailed(Task::Ptr task, [[maybe_unused]] const QString& msg) { subTaskFinished(task, TaskStepState::Failed); } void ConcurrentTask::subTaskStatus(Task::Ptr task, const QString& msg) { auto task_progress = m_task_progress.value(task->getUid()); task_progress->status = msg; task_progress->state = TaskStepState::Running; emit stepProgress(*task_progress); if (totalSize() == 1) { setStatus(msg); } } void ConcurrentTask::subTaskDetails(Task::Ptr task, const QString& msg) { auto task_progress = m_task_progress.value(task->getUid()); task_progress->details = msg; task_progress->state = TaskStepState::Running; emit stepProgress(*task_progress); if (totalSize() == 1) { setDetails(msg); } } void ConcurrentTask::subTaskProgress(Task::Ptr task, qint64 current, qint64 total) { auto task_progress = m_task_progress.value(task->getUid()); task_progress->update(current, total); emit stepProgress(*task_progress); updateState(); if (totalSize() == 1) { setProgress(task_progress->current, task_progress->total); } } void ConcurrentTask::updateState() { if (totalSize() > 1) { setProgress(m_done.count(), totalSize()); setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)") .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize()))); } else { QString status = tr("Please wait..."); if (m_queue.size() > 0) { status = tr("Waiting for a task to start..."); } else if (m_doing.size() > 0) { status = tr("Executing 1 task:"); } else if (m_done.size() > 0) { status = tr("Task finished."); } setStatus(status); } } PrismLauncher-11.0.3/launcher/tasks/Task.cpp0000644000175100017510000001452515224505336020326 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "Task.h" #include #include "AssertHelpers.h" Q_LOGGING_CATEGORY(taskLogC, "launcher.task") Task::Task(bool show_debug) : m_show_debug(show_debug) { m_uid = QUuid::createUuid(); setAutoDelete(false); } Task::~Task() { if (isRunning()) { qCWarning(taskLogC) << "Task" << describe() << "disposed while running!"; } } void Task::setStatus(const QString& new_status) { if (m_status != new_status) { m_status = new_status; emit status(m_status); } } void Task::setDetails(const QString& new_details) { if (m_details != new_details) { m_details = new_details; emit details(m_details); } } void Task::setProgress(qint64 current, qint64 total) { if ((m_progress != current) || (m_progressTotal != total)) { m_progress = current; m_progressTotal = total; emit progress(m_progress, m_progressTotal); } } void Task::start() { switch (m_state) { case State::Inactive: { if (m_show_debug) qCDebug(taskLogC) << "Task" << describe() << "starting for the first time"; break; } case State::AbortedByUser: { if (m_show_debug) qCDebug(taskLogC) << "Task" << describe() << "restarting for after being aborted by user"; break; } case State::Failed: { if (m_show_debug) qCDebug(taskLogC) << "Task" << describe() << "restarting for after failing at first"; break; } case State::Succeeded: { if (m_show_debug) qCDebug(taskLogC) << "Task" << describe() << "restarting for after succeeding at first"; break; } case State::Running: { if (ASSERT_NEVER(isRunning()) && m_show_debug) qCWarning(taskLogC) << "The launcher tried to start task" << describe() << "while it was already running!"; return; } } // NOTE: only fall through to here in end states m_state = State::Running; emit started(); executeTask(); } void Task::emitFailed(QString reason) { // Don't fail twice. if (ASSERT_NEVER(!isRunning())) { qCCritical(taskLogC) << "Task" << describe() << "failed while not running!!!!:" << reason; return; } m_state = State::Failed; m_failReason = reason; qCCritical(taskLogC) << "Task" << describe() << "failed:" << reason; emit failed(reason); emit finished(); } void Task::emitAborted() { // Don't abort twice. if (ASSERT_NEVER(!isRunning())) { qCCritical(taskLogC) << "Task" << describe() << "aborted while not running!!!!"; return; } m_state = State::AbortedByUser; m_failReason = tr("Aborted"); if (m_show_debug) qCDebug(taskLogC) << "Task" << describe() << "aborted."; emit aborted(); emit finished(); } void Task::emitSucceeded() { // Don't succeed twice. if (ASSERT_NEVER(!isRunning())) { qCCritical(taskLogC) << "Task" << describe() << "succeeded while not running!!!!"; return; } m_state = State::Succeeded; if (m_show_debug) qCDebug(taskLogC) << "Task" << describe() << "succeeded"; emit succeeded(); emit finished(); } void Task::propagateStepProgress(TaskStepProgress const& task_progress) { emit stepProgress(task_progress); } QString Task::describe() { QString outStr; QTextStream out(&outStr); out << metaObject()->className() << QChar('('); auto name = objectName(); if (name.isEmpty()) { out << QString("0x%1").arg(reinterpret_cast(this), 0, 16); } else { out << name; } out << " ID: " << m_uid.toString(QUuid::WithoutBraces); out << QChar(')'); out.flush(); return outStr; } bool Task::isRunning() const { return m_state == State::Running; } bool Task::isFinished() const { return m_state != State::Running && m_state != State::Inactive; } bool Task::wasSuccessful() const { return m_state == State::Succeeded; } QString Task::failReason() const { return m_failReason; } void Task::propagateFromOther(Task* other) { Q_ASSERT(other); connect(other, &Task::status, this, &Task::setStatus); connect(other, &Task::details, this, &Task::setDetails); connect(other, &Task::progress, this, &Task::setProgress); connect(other, &Task::stepProgress, this, &Task::propagateStepProgress); setStatus(other->getStatus()); setDetails(other->getDetails()); setProgress(other->getProgress(), other->getTotalProgress()); for (const auto& progress : other->getStepProgress()) { propagateStepProgress(*progress); } } void Task::logWarning(const QString& line) { qWarning() << line; m_Warnings.append(line); emit warningLogged(line); } QStringList Task::warnings() const { return m_Warnings; } PrismLauncher-11.0.3/launcher/tasks/MultipleOptionsTask.cpp0000644000175100017510000000424115224505336023410 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "MultipleOptionsTask.h" #include MultipleOptionsTask::MultipleOptionsTask(const QString& task_name) : ConcurrentTask(task_name, 1) {} void MultipleOptionsTask::executeNextSubTask() { if (m_done.size() != m_failed.size()) { emitSucceeded(); return; } if (m_queue.isEmpty()) { emitFailed(tr("All attempts have failed!")); qWarning() << "All attempts have failed!"; return; } ConcurrentTask::executeNextSubTask(); } void MultipleOptionsTask::updateState() { setProgress(m_done.count(), totalSize()); setStatus(tr("Attempting task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(totalSize()))); } PrismLauncher-11.0.3/launcher/InstanceCopyTask.cpp0000644000175100017510000001736615224505336021527 0ustar runnerrunner#include "InstanceCopyTask.h" #include #include #include #include "FileSystem.h" #include "Filter.h" #include "NullInstance.h" #include "settings/INISettingsObject.h" #include "tasks/Task.h" InstanceCopyTask::InstanceCopyTask(BaseInstance* origInstance, const InstanceCopyPrefs& prefs) { m_origInstance = origInstance; m_keepPlaytime = prefs.isKeepPlaytimeEnabled(); m_useLinks = prefs.isUseSymLinksEnabled(); m_linkRecursively = prefs.isLinkRecursivelyEnabled(); m_useHardLinks = prefs.isLinkRecursivelyEnabled() && prefs.isUseHardLinksEnabled(); m_copySaves = prefs.isLinkRecursivelyEnabled() && prefs.isDontLinkSavesEnabled() && prefs.isCopySavesEnabled(); m_useClone = prefs.isUseCloneEnabled(); QString filters = prefs.getSelectedFiltersAsRegex(); if (m_useLinks || m_useHardLinks) { if (!filters.isEmpty()) filters += "|"; filters += "instance.cfg"; } qDebug() << "CopyFilters:" << filters; if (!filters.isEmpty()) { // Set regex filter: // FIXME: get this from the original instance type... QRegularExpression regexp(filters, QRegularExpression::CaseInsensitiveOption); m_matcher = Filters::regexp(regexp); } } void InstanceCopyTask::executeTask() { setStatus(tr("Copying instance %1").arg(m_origInstance->name())); m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { if (m_useClone) { FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath); folderClone.matcher(m_matcher); folderClone(true); setProgress(0, folderClone.totalCloned()); connect(&folderClone, &FS::clone::fileCloned, [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); return folderClone(); } if (m_useLinks || m_useHardLinks) { std::unique_ptr savesCopy; if (m_copySaves) { QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft")); QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft")); QString staging_mc_dir; if (dotMCDir.exists() && !mcDir.exists()) staging_mc_dir = dotMCDir.filePath(); else staging_mc_dir = mcDir.filePath(); savesCopy = std::make_unique(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves")); (*savesCopy)(true); setProgress(0, savesCopy->totalCopied()); connect(savesCopy.get(), &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); }); } FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath); int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher); folderLink(true); setProgress(0, m_progressTotal + folderLink.totalToLink()); connect(&folderLink, &FS::create_link::fileLinked, [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); bool there_were_errors = false; if (!folderLink()) { #if defined Q_OS_WIN32 if (!m_useHardLinks) { setProgress(0, m_progressTotal); qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks"; qDebug() << "attempting to run with privelage"; QEventLoop loop; bool got_priv_results = false; connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&got_priv_results, &loop](bool gotResults) { if (!gotResults) { qDebug() << "Privileged run exited without results!"; } got_priv_results = gotResults; loop.quit(); }); folderLink.runPrivileged(); loop.exec(); // wait for the finished signal for (auto result : folderLink.getResults()) { if (result.err_value != 0) { there_were_errors = true; } } if (savesCopy) { there_were_errors |= !(*savesCopy)(); } return got_priv_results && !there_were_errors; } #else qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str(); #endif return false; } if (savesCopy) { there_were_errors |= !(*savesCopy)(); } return !there_were_errors; } FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); folderCopy.matcher(m_matcher); folderCopy(true); setProgress(0, folderCopy.totalCopied()); connect(&folderCopy, &FS::copy::fileCopied, [this]() { setProgress(m_progress + 1, m_progressTotal); }); return folderCopy(); }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &InstanceCopyTask::copyFinished); connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &InstanceCopyTask::copyAborted); m_copyFutureWatcher.setFuture(m_copyFuture); } void InstanceCopyTask::copyFinished() { auto successful = m_copyFuture.result(); if (!successful) { emitFailed(tr("Instance folder copy failed.")); return; } // FIXME: shouldn't this be able to report errors? auto instanceSettings = std::make_unique(FS::PathCombine(m_stagingPath, "instance.cfg")); BaseInstance* inst(new NullInstance(m_globalSettings, std::move(instanceSettings), m_stagingPath)); inst->setName(name()); inst->setIconKey(m_instIcon); if (!m_keepPlaytime) { inst->resetTimePlayed(); } if (m_useLinks) { inst->addLinkedInstanceId(m_origInstance->id()); auto allowed_symlinks_file = QFileInfo(FS::PathCombine(inst->gameRoot(), "allowed_symlinks.txt")); QByteArray allowed_symlinks; if (allowed_symlinks_file.exists()) { allowed_symlinks.append(FS::read(allowed_symlinks_file.filePath())); if (allowed_symlinks.right(1) != "\n") allowed_symlinks.append("\n"); // we want to be on a new line } allowed_symlinks.append(m_origInstance->gameRoot().toUtf8()); allowed_symlinks.append("\n"); if (allowed_symlinks_file.isSymLink()) FS::deletePath( allowed_symlinks_file .filePath()); // we dont want to modify the original. also make sure the resulting file is not itself a link. try { FS::write(allowed_symlinks_file.filePath(), allowed_symlinks); } catch (const FS::FileSystemException& e) { qCritical() << "Failed to write symlink :" << e.cause(); } } emitSucceeded(); } void InstanceCopyTask::copyAborted() { emitFailed(tr("Instance folder copy has been aborted.")); return; } bool InstanceCopyTask::abort() { if (m_copyFutureWatcher.isRunning()) { m_copyFutureWatcher.cancel(); // NOTE: Here we don't do `emitAborted()` because it will be done when `m_copyFutureWatcher` actually cancels, which may not occur // immediately. return true; } return false; } PrismLauncher-11.0.3/launcher/MessageLevel.h0000644000175100017510000000315415224505336020314 0ustar runnerrunner#pragma once #include #include /** * @brief the MessageLevel Enum * defines what level a log message is */ struct MessageLevel { enum class Enum { Unknown, /**< No idea what this is or where it came from */ StdOut, /**< Undetermined stderr messages */ StdErr, /**< Undetermined stdout messages */ Launcher, /**< Launcher Messages */ Trace, /**< Trace Messages */ Debug, /**< Debug Messages */ Info, /**< Info Messages */ Message, /**< Standard Messages */ Warning, /**< Warnings */ Error, /**< Errors */ Fatal, /**< Fatal Errors */ }; using enum Enum; constexpr MessageLevel(Enum e = Unknown) : m_type(e) {} static MessageLevel fromName(const QString& type); static MessageLevel fromQtMsgType(const QtMsgType& type); static MessageLevel fromLine(const QString& line); inline bool isValid() const { return m_type != Unknown; } std::strong_ordering operator<=>(const MessageLevel& other) const = default; std::strong_ordering operator<=>(const MessageLevel::Enum& other) const { return m_type <=> other; } explicit operator int() const { return static_cast(m_type); } explicit operator MessageLevel::Enum() { return m_type; } /* Get message level from a line. Line is modified if it was successful. */ static MessageLevel takeFromLine(QString& line); /* Get message level from a line from the launcher log. Line is modified if it was successful. */ static MessageLevel takeFromLauncherLine(QString& line); private: Enum m_type; }; PrismLauncher-11.0.3/launcher/FileIgnoreProxy.h0000644000175100017510000000671615224505336021034 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include "SeparatorPrefixTree.h" class FileIgnoreProxy : public QSortFilterProxyModel { Q_OBJECT public: FileIgnoreProxy(QString root, QObject* parent); // NOTE: Sadly, we have to do sorting ourselves. bool lessThan(const QModelIndex& left, const QModelIndex& right) const; virtual Qt::ItemFlags flags(const QModelIndex& index) const; virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); QString relPath(const QString& path) const; bool setFilterState(QModelIndex index, Qt::CheckState state); bool shouldExpand(QModelIndex index); void setBlockedPaths(QStringList paths); inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return m_blocked; } inline SeparatorPrefixTree<'/'>& blockedPaths() { return m_blocked; } // list of file names that need to be removed completely from model inline QStringList& ignoreFilesWithName() { return m_ignoreFiles; } inline QStringList& ignoreFilesWithSuffix() { return m_ignoreFilesSuffixes; } // list of relative paths that need to be removed completely from model inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; } bool filterFile(const QFileInfo& fileName) const; void loadBlockedPathsFromFile(const QString& fileName); void saveBlockedPathsToFile(const QString& fileName); protected: bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const; bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; bool ignoreFile(QFileInfo file) const; private: const QString m_root; SeparatorPrefixTree<'/'> m_blocked; QStringList m_ignoreFiles; QStringList m_ignoreFilesSuffixes; SeparatorPrefixTree<'/'> m_ignoreFilePaths; }; PrismLauncher-11.0.3/launcher/InstanceImportTask.cpp0000644000175100017510000004167415224505336022066 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "InstanceImportTask.h" #include "Application.h" #include "FileSystem.h" #include "NullInstance.h" #include "QObjectPtr.h" #include "archive/ArchiveReader.h" #include "archive/ExtractZipTask.h" #include "icons/IconList.h" #include "icons/IconUtils.h" #include "modplatform/flame/FlameInstanceCreationTask.h" #include "modplatform/modrinth/ModrinthInstanceCreationTask.h" #include "modplatform/technic/TechnicPackProcessor.h" #include "settings/INISettingsObject.h" #include "tasks/Task.h" #include "net/ApiDownload.h" #include #include #include InstanceImportTask::InstanceImportTask(const QUrl& sourceUrl, QWidget* parent, QMap&& extra_info) : m_sourceUrl(sourceUrl), m_extra_info(extra_info), m_parent(parent) {} bool InstanceImportTask::abort() { if (!canAbort()) return false; bool wasAborted = false; if (m_task) wasAborted = m_task->abort(); return wasAborted; } void InstanceImportTask::executeTask() { setAbortable(true); if (m_sourceUrl.isLocalFile()) { m_archivePath = m_sourceUrl.toLocalFile(); processZipPack(); } else { setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); downloadFromUrl(); } } void InstanceImportTask::downloadFromUrl() { const QString path(m_sourceUrl.host() + '/' + m_sourceUrl.path()); auto entry = APPLICATION->metacache()->resolveEntry("general", path); entry->setStale(true); m_archivePath = entry->getFullPath(); auto filesNetJob = makeShared(tr("Modpack download"), APPLICATION->network()); filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry)); connect(filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::processZipPack); connect(filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::setProgress); connect(filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress); connect(filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::emitFailed); connect(filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::emitAborted); m_task.reset(filesNetJob); filesNetJob->start(); } QString cleanPath(QString path) { if (path == ".") return QString(); QString result = path; if (result.startsWith("./")) result = result.mid(2); return result; } void InstanceImportTask::processZipPack() { setStatus(tr("Attempting to determine instance type")); QDir extractDir(m_stagingPath); qDebug() << "Attempting to create instance from" << m_archivePath; // open the zip and find relevant files in it MMCZip::ArchiveReader packZip(m_archivePath); qDebug() << "Attempting to determine instance type"; QString root; // NOTE: Prioritize modpack platforms that aren't searched for recursively. // Especially Flame has a very common filename for its manifest, which may appear inside overrides for example // https://docs.modrinth.com/docs/modpacks/format_definition/#storage auto detectInstance = [this, &extractDir, &root](MMCZip::ArchiveReader::File* f, bool& stop) { if (!isRunning()) { stop = true; return true; } auto fileName = f->filename(); if (fileName == "modrinth.index.json") { // process as Modrinth pack qDebug() << "Modrinth:" << true; m_modpackType = ModpackType::Modrinth; stop = true; } else if (fileName == "bin/modpack.jar" || fileName == "bin/version.json") { // process as Technic pack qDebug() << "Technic:" << true; extractDir.mkpath("minecraft"); extractDir.cd("minecraft"); m_modpackType = ModpackType::Technic; stop = true; } else if (fileName == "manifest.json") { qDebug() << "Flame:" << true; m_modpackType = ModpackType::Flame; stop = true; } else if (QFileInfo fileInfo(fileName); fileInfo.fileName() == "instance.cfg") { qDebug() << "MultiMC:" << true; m_modpackType = ModpackType::MultiMC; root = cleanPath(fileInfo.path()); stop = true; } QCoreApplication::processEvents(); return true; }; if (!packZip.parse(detectInstance)) { emitFailed(tr("Unable to open supplied modpack zip file.")); return; } if (m_modpackType == ModpackType::Unknown) { emitFailed(tr("Archive does not contain a recognized modpack type.")); return; } setStatus(tr("Extracting modpack")); // make sure we extract just the pack auto zipTask = makeShared(m_archivePath, extractDir, root); auto progressStep = std::make_shared(); connect(zipTask.get(), &Task::finished, this, [this, progressStep] { progressStep->state = TaskStepState::Succeeded; stepProgress(*progressStep); }); connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished, Qt::QueuedConnection); connect(zipTask.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) { progressStep->state = TaskStepState::Failed; stepProgress(*progressStep); emitFailed(reason); }); connect(zipTask.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); connect(zipTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { progressStep->update(current, total); stepProgress(*progressStep); }); connect(zipTask.get(), &Task::status, this, [this, progressStep](QString status) { progressStep->status = status; stepProgress(*progressStep); }); connect(zipTask.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); m_task.reset(zipTask); zipTask->start(); } void InstanceImportTask::extractFinished() { setAbortable(false); QDir extractDir(m_stagingPath); qDebug() << "Fixing permissions for extracted pack files..."; QDirIterator it(extractDir, QDirIterator::Subdirectories); while (it.hasNext()) { auto filepath = it.next(); QFileInfo file(filepath); auto permissions = QFile::permissions(filepath); auto origPermissions = permissions; if (file.isDir()) { // Folder +rwx for current user permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser; } else { // File +rw for current user permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; } if (origPermissions != permissions) { if (!QFile::setPermissions(filepath, permissions)) { logWarning(tr("Could not fix permissions for %1").arg(filepath)); } else { qDebug() << "Fixed" << filepath; } } } switch (m_modpackType) { case ModpackType::MultiMC: processMultiMC(); return; case ModpackType::Technic: processTechnic(); return; case ModpackType::Flame: processFlame(); return; case ModpackType::Modrinth: processModrinth(); return; case ModpackType::Unknown: emitFailed(tr("Archive does not contain a recognized modpack type.")); return; } } bool installIcon(QString root, QString instIconKey) { auto importIconPath = IconUtils::findBestIconIn(root, instIconKey); if (importIconPath.isNull() || !QFile::exists(importIconPath)) importIconPath = IconUtils::findBestIconIn(root, "icon.png"); if (importIconPath.isNull() || !QFile::exists(importIconPath)) importIconPath = IconUtils::findBestIconIn(FS::PathCombine(root, "overrides"), "icon.png"); if (!importIconPath.isNull() && QFile::exists(importIconPath)) { // import icon auto iconList = APPLICATION->icons(); if (iconList->iconFileExists(instIconKey)) { iconList->deleteIcon(instIconKey); } iconList->installIcon(importIconPath, instIconKey + "." + QFileInfo(importIconPath).suffix()); return true; } return false; } void InstanceImportTask::processFlame() { shared_qobject_ptr inst_creation_task = nullptr; if (!m_extra_info.isEmpty()) { auto pack_id_it = m_extra_info.constFind("pack_id"); Q_ASSERT(pack_id_it != m_extra_info.constEnd()); auto pack_id = pack_id_it.value(); auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); Q_ASSERT(pack_version_id_it != m_extra_info.constEnd()); auto pack_version_id = pack_version_id_it.value(); QString original_instance_id; auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); if (original_instance_id_it != m_extra_info.constEnd()) original_instance_id = original_instance_id_it.value(); inst_creation_task = makeShared(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); } else { // FIXME: Find a way to get IDs in directly imported ZIPs inst_creation_task = makeShared(m_stagingPath, m_globalSettings, m_parent, QString(), QString()); } inst_creation_task->setName(*this); // if the icon was specified by user, use that. otherwise pull icon from the pack if (m_instIcon == "default") { auto iconKey = QString("Flame_%1_Icon").arg(name()); if (installIcon(m_stagingPath, iconKey)) { m_instIcon = iconKey; } } inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); auto weak = inst_creation_task.toWeakRef(); connect(inst_creation_task.get(), &Task::succeeded, this, [this, weak] { if (auto sp = weak.lock()) { setOverride(sp->shouldOverride(), sp->originalInstanceID()); } emitSucceeded(); }); connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress); connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); connect(inst_creation_task.get(), &Task::abortButtonTextChanged, this, &Task::setAbortButtonText); connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); m_task.reset(inst_creation_task); setAbortable(true); m_task->start(); } void InstanceImportTask::processTechnic() { shared_qobject_ptr packProcessor{ new Technic::TechnicPackProcessor }; connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath); } void InstanceImportTask::processMultiMC() { QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_unique(configPath); NullInstance instance(m_globalSettings, std::move(instanceSettings), m_stagingPath); // reset time played on import... because packs. instance.resetTimePlayed(); // set a new nice name instance.setName(name()); // if the icon was specified by user, use that. otherwise pull icon from the pack if (m_instIcon != "default") { instance.setIconKey(m_instIcon); } else { m_instIcon = instance.iconKey(); installIcon(instance.instanceRoot(), m_instIcon); } emitSucceeded(); } void InstanceImportTask::processModrinth() { shared_qobject_ptr inst_creation_task = nullptr; if (!m_extra_info.isEmpty()) { auto pack_id_it = m_extra_info.constFind("pack_id"); Q_ASSERT(pack_id_it != m_extra_info.constEnd()); auto pack_id = pack_id_it.value(); QString pack_version_id; auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); if (pack_version_id_it != m_extra_info.constEnd()) pack_version_id = pack_version_id_it.value(); QString original_instance_id; auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); if (original_instance_id_it != m_extra_info.constEnd()) original_instance_id = original_instance_id_it.value(); inst_creation_task = makeShared(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); } else { QString pack_id; if (!m_sourceUrl.isEmpty()) { static const QRegularExpression s_regex(R"(data\/([^\/]*)\/versions)"); pack_id = s_regex.match(m_sourceUrl.toString()).captured(1); } // FIXME: Find a way to get the ID in directly imported ZIPs inst_creation_task = makeShared(m_stagingPath, m_globalSettings, m_parent, pack_id); } inst_creation_task->setName(*this); // if the icon was specified by user, use that. otherwise pull icon from the pack if (m_instIcon == "default") { auto iconKey = QString("Modrinth_%1_Icon").arg(name()); if (installIcon(m_stagingPath, iconKey)) { m_instIcon = iconKey; } } inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); auto weak = inst_creation_task.toWeakRef(); connect(inst_creation_task.get(), &Task::succeeded, this, [this, weak] { if (auto sp = weak.lock()) { setOverride(sp->shouldOverride(), sp->originalInstanceID()); } emitSucceeded(); }); connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress); connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); connect(inst_creation_task.get(), &Task::abortButtonTextChanged, this, &Task::setAbortButtonText); connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); m_task.reset(inst_creation_task); setAbortable(true); m_task->start(); } PrismLauncher-11.0.3/launcher/JavaCommon.cpp0000644000175100017510000001414515224505336020327 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "JavaCommon.h" #include "java/JavaUtils.h" #include "ui/dialogs/CustomMessageBox.h" #include bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) { static const QRegularExpression s_memRegex("-Xm[sx]"); static const QRegularExpression s_versionRegex("-version:.*"); if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(s_memRegex) || jvmargs.contains("-XX-MaxHeapSize") || jvmargs.contains("-XX:InitialHeapSize")) { auto warnStr = QObject::tr( "You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", \"-XX:InitialHeapSize\", \"-Xmx\" " "or \"-Xms\").\n" "There are dedicated boxes for these in the settings (Java tab, in the Memory group at the top).\n" "This message will be displayed until you remove them from the JVM arguments."); CustomMessageBox::selectable(parent, QObject::tr("JVM arguments warning"), warnStr, QMessageBox::Warning)->exec(); return false; } // block lunacy with passing required version to the JVM if (jvmargs.contains(s_versionRegex)) { auto warnStr = QObject::tr( "You tried to pass required Java version argument to the JVM (using \"-version:xxx\"). This is not safe and will not be " "allowed.\n" "This message will be displayed until you remove this from the JVM arguments."); CustomMessageBox::selectable(parent, QObject::tr("JVM arguments warning"), warnStr, QMessageBox::Warning)->exec(); return false; } return true; } void JavaCommon::javaWasOk(QWidget* parent, const JavaChecker::Result& result) { QString text; text += QObject::tr( "Java test succeeded!
    Platform reported: %1
    Java version " "reported: %2
    Java vendor " "reported: %3
    ") .arg(result.realPlatform, result.javaVersion.toString(), result.javaVendor); if (result.errorLog.size()) { auto htmlError = result.errorLog; htmlError.replace('\n', "
    "); text += QObject::tr("
    Warnings:
    %1").arg(htmlError); } CustomMessageBox::selectable(parent, QObject::tr("Java test success"), text, QMessageBox::Information)->show(); } void JavaCommon::javaArgsWereBad(QWidget* parent, const JavaChecker::Result& result) { auto htmlError = result.errorLog; QString text; htmlError.replace('\n', "
    "); text += QObject::tr("The specified Java binary didn't work with the arguments you provided:
    "); text += QString("%1").arg(htmlError); CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } void JavaCommon::javaBinaryWasBad(QWidget* parent, const JavaChecker::Result& result) { QString text; text += QObject::tr( "The specified Java binary didn't work.
    You should press 'Detect', " "or set the path to the Java executable.
    "); CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } void JavaCommon::javaCheckNotFound(QWidget* parent) { QString text; text += QObject::tr("Java checker library could not be found. Please check your installation."); CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } void JavaCommon::TestCheck::run() { if (!JavaCommon::checkJVMArgs(m_args, m_parent)) { emit finished(); return; } if (JavaUtils::getJavaCheckPath().isEmpty()) { javaCheckNotFound(m_parent); emit finished(); return; } checker.reset(new JavaChecker(m_path, "", 0, 0, 0, 0)); connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinished); checker->start(); } void JavaCommon::TestCheck::checkFinished(const JavaChecker::Result& result) { if (result.validity != JavaChecker::Result::Validity::Valid) { javaBinaryWasBad(m_parent, result); emit finished(); return; } checker.reset(new JavaChecker(m_path, m_args, m_maxMem, m_maxMem, result.javaVersion.requiresPermGen() ? m_permGen : 0, 0)); connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinishedWithArgs); checker->start(); } void JavaCommon::TestCheck::checkFinishedWithArgs(const JavaChecker::Result& result) { if (result.validity == JavaChecker::Result::Validity::Valid) { javaWasOk(m_parent, result); emit finished(); return; } javaArgsWereBad(m_parent, result); emit finished(); } PrismLauncher-11.0.3/launcher/java/0000755000175100017510000000000015224505336016505 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/java/JavaMetadata.h0000644000175100017510000000367015224505336021206 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include "BaseVersion.h" #include "java/JavaVersion.h" namespace Java { enum class DownloadType { Manifest, Archive, Unknown }; class Metadata : public BaseVersion { public: virtual QString descriptor() const override { return version.toString(); } virtual QString name() const override { return m_name; } virtual QString typeString() const override { return vendor; } virtual bool operator<(BaseVersion& a) const override; virtual bool operator>(BaseVersion& a) const override; bool operator<(const Metadata& rhs) const; bool operator==(const Metadata& rhs) const; bool operator>(const Metadata& rhs) const; QString m_name; QString vendor; QString url; QDateTime releaseTime; QString checksumType; QString checksumHash; DownloadType downloadType; QString packageType; JavaVersion version; QString runtimeOS; }; using MetadataPtr = std::shared_ptr; DownloadType parseDownloadType(QString javaDownload); QString downloadTypeToString(DownloadType javaDownload); MetadataPtr parseJavaMeta(const QJsonObject& libObj); } // namespace Java PrismLauncher-11.0.3/launcher/java/JavaInstall.cpp0000644000175100017510000000352515224505336021426 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "JavaInstall.h" #include "BaseVersion.h" #include "StringUtils.h" bool JavaInstall::operator<(const JavaInstall& rhs) const { auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); if (archCompare != 0) return archCompare < 0; if (id < rhs.id) { return true; } if (id > rhs.id) { return false; } return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0; } bool JavaInstall::operator==(const JavaInstall& rhs) const { return arch == rhs.arch && id == rhs.id && path == rhs.path; } bool JavaInstall::operator>(const JavaInstall& rhs) const { return (!operator<(rhs)) && (!operator==(rhs)); } bool JavaInstall::operator<(BaseVersion& a) const { try { return operator<(dynamic_cast(a)); } catch (const std::bad_cast&) { return BaseVersion::operator<(a); } } bool JavaInstall::operator>(BaseVersion& a) const { try { return operator>(dynamic_cast(a)); } catch (const std::bad_cast&) { return BaseVersion::operator>(a); } } PrismLauncher-11.0.3/launcher/java/JavaChecker.cpp0000644000175100017510000001527615224505336021372 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 "JavaChecker.h" #include #include #include #include #include "Commandline.h" #include "FileSystem.h" #include "java/JavaUtils.h" JavaChecker::JavaChecker(QString path, QString args, int minMem, int maxMem, int permGen, int id) : Task(), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen), m_id(id) {} void JavaChecker::executeTask() { QString checkerJar = JavaUtils::getJavaCheckPath(); if (checkerJar.isEmpty()) { qDebug() << "Java checker library could not be found. Please check your installation."; return; } #ifdef Q_OS_WIN checkerJar = FS::getPathNameInLocal8bit(checkerJar); #endif QStringList args; process.reset(new QProcess()); if (m_args.size()) { auto extraArgs = Commandline::splitArgs(m_args); args.append(extraArgs); } if (m_minMem != 0) { args << QString("-Xms%1m").arg(m_minMem); } if (m_maxMem != 0) { args << QString("-Xmx%1m").arg(m_maxMem); } if (m_permGen != 64 && m_permGen != 0) { args << QString("-XX:PermSize=%1m").arg(m_permGen); } args.append({ "-jar", checkerJar }); process->setArguments(args); process->setProgram(m_path); process->setProcessChannelMode(QProcess::SeparateChannels); process->setProcessEnvironment(CleanEnviroment()); qDebug() << "Running java checker:" << m_path << args.join(" "); connect(process.get(), &QProcess::finished, this, &JavaChecker::finished); connect(process.get(), &QProcess::errorOccurred, this, &JavaChecker::error); connect(process.get(), &QProcess::readyReadStandardOutput, this, &JavaChecker::stdoutReady); connect(process.get(), &QProcess::readyReadStandardError, this, &JavaChecker::stderrReady); connect(&killTimer, &QTimer::timeout, this, &JavaChecker::timeout); killTimer.setSingleShot(true); killTimer.start(15000); process->start(); } void JavaChecker::stdoutReady() { QByteArray data = process->readAllStandardOutput(); QString added = QString::fromLocal8Bit(data); added.remove('\r'); m_stdout += added; } void JavaChecker::stderrReady() { QByteArray data = process->readAllStandardError(); QString added = QString::fromLocal8Bit(data); added.remove('\r'); m_stderr += added; } void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) { killTimer.stop(); QProcessPtr _process = process; process.reset(); Result result = { m_path, m_id, }; result.errorLog = m_stderr; result.outLog = m_stdout; qDebug() << "STDOUT" << m_stdout; qWarning() << "STDERR" << m_stderr; qDebug() << "Java checker finished with status" << status << "exit code" << exitcode; if (status == QProcess::CrashExit || exitcode == 1) { result.validity = Result::Validity::Errored; emit checkFinished(result); emitSucceeded(); return; } bool success = true; QMap results; QStringList lines = m_stdout.split("\n", Qt::SkipEmptyParts); for (QString line : lines) { line = line.trimmed(); // NOTE: workaround for GH-4125, where garbage is getting printed into stdout on bedrock linux if (line.contains("/bedrock/strata")) { continue; } auto parts = line.split('=', Qt::SkipEmptyParts); if (parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) { continue; } else { results.insert(parts[0], parts[1]); } } if (!results.contains("os.arch") || !results.contains("java.version") || !results.contains("java.vendor") || !success) { result.validity = Result::Validity::ReturnedInvalidData; emit checkFinished(result); emitSucceeded(); return; } 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" || os_arch == "riscv64" || os_arch == "ppc64le" || os_arch == "ppc64"; result.validity = Result::Validity::Valid; result.is_64bit = is_64; result.mojangPlatform = is_64 ? "64" : "32"; result.realPlatform = os_arch; result.javaVersion = java_version; result.javaVendor = java_vendor; qDebug() << "Java checker succeeded."; emit checkFinished(result); emitSucceeded(); } void JavaChecker::error(QProcess::ProcessError err) { if (err == QProcess::FailedToStart) { qDebug() << "Java checker has failed to start:" << process->errorString(); qDebug() << "Process environment:"; qDebug() << process->environment(); qDebug() << "Native environment:"; qDebug() << QProcessEnvironment::systemEnvironment().toStringList(); killTimer.stop(); Result result = { m_path, m_id, }; result.errorLog = process->errorString(); result.validity = Result::Validity::Errored; emit checkFinished(result); } emitSucceeded(); } void JavaChecker::timeout() { // NO MERCY. NO ABUSE. if (process) { qDebug() << "Java checker has been killed by timeout."; process->kill(); } } PrismLauncher-11.0.3/launcher/java/JavaVersion.cpp0000644000175100017510000001017015224505336021437 0ustar runnerrunner#include "JavaVersion.h" #include "StringUtils.h" #include #include JavaVersion& JavaVersion::operator=(const QString& javaVersionString) { m_string = javaVersionString; auto getCapturedInteger = [](const QRegularExpressionMatch& match, const QString& what) -> int { auto str = match.captured(what); if (str.isEmpty()) { return 0; } return str.toInt(); }; QRegularExpression pattern; if (javaVersionString.startsWith("1.")) { static const QRegularExpression s_withOne( "1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?"); pattern = s_withOne; } else { static const QRegularExpression s_withoutOne( "(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?"); pattern = s_withoutOne; } auto match = pattern.match(m_string); m_parseable = match.hasMatch(); m_major = getCapturedInteger(match, "major"); m_minor = getCapturedInteger(match, "minor"); m_security = getCapturedInteger(match, "security"); m_prerelease = match.captured("prerelease"); return *this; } JavaVersion::JavaVersion(const QString& rhs) { operator=(rhs); } QString JavaVersion::toString() const { return m_string; } bool JavaVersion::requiresPermGen() const { return !m_parseable || m_major < 8; } bool JavaVersion::defaultsToUtf8() const { // starting from Java 18, UTF-8 is the default charset: https://openjdk.org/jeps/400 return m_parseable && m_major >= 18; } bool JavaVersion::isModular() const { return m_parseable && m_major >= 9; } bool JavaVersion::operator<(const JavaVersion& rhs) const { if (m_parseable && rhs.m_parseable) { auto major = m_major; auto rmajor = rhs.m_major; if (major < rmajor) return true; if (major > rmajor) return false; if (m_minor < rhs.m_minor) return true; if (m_minor > rhs.m_minor) return false; if (m_security < rhs.m_security) return true; if (m_security > rhs.m_security) return false; // everything else being equal, consider prerelease status bool thisPre = !m_prerelease.isEmpty(); bool rhsPre = !rhs.m_prerelease.isEmpty(); if (thisPre && !rhsPre) { // this is a prerelease and the other one isn't -> lesser return true; } else if (!thisPre && rhsPre) { // this isn't a prerelease and the other one is -> greater return false; } else if (thisPre && rhsPre) { // both are prereleases - use natural compare... return StringUtils::naturalCompare(m_prerelease, rhs.m_prerelease, Qt::CaseSensitive) < 0; } // neither is prerelease, so they are the same -> this cannot be less than rhs return false; } else return StringUtils::naturalCompare(m_string, rhs.m_string, Qt::CaseSensitive) < 0; } bool JavaVersion::operator==(const JavaVersion& rhs) const { if (m_parseable && rhs.m_parseable) { return m_major == rhs.m_major && m_minor == rhs.m_minor && m_security == rhs.m_security && m_prerelease == rhs.m_prerelease; } return m_string == rhs.m_string; } bool JavaVersion::operator>(const JavaVersion& rhs) const { return (!operator<(rhs)) && (!operator==(rhs)); } JavaVersion::JavaVersion(int major, int minor, int security, int build, QString name) : m_major(major), m_minor(minor), m_security(security), m_name(name), m_parseable(true) { QStringList versions; if (build != 0) { m_prerelease = QString::number(build); versions.push_front(m_prerelease); } if (m_security != 0) versions.push_front(QString::number(m_security)); else if (!versions.isEmpty()) versions.push_front("0"); if (m_minor != 0) versions.push_front(QString::number(m_minor)); else if (!versions.isEmpty()) versions.push_front("0"); versions.push_front(QString::number(m_major)); m_string = versions.join("."); } PrismLauncher-11.0.3/launcher/java/JavaInstallList.h0000644000175100017510000000420515224505336021723 0ustar runnerrunner/* 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 #include #include "BaseVersionList.h" #include "java/JavaChecker.h" #include "tasks/Task.h" #include "JavaInstall.h" #include "QObjectPtr.h" class JavaListLoadTask; class JavaInstallList : public BaseVersionList { Q_OBJECT enum class Status { NotDone, InProgress, Done }; public: explicit JavaInstallList(QObject* parent = 0, bool onlyManagedVersions = false); Task::Ptr getLoadTask(bool forceReload = false) override; bool isLoaded() override; const BaseVersion::Ptr at(int i) const override; int count() const override; void sortVersions() override; QVariant data(const QModelIndex& index, int role) const override; RoleList providesRoles() const override; public slots: void updateListData(QList versions) override; protected: void load(); Task::Ptr getCurrentTask(); protected: Status m_status = Status::NotDone; shared_qobject_ptr m_load_task; QList m_vlist; bool m_only_managed_versions; }; class JavaListLoadTask : public Task { Q_OBJECT public: explicit JavaListLoadTask(JavaInstallList* vlist, bool onlyManagedVersions = false); virtual ~JavaListLoadTask() = default; protected: void executeTask() override; public slots: void javaCheckerFinished(); protected: Task::Ptr m_job; JavaInstallList* m_list; JavaInstall* m_current_recommended; QList m_results; bool m_only_managed_versions; }; PrismLauncher-11.0.3/launcher/java/JavaInstallList.cpp0000644000175100017510000001474515224505336022270 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include "Application.h" #include "settings/SettingsObject.h" #include "java/JavaChecker.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" #include "tasks/ConcurrentTask.h" JavaInstallList::JavaInstallList(QObject* parent, bool onlyManagedVersions) : BaseVersionList(parent), m_only_managed_versions(onlyManagedVersions) {} Task::Ptr JavaInstallList::getLoadTask(bool forceReload) { Q_UNUSED(forceReload) load(); return getCurrentTask(); } Task::Ptr JavaInstallList::getCurrentTask() { if (m_status == Status::InProgress) { return m_load_task; } return nullptr; } void JavaInstallList::load() { if (m_status != Status::InProgress) { m_status = Status::InProgress; m_load_task.reset(new JavaListLoadTask(this, m_only_managed_versions)); m_load_task->start(); } } const BaseVersion::Ptr JavaInstallList::at(int i) const { return m_vlist.at(i); } bool JavaInstallList::isLoaded() { return m_status == JavaInstallList::Status::Done; } int JavaInstallList::count() const { return m_vlist.count(); } QVariant JavaInstallList::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() > count()) return QVariant(); auto version = std::dynamic_pointer_cast(m_vlist[index.row()]); switch (role) { case SortRole: return -index.row(); case VersionPointerRole: return QVariant::fromValue(m_vlist[index.row()]); case VersionIdRole: return version->descriptor(); case VersionRole: return version->id.toString(); case RecommendedRole: return false; case PathRole: return version->path; case CPUArchitectureRole: return version->arch; default: return QVariant(); } } BaseVersionList::RoleList JavaInstallList::providesRoles() const { return { VersionPointerRole, VersionIdRole, VersionRole, RecommendedRole, PathRole, CPUArchitectureRole }; } void JavaInstallList::updateListData(QList versions) { beginResetModel(); m_vlist = versions; sortVersions(); endResetModel(); m_status = Status::Done; m_load_task.reset(); } bool sortJavas(BaseVersion::Ptr left, BaseVersion::Ptr right) { auto rleft = std::dynamic_pointer_cast(right); auto rright = std::dynamic_pointer_cast(left); return (*rleft) > (*rright); } void JavaInstallList::sortVersions() { beginResetModel(); std::sort(m_vlist.begin(), m_vlist.end(), sortJavas); endResetModel(); } JavaListLoadTask::JavaListLoadTask(JavaInstallList* vlist, bool onlyManagedVersions) : Task(), m_only_managed_versions(onlyManagedVersions) { m_list = vlist; m_current_recommended = NULL; } void JavaListLoadTask::executeTask() { setStatus(tr("Detecting Java installations...")); JavaUtils ju; QList candidate_paths = m_only_managed_versions ? getPrismJavaBundle() : ju.FindJavaPaths(); ConcurrentTask::Ptr job(new ConcurrentTask("Java detection", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); m_job.reset(job); connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished); connect(m_job.get(), &Task::progress, this, &Task::setProgress); qDebug() << "Probing the following Java paths: "; int id = 0; for (QString candidate : candidate_paths) { auto checker = new JavaChecker(candidate, "", 0, 0, 0, id); connect(checker, &JavaChecker::checkFinished, [this](const JavaChecker::Result& result) { m_results << result; }); job->addTask(Task::Ptr(checker)); id++; } m_job->start(); } void JavaListLoadTask::javaCheckerFinished() { QList candidates; std::sort(m_results.begin(), m_results.end(), [](const JavaChecker::Result& a, const JavaChecker::Result& b) { return a.id < b.id; }); qDebug() << "Found the following valid Java installations:"; for (auto result : m_results) { if (result.validity == JavaChecker::Result::Validity::Valid) { JavaInstallPtr javaVersion(new JavaInstall()); javaVersion->id = result.javaVersion; javaVersion->arch = result.realPlatform; javaVersion->path = result.path; javaVersion->is_64bit = result.is_64bit; candidates.append(javaVersion); qDebug() << " " << javaVersion->id.toString() << javaVersion->arch << javaVersion->path; } } QList javas_bvp; for (auto java : candidates) { // qDebug() << java->id << java->arch << " at " << java->path; BaseVersion::Ptr bp_java = std::dynamic_pointer_cast(java); if (bp_java) { javas_bvp.append(java); } } m_list->updateListData(javas_bvp); emitSucceeded(); } PrismLauncher-11.0.3/launcher/java/JavaUtils.cpp0000644000175100017510000005775615224505336021137 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * 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 #include #include #include #include #include #include "Application.h" #include "BuildConfig.h" #include "FileSystem.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" #define IBUS "@im=ibus" JavaUtils::JavaUtils() {} QString stripVariableEntries(QString name, QString target, QString remove) { char delimiter = ':'; #ifdef Q_OS_WIN32 delimiter = ';'; #endif auto targetItems = target.split(delimiter); auto toRemove = remove.split(delimiter); for (QString item : toRemove) { bool removed = targetItems.removeOne(item); if (!removed) qWarning() << "Entry" << item << "could not be stripped from variable" << name; } return targetItems.join(delimiter); } QProcessEnvironment CleanEnviroment() { // prepare the process environment QProcessEnvironment rawenv = QProcessEnvironment::systemEnvironment(); QProcessEnvironment env; QStringList ignored = { "JAVA_ARGS", "CLASSPATH", "CONFIGPATH", "JAVA_HOME", "JRE_HOME", "_JAVA_OPTIONS", "JAVA_OPTIONS", "JAVA_TOOL_OPTIONS" }; QStringList stripped = { #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) "LD_LIBRARY_PATH", "LD_PRELOAD", #endif "QT_PLUGIN_PATH", "QT_FONTPATH" }; for (auto key : rawenv.keys()) { auto value = rawenv.value(key); // filter out dangerous java crap if (ignored.contains(key)) { qDebug() << "Env: ignoring" << key << value; continue; } // These are used to strip the original variables // If there is "LD_LIBRARY_PATH" and "LAUNCHER_LD_LIBRARY_PATH", we want to // remove all values in "LAUNCHER_LD_LIBRARY_PATH" from "LD_LIBRARY_PATH" if (key.startsWith("LAUNCHER_")) { qDebug() << "Env: ignoring" << key << value; continue; } if (stripped.contains(key)) { QString newValue = stripVariableEntries(key, value, rawenv.value("LAUNCHER_" + key)); qDebug() << "Env: stripped" << key << value << "to" << newValue; value = newValue; } #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) // Strip IBus // IBus is a Linux IME framework. For some reason, it breaks MC? if (key == "XMODIFIERS" && value.contains(IBUS)) { QString save = value; value.replace(IBUS, ""); qDebug() << "Env: stripped" << IBUS << "from" << save << ":" << value; } #endif // qDebug() << "Env: " << key << value; env.insert(key, value); } #ifdef Q_OS_LINUX // HACK: Workaround for QTBUG-42500 if (!env.contains("LD_LIBRARY_PATH")) { env.insert("LD_LIBRARY_PATH", ""); } #endif return env; } JavaInstallPtr JavaUtils::MakeJavaPtr(QString path, QString id, QString arch) { JavaInstallPtr javaVersion(new JavaInstall()); javaVersion->id = id; javaVersion->arch = arch; javaVersion->path = path; return javaVersion; } JavaInstallPtr JavaUtils::GetDefaultJava() { JavaInstallPtr javaVersion(new JavaInstall()); javaVersion->id = "java"; javaVersion->arch = "unknown"; #if defined(Q_OS_WIN32) javaVersion->path = "javaw"; #else javaVersion->path = "java"; #endif return javaVersion; } QStringList addJavasFromEnv(QList javas) { auto env = QProcessEnvironment::systemEnvironment().value(QStringLiteral("%1_JAVA_PATHS").arg(BuildConfig.LAUNCHER_ENVNAME)); #if defined(Q_OS_WIN32) QList javaPaths = env.replace("\\", "/").split(QLatin1String(";")); auto envPath = qEnvironmentVariable("PATH"); QList javaPathsfromPath = envPath.replace("\\", "/").split(QLatin1String(";")); for (QString string : javaPathsfromPath) { javaPaths.append(string + "/javaw.exe"); } #else QList javaPaths = env.split(QLatin1String(":")); #endif for (QString i : javaPaths) { javas.append(i); }; return javas; } #if defined(Q_OS_WIN32) QList JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix) { QList javas; QString archType = "unknown"; if (keyType == KEY_WOW64_64KEY) archType = "64"; else if (keyType == KEY_WOW64_32KEY) archType = "32"; for (HKEY baseRegistry : { HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE }) { HKEY jreKey; if (RegOpenKeyExW(baseRegistry, keyName.toStdWString().c_str(), 0, KEY_READ | keyType | KEY_ENUMERATE_SUB_KEYS, &jreKey) == ERROR_SUCCESS) { // Read the current type version from the registry. // This will be used to find any key that contains the JavaHome value. WCHAR subKeyName[255]; DWORD subKeyNameSize, numSubKeys, retCode; // Get the number of subkeys RegQueryInfoKeyW(jreKey, NULL, NULL, NULL, &numSubKeys, NULL, NULL, NULL, NULL, NULL, NULL, NULL); // Iterate until RegEnumKeyEx fails if (numSubKeys > 0) { for (DWORD i = 0; i < numSubKeys; i++) { subKeyNameSize = 255; retCode = RegEnumKeyExW(jreKey, i, subKeyName, &subKeyNameSize, NULL, NULL, NULL, NULL); QString newSubkeyName = QString::fromWCharArray(subKeyName); if (retCode == ERROR_SUCCESS) { // Now open the registry key for the version that we just got. QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix; HKEY newKey; if (RegOpenKeyExW(baseRegistry, newKeyName.toStdWString().c_str(), 0, KEY_READ | keyType, &newKey) == ERROR_SUCCESS) { // Read the JavaHome value to find where Java is installed. DWORD valueSz = 0; if (RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, NULL, &valueSz) == ERROR_SUCCESS) { WCHAR* value = new WCHAR[valueSz]; RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, (BYTE*)value, &valueSz); QString newValue = QString::fromWCharArray(value); delete[] value; // Now, we construct the version object and add it to the list. JavaInstallPtr javaVersion(new JavaInstall()); javaVersion->id = newSubkeyName; javaVersion->arch = archType; javaVersion->path = QDir(FS::PathCombine(newValue, "bin")).absoluteFilePath("javaw.exe"); javas.append(javaVersion); } RegCloseKey(newKey); } } } } RegCloseKey(jreKey); } } return javas; } QList JavaUtils::FindJavaPaths() { QList java_candidates; // Oracle QList JRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome"); QList JDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome"); QList JRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome"); QList JDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome"); // Oracle for Java 9 and newer QList NEWJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome"); QList NEWJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome"); QList NEWJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome"); QList NEWJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome"); // AdoptOpenJDK QList ADOPTOPENJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI"); QList ADOPTOPENJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI"); QList ADOPTOPENJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI"); QList ADOPTOPENJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI"); // Eclipse Foundation QList FOUNDATIONJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path", "\\hotspot\\MSI"); QList FOUNDATIONJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path", "\\hotspot\\MSI"); // Eclipse Adoptium QList ADOPTIUMJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Adoptium\\JRE", "Path", "\\hotspot\\MSI"); QList ADOPTIUMJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JRE", "Path", "\\hotspot\\MSI"); QList ADOPTIUMJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI"); QList ADOPTIUMJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI"); // IBM Semeru QList SEMERUJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI"); QList SEMERUJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI"); QList SEMERUJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI"); QList SEMERUJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI"); // Microsoft QList MICROSOFTJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI"); // Azul Zulu QList ZULU64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); QList ZULU32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); // BellSoft Liberica QList LIBERICA64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath"); QList LIBERICA32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath"); // List x64 before x86 java_candidates.append(JRE64s); java_candidates.append(NEWJRE64s); java_candidates.append(ADOPTOPENJRE64s); java_candidates.append(ADOPTIUMJRE64s); java_candidates.append(SEMERUJRE64s); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe")); java_candidates.append(JDK64s); java_candidates.append(NEWJDK64s); java_candidates.append(ADOPTOPENJDK64s); java_candidates.append(FOUNDATIONJDK64s); java_candidates.append(ADOPTIUMJDK64s); java_candidates.append(SEMERUJDK64s); java_candidates.append(MICROSOFTJDK64s); java_candidates.append(ZULU64s); java_candidates.append(LIBERICA64s); java_candidates.append(JRE32s); java_candidates.append(NEWJRE32s); java_candidates.append(ADOPTOPENJRE32s); java_candidates.append(ADOPTIUMJRE32s); java_candidates.append(SEMERUJRE32s); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe")); java_candidates.append(JDK32s); java_candidates.append(NEWJDK32s); java_candidates.append(ADOPTOPENJDK32s); java_candidates.append(FOUNDATIONJDK32s); java_candidates.append(ADOPTIUMJDK32s); java_candidates.append(SEMERUJDK32s); java_candidates.append(ZULU32s); java_candidates.append(LIBERICA32s); java_candidates.append(MakeJavaPtr(this->GetDefaultJava()->path)); QList candidates; for (JavaInstallPtr java_candidate : java_candidates) { if (!candidates.contains(java_candidate->path)) { candidates.append(java_candidate->path); } } candidates.append(getMinecraftJavaBundle()); candidates.append(getPrismJavaBundle()); candidates = addJavasFromEnv(candidates); candidates.removeDuplicates(); return candidates; } #elif defined(Q_OS_MAC) QList JavaUtils::FindJavaPaths() { QList javas; javas.append(this->GetDefaultJava()->path); javas.append("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"); javas.append("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"); javas.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java"); QDir libraryJVMDir("/Library/Java/JavaVirtualMachines/"); QStringList libraryJVMJavas = libraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& java : libraryJVMJavas) { javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/jre/bin/java"); } QDir systemLibraryJVMDir("/System/Library/Java/JavaVirtualMachines/"); QStringList systemLibraryJVMJavas = systemLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& java : systemLibraryJVMJavas) { javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } auto home = qEnvironmentVariable("HOME"); // javas downloaded by sdkman QString sdkmanDir = qEnvironmentVariable("SDKMAN_DIR", FS::PathCombine(home, ".sdkman")); QDir sdkmanJavaDir(FS::PathCombine(sdkmanDir, "candidates/java")); QStringList sdkmanJavas = sdkmanJavaDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& java : sdkmanJavas) { javas.append(sdkmanJavaDir.absolutePath() + "/" + java + "/bin/java"); } // javas downloaded by asdf QString asdfDataDir = qEnvironmentVariable("ASDF_DATA_DIR", FS::PathCombine(home, ".asdf")); QDir asdfJavaDir(FS::PathCombine(asdfDataDir, "installs/java")); QStringList asdfJavas = asdfJavaDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& java : asdfJavas) { javas.append(asdfJavaDir.absolutePath() + "/" + java + "/bin/java"); } // java in user library folder (like from intellij downloads) QDir userLibraryJVMDir(FS::PathCombine(home, "Library/Java/JavaVirtualMachines/")); QStringList userLibraryJVMJavas = userLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& java : userLibraryJVMJavas) { javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } javas.append(getMinecraftJavaBundle()); javas.append(getPrismJavaBundle()); javas = addJavasFromEnv(javas); javas.removeDuplicates(); return javas; } #elif defined(Q_OS_LINUX) || defined(Q_OS_OPENBSD) || defined(Q_OS_FREEBSD) QList JavaUtils::FindJavaPaths() { QList javas; javas.append(this->GetDefaultJava()->path); auto scanJavaDir = [&javas]( const QString& dirPath, const std::function& filter = [](const QFileInfo&) { return true; }) { QDir dir(dirPath); if (!dir.exists()) return; auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); for (auto& entry : entries) { if (!filter(entry)) continue; QString prefix; prefix = entry.canonicalFilePath(); javas.append(FS::PathCombine(prefix, "jre/bin/java")); javas.append(FS::PathCombine(prefix, "bin/java")); } }; // java installed in a snap is installed in the standard directory, but underneath $SNAP auto snap = qEnvironmentVariable("SNAP"); auto scanJavaDirs = [scanJavaDir, snap](const QString& dirPath) { scanJavaDir(dirPath); if (!snap.isNull()) { scanJavaDir(snap + dirPath); } }; #if defined(Q_OS_LINUX) // oracle RPMs scanJavaDirs("/usr/java"); // general locations used by distro packaging scanJavaDirs("/usr/lib/jvm"); scanJavaDirs("/usr/lib64/jvm"); scanJavaDirs("/usr/lib32/jvm"); // Gentoo's locations for openjdk and openjdk-bin respectively auto gentooFilter = [](const QFileInfo& info) { QString fileName = info.fileName(); return fileName.startsWith("openjdk-") || fileName.startsWith("openj9-"); }; // AOSC OS's locations for openjdk auto aoscFilter = [](const QFileInfo& info) { QString fileName = info.fileName(); return fileName == "java" || fileName.startsWith("java-"); }; scanJavaDir("/usr/lib64", gentooFilter); scanJavaDir("/usr/lib", gentooFilter); scanJavaDir("/opt", gentooFilter); scanJavaDir("/usr/lib", aoscFilter); // javas stored in Prism Launcher's folder scanJavaDirs("java"); // manually installed JDKs in /opt scanJavaDirs("/opt/jdk"); scanJavaDirs("/opt/jdks"); scanJavaDirs("/opt/ibm"); // IBM Semeru Certified Edition // flatpak scanJavaDirs("/app/jdk"); #elif defined(Q_OS_OPENBSD) || defined(Q_OS_FREEBSD) // ports install to /usr/local on OpenBSD & FreeBSD scanJavaDirs("/usr/local"); #endif auto home = qEnvironmentVariable("HOME"); // javas downloaded by IntelliJ scanJavaDirs(FS::PathCombine(home, ".jdks")); // javas downloaded by sdkman QString sdkmanDir = qEnvironmentVariable("SDKMAN_DIR", FS::PathCombine(home, ".sdkman")); scanJavaDirs(FS::PathCombine(sdkmanDir, "candidates/java")); // javas downloaded by asdf QString asdfDataDir = qEnvironmentVariable("ASDF_DATA_DIR", FS::PathCombine(home, ".asdf")); scanJavaDirs(FS::PathCombine(asdfDataDir, "installs/java")); // javas downloaded by gradle (toolchains) QString gradleUserHome = qEnvironmentVariable("GRADLE_USER_HOME", FS::PathCombine(home, ".gradle")); scanJavaDirs(FS::PathCombine(gradleUserHome, "jdks")); javas.append(getMinecraftJavaBundle()); javas.append(getPrismJavaBundle()); javas = addJavasFromEnv(javas); javas.removeDuplicates(); return javas; } #else QList JavaUtils::FindJavaPaths() { qDebug() << "Unknown operating system build - defaulting to \"java\""; QList javas; javas.append(this->GetDefaultJava()->path); javas.append(getMinecraftJavaBundle()); javas.append(getPrismJavaBundle()); javas.removeDuplicates(); return addJavasFromEnv(javas); } #endif QString JavaUtils::getJavaCheckPath() { return APPLICATION->getJarPath("JavaCheck.jar"); } QStringList getMinecraftJavaBundle() { QStringList processpaths; #if defined(Q_OS_MACOS) processpaths << FS::PathCombine(QDir::homePath(), FS::PathCombine("Library", "Application Support", "minecraft", "runtime")); #elif defined(Q_OS_WIN32) auto appDataPath = QProcessEnvironment::systemEnvironment().value("APPDATA", ""); processpaths << FS::PathCombine(QFileInfo(appDataPath).absoluteFilePath(), ".minecraft", "runtime"); // add the microsoft store version of the launcher to the search. the current path is: // C:\Users\USERNAME\AppData\Local\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime auto localAppDataPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", ""); auto minecraftMSStorePath = FS::PathCombine(QFileInfo(localAppDataPath).absoluteFilePath(), "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe"); processpaths << FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime"); #else processpaths << FS::PathCombine(QDir::homePath(), ".minecraft", "runtime"); #endif QStringList javas; while (!processpaths.isEmpty()) { auto dirPath = processpaths.takeFirst(); QDir dir(dirPath); if (!dir.exists()) continue; auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); auto binFound = false; for (auto& entry : entries) { if (entry.baseName() == "bin") { javas.append(FS::PathCombine(entry.canonicalFilePath(), JavaUtils::javaExecutable)); binFound = true; break; } } if (!binFound) { for (auto& entry : entries) { processpaths << entry.canonicalFilePath(); } } } return javas; } #if defined(Q_OS_WIN32) const QString JavaUtils::javaExecutable = "javaw.exe"; #else const QString JavaUtils::javaExecutable = "java"; #endif QStringList getPrismJavaBundle() { QList javas; auto scanDir = [&javas](QString prefix) { javas.append(FS::PathCombine(prefix, "jre", "bin", JavaUtils::javaExecutable)); javas.append(FS::PathCombine(prefix, "bin", JavaUtils::javaExecutable)); javas.append(FS::PathCombine(prefix, JavaUtils::javaExecutable)); }; auto scanJavaDir = [scanDir](const QString& dirPath) { QDir dir(dirPath); if (!dir.exists()) return; auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); for (auto& entry : entries) { scanDir(entry.canonicalFilePath()); } }; scanJavaDir(APPLICATION->javaPath()); return javas; } PrismLauncher-11.0.3/launcher/java/JavaInstall.h0000644000175100017510000000306715224505336021074 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "BaseVersion.h" #include "JavaVersion.h" struct JavaInstall : public BaseVersion { JavaInstall() {} JavaInstall(QString id, QString arch, QString path) : id(id), arch(arch), path(path) {} virtual QString descriptor() const override { return id.toString(); } virtual QString name() const override { return id.toString(); } virtual QString typeString() const override { return arch; } virtual bool operator<(BaseVersion& a) const override; virtual bool operator>(BaseVersion& a) const override; bool operator<(const JavaInstall& rhs) const; bool operator==(const JavaInstall& rhs) const; bool operator>(const JavaInstall& rhs) const; JavaVersion id; QString arch; QString path; bool is_64bit = false; }; using JavaInstallPtr = std::shared_ptr; PrismLauncher-11.0.3/launcher/java/download/0000755000175100017510000000000015224505336020314 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/java/download/ArchiveDownloadTask.h0000644000175100017510000000254615224505336024370 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include "tasks/Task.h" namespace Java { class ArchiveDownloadTask : public Task { Q_OBJECT public: ArchiveDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); virtual ~ArchiveDownloadTask() = default; bool canAbort() const override { return true; } void executeTask() override; virtual bool abort() override; private slots: void extractJava(QString input); protected: QUrl m_url; QString m_final_path; QString m_checksum_type; QString m_checksum_hash; Task::Ptr m_task; }; } // namespace Java PrismLauncher-11.0.3/launcher/java/download/SymlinkTask.h0000644000175100017510000000203715224505336022740 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "tasks/Task.h" namespace Java { class SymlinkTask : public Task { Q_OBJECT public: SymlinkTask(QString final_path); virtual ~SymlinkTask() = default; void executeTask() override; protected: QString m_path; Task::Ptr m_task; }; } // namespace Java PrismLauncher-11.0.3/launcher/java/download/ManifestDownloadTask.h0000644000175100017510000000256615224505336024557 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include "tasks/Task.h" namespace Java { class ManifestDownloadTask : public Task { Q_OBJECT public: ManifestDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); virtual ~ManifestDownloadTask() = default; bool canAbort() const override { return true; } void executeTask() override; virtual bool abort() override; private slots: void downloadJava(const QJsonDocument& doc); protected: QUrl m_url; QString m_final_path; QString m_checksum_type; QString m_checksum_hash; Task::Ptr m_task; }; } // namespace Java PrismLauncher-11.0.3/launcher/java/download/ArchiveDownloadTask.cpp0000644000175100017510000001103715224505336024716 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "java/download/ArchiveDownloadTask.h" #include #include "Application.h" #include "archive/ArchiveReader.h" #include "archive/ExtractZipTask.h" #include "net/ChecksumValidator.h" #include "net/NetJob.h" #include "tasks/Task.h" namespace Java { ArchiveDownloadTask::ArchiveDownloadTask(QUrl url, QString final_path, QString checksumType, QString checksumHash) : m_url(url), m_final_path(final_path), m_checksum_type(checksumType), m_checksum_hash(checksumHash) {} void ArchiveDownloadTask::executeTask() { // JRE found ! download the zip setStatus(tr("Downloading Java")); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("java", m_url.fileName()); auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); auto action = Net::Download::makeCached(m_url, entry); if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) { auto hashType = QCryptographicHash::Algorithm::Sha1; if (m_checksum_type == "sha256") { hashType = QCryptographicHash::Algorithm::Sha256; } action->addValidator(new Net::ChecksumValidator(hashType, QByteArray::fromHex(m_checksum_hash.toUtf8()))); } download->addNetAction(action); auto fullPath = entry->getFullPath(); connect(download.get(), &Task::failed, this, &ArchiveDownloadTask::emitFailed); connect(download.get(), &Task::progress, this, &ArchiveDownloadTask::setProgress); connect(download.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); connect(download.get(), &Task::status, this, &ArchiveDownloadTask::setStatus); connect(download.get(), &Task::details, this, &ArchiveDownloadTask::setDetails); connect(download.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); connect(download.get(), &Task::succeeded, [this, fullPath] { // This should do all of the extracting and creating folders extractJava(fullPath); }); m_task = download; m_task->start(); } void ArchiveDownloadTask::extractJava(QString input) { setStatus(tr("Extracting Java")); MMCZip::ArchiveReader zip(input); if (!zip.collectFiles()) { emitFailed(tr("Unable to open supplied zip file.")); return; } auto files = zip.getFiles(); if (files.isEmpty()) { emitFailed(tr("No files were found in the supplied zip file.")); return; } auto firstFolderParts = files[0].split('/', Qt::SkipEmptyParts); m_task = makeShared(input, m_final_path, firstFolderParts.value(0)); auto progressStep = std::make_shared(); connect(m_task.get(), &Task::finished, this, [this, progressStep] { progressStep->state = TaskStepState::Succeeded; stepProgress(*progressStep); }); connect(m_task.get(), &Task::succeeded, this, &ArchiveDownloadTask::emitSucceeded); connect(m_task.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); connect(m_task.get(), &Task::failed, this, [this, progressStep](QString reason) { progressStep->state = TaskStepState::Failed; stepProgress(*progressStep); emitFailed(reason); }); connect(m_task.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); connect(m_task.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { progressStep->update(current, total); stepProgress(*progressStep); }); connect(m_task.get(), &Task::status, this, [this, progressStep](QString status) { progressStep->status = status; stepProgress(*progressStep); }); m_task->start(); return; } bool ArchiveDownloadTask::abort() { auto aborted = canAbort(); if (m_task) aborted = m_task->abort(); return aborted; }; } // namespace Java PrismLauncher-11.0.3/launcher/java/download/ManifestDownloadTask.cpp0000644000175100017510000001273315224505336025107 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "java/download/ManifestDownloadTask.h" #include "Application.h" #include "FileSystem.h" #include "Json.h" #include "net/ChecksumValidator.h" #include "net/NetJob.h" struct File { QString path; QString url; QByteArray hash; bool isExec; }; namespace Java { ManifestDownloadTask::ManifestDownloadTask(QUrl url, QString final_path, QString checksumType, QString checksumHash) : m_url(url), m_final_path(final_path), m_checksum_type(checksumType), m_checksum_hash(checksumHash) {} void ManifestDownloadTask::executeTask() { setStatus(tr("Downloading Java")); auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); auto [action, files] = Net::Download::makeByteArray(m_url); if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) { auto hashType = QCryptographicHash::Algorithm::Sha1; if (m_checksum_type == "sha256") { hashType = QCryptographicHash::Algorithm::Sha256; } action->addValidator(new Net::ChecksumValidator(hashType, QByteArray::fromHex(m_checksum_hash.toUtf8()))); } download->addNetAction(action); connect(download.get(), &Task::failed, this, &ManifestDownloadTask::emitFailed); connect(download.get(), &Task::progress, this, &ManifestDownloadTask::setProgress); connect(download.get(), &Task::stepProgress, this, &ManifestDownloadTask::propagateStepProgress); connect(download.get(), &Task::status, this, &ManifestDownloadTask::setStatus); connect(download.get(), &Task::details, this, &ManifestDownloadTask::setDetails); connect(download.get(), &Task::succeeded, [files, this] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*files, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *files; emitFailed(parse_error.errorString()); return; } downloadJava(doc); }); m_task = download; m_task->start(); } void ManifestDownloadTask::downloadJava(const QJsonDocument& doc) { // valid json doc, begin making jre spot FS::ensureFolderPathExists(m_final_path); std::vector toDownload; auto list = doc.object()["files"].toObject(); for (const auto& paths : list.keys()) { auto file = FS::PathCombine(m_final_path, paths); const QJsonObject& meta = list[paths].toObject(); auto type = meta["type"].toString(); if (type == "directory") { FS::ensureFolderPathExists(file); } else if (type == "link") { // this is *nix only ! auto path = meta["target"].toString(); if (!path.isEmpty()) { QFile::link(path, file); } } else if (type == "file") { // TODO download compressed version if it exists ? auto raw = meta["downloads"].toObject()["raw"].toObject(); auto isExec = meta["executable"].toBool(); auto url = raw["url"].toString(); if (!url.isEmpty() && QUrl(url).isValid()) { auto f = File{ file, url, QByteArray::fromHex(raw["sha1"].toString().toLatin1()), isExec }; toDownload.push_back(f); } } } auto elementDownload = makeShared("JRE::FileDownload", APPLICATION->network()); for (const auto& file : toDownload) { auto dl = Net::Download::makeFile(file.url, file.path); if (!file.hash.isEmpty()) { dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, file.hash)); } if (file.isExec) { connect(dl.get(), &Net::Download::succeeded, [file] { QFile(file.path).setPermissions(QFile(file.path).permissions() | QFileDevice::Permissions(0x1111)); }); } elementDownload->addNetAction(dl); } connect(elementDownload.get(), &Task::failed, this, &ManifestDownloadTask::emitFailed); connect(elementDownload.get(), &Task::progress, this, &ManifestDownloadTask::setProgress); connect(elementDownload.get(), &Task::stepProgress, this, &ManifestDownloadTask::propagateStepProgress); connect(elementDownload.get(), &Task::status, this, &ManifestDownloadTask::setStatus); connect(elementDownload.get(), &Task::details, this, &ManifestDownloadTask::setDetails); connect(elementDownload.get(), &Task::succeeded, this, &ManifestDownloadTask::emitSucceeded); m_task = elementDownload; m_task->start(); } bool ManifestDownloadTask::abort() { auto aborted = canAbort(); if (m_task) aborted = m_task->abort(); emitAborted(); return aborted; }; } // namespace Java PrismLauncher-11.0.3/launcher/java/download/SymlinkTask.cpp0000644000175100017510000000536615224505336023303 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "java/download/SymlinkTask.h" #include #include "FileSystem.h" namespace Java { SymlinkTask::SymlinkTask(QString final_path) : m_path(final_path) {} QString findBinPath(QString root, QString pattern) { auto path = FS::PathCombine(root, pattern); if (QFileInfo::exists(path)) { return path; } auto entries = QDir(root).entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); for (auto& entry : entries) { path = FS::PathCombine(entry.absoluteFilePath(), pattern); if (QFileInfo::exists(path)) { return path; } } return {}; } void SymlinkTask::executeTask() { setStatus(tr("Checking for Java binary path")); const auto binPath = FS::PathCombine("bin", "java"); const auto wantedPath = FS::PathCombine(m_path, binPath); if (QFileInfo::exists(wantedPath)) { emitSucceeded(); return; } setStatus(tr("Searching for Java binary path")); const auto contentsPartialPath = FS::PathCombine("Contents", "Home", binPath); const auto relativePathToBin = findBinPath(m_path, contentsPartialPath); if (relativePathToBin.isEmpty()) { emitFailed(tr("Failed to find Java binary path")); return; } const auto folderToLink = relativePathToBin.chopped(binPath.length()); setStatus(tr("Collecting folders to symlink")); auto entries = QDir(folderToLink).entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries); QList files; setProgress(0, entries.length()); for (auto& entry : entries) { files.append({ entry.absoluteFilePath(), FS::PathCombine(m_path, entry.fileName()) }); } setStatus(tr("Symlinking Java binary path")); FS::create_link folderLink(files); connect(&folderLink, &FS::create_link::fileLinked, [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); if (!folderLink()) { emitFailed(folderLink.getOSError().message().c_str()); } else { emitSucceeded(); } } } // namespace Java PrismLauncher-11.0.3/launcher/java/JavaChecker.h0000644000175100017510000000242615224505336021030 0ustar runnerrunner#pragma once #include #include #include "JavaVersion.h" #include "QObjectPtr.h" #include "tasks/Task.h" class JavaChecker : public Task { Q_OBJECT public: using QProcessPtr = shared_qobject_ptr; using Ptr = shared_qobject_ptr; struct Result { QString path; int id; QString mojangPlatform; QString realPlatform; JavaVersion javaVersion; QString javaVendor; QString outLog; QString errorLog; bool is_64bit = false; enum class Validity { Errored, ReturnedInvalidData, Valid } validity = Validity::Errored; }; explicit JavaChecker(QString path, QString args, int minMem = 0, int maxMem = 0, int permGen = 0, int id = 0); signals: void checkFinished(const Result& result); protected: virtual void executeTask() override; private: QProcessPtr process; QTimer killTimer; QString m_stdout; QString m_stderr; QString m_path; QString m_args; int m_minMem = 0; int m_maxMem = 0; int m_permGen = 64; int m_id = 0; private slots: void timeout(); void finished(int exitcode, QProcess::ExitStatus); void error(QProcess::ProcessError); void stdoutReady(); void stderrReady(); }; PrismLauncher-11.0.3/launcher/java/JavaMetadata.cpp0000644000175100017510000000716115224505336021540 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "java/JavaMetadata.h" #include #include "Json.h" #include "StringUtils.h" #include "java/JavaVersion.h" #include "minecraft/ParseUtils.h" namespace Java { DownloadType parseDownloadType(QString javaDownload) { if (javaDownload == "manifest") return DownloadType::Manifest; else if (javaDownload == "archive") return DownloadType::Archive; else return DownloadType::Unknown; } QString downloadTypeToString(DownloadType javaDownload) { switch (javaDownload) { case DownloadType::Manifest: return "manifest"; case DownloadType::Archive: return "archive"; case DownloadType::Unknown: break; } return "unknown"; } MetadataPtr parseJavaMeta(const QJsonObject& in) { auto meta = std::make_shared(); meta->m_name = in["name"].toString(""); meta->vendor = in["vendor"].toString(""); meta->url = in["url"].toString(""); meta->releaseTime = timeFromS3Time(in["releaseTime"].toString("")); meta->downloadType = parseDownloadType(in["downloadType"].toString("")); meta->packageType = in["packageType"].toString(""); meta->runtimeOS = in["runtimeOS"].toString("unknown"); if (in.contains("checksum")) { auto obj = Json::requireObject(in, "checksum"); meta->checksumHash = obj["hash"].toString(""); meta->checksumType = obj["type"].toString(""); } if (in.contains("version")) { auto obj = Json::requireObject(in, "version"); auto name = obj["name"].toString(""); auto major = obj["major"].toInteger(); auto minor = obj["minor"].toInteger(); auto security = obj["security"].toInteger(); auto build = obj["build"].toInteger(); meta->version = JavaVersion(major, minor, security, build, name); } return meta; } bool Metadata::operator<(const Metadata& rhs) const { auto id = version; if (id < rhs.version) { return true; } if (id > rhs.version) { return false; } auto date = releaseTime; if (date < rhs.releaseTime) { return true; } if (date > rhs.releaseTime) { return false; } return StringUtils::naturalCompare(m_name, rhs.m_name, Qt::CaseInsensitive) < 0; } bool Metadata::operator==(const Metadata& rhs) const { return version == rhs.version && m_name == rhs.m_name; } bool Metadata::operator>(const Metadata& rhs) const { return (!operator<(rhs)) && (!operator==(rhs)); } bool Metadata::operator<(BaseVersion& a) const { try { return operator<(dynamic_cast(a)); } catch (const std::bad_cast&) { return BaseVersion::operator<(a); } } bool Metadata::operator>(BaseVersion& a) const { try { return operator>(dynamic_cast(a)); } catch (const std::bad_cast&) { return BaseVersion::operator>(a); } } } // namespace Java PrismLauncher-11.0.3/launcher/java/JavaUtils.h0000644000175100017510000000260415224505336020562 0ustar runnerrunner/* 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 #include #include "java/JavaInstall.h" #ifdef Q_OS_WIN #include #endif QString stripVariableEntries(QString name, QString target, QString remove); QProcessEnvironment CleanEnviroment(); QStringList getMinecraftJavaBundle(); QStringList getPrismJavaBundle(); class JavaUtils : public QObject { Q_OBJECT public: JavaUtils(); JavaInstallPtr MakeJavaPtr(QString path, QString id = "unknown", QString arch = "unknown"); QList FindJavaPaths(); JavaInstallPtr GetDefaultJava(); #ifdef Q_OS_WIN QList FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix = ""); #endif static QString getJavaCheckPath(); static const QString javaExecutable; }; PrismLauncher-11.0.3/launcher/java/JavaVersion.h0000644000175100017510000000217315224505336021110 0ustar runnerrunner#pragma once #include // NOTE: apparently the GNU C library pollutes the global namespace with these... undef them. #ifdef major #undef major #endif #ifdef minor #undef minor #endif class JavaVersion { friend class JavaVersionTest; public: JavaVersion() {} JavaVersion(const QString& rhs); JavaVersion(int major, int minor, int security, int build = 0, QString name = ""); JavaVersion& operator=(const QString& rhs); bool operator<(const JavaVersion& rhs) const; bool operator==(const JavaVersion& rhs) const; bool operator>(const JavaVersion& rhs) const; bool requiresPermGen() const; bool defaultsToUtf8() const; bool isModular() const; QString toString() const; int major() const { return m_major; } int minor() const { return m_minor; } int security() const { return m_security; } QString build() const { return m_prerelease; } QString name() const { return m_name; } private: QString m_string; int m_major = 0; int m_minor = 0; int m_security = 0; QString m_name = ""; bool m_parseable = false; QString m_prerelease; }; PrismLauncher-11.0.3/launcher/resources/0000755000175100017510000000000015224505336017576 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/sources/0000755000175100017510000000000015224505336021261 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/sources/burfcat_hat.png0000644000175100017510000002336115224505336024256 0ustar runnerrunnerPNG  IHDRR1q\&IDATx1}A.| g=̮~=95PNc[ͱm̷۞Y۶mvMVJ>"/;>^}QZ&#/< M (MAw?@e:(/:"d%##x=BNDDf ݊G0ws\[r01*B ߑSN٥d٪q18VN jƈȀF_Q^pOݑ}cB`f?3U9'Bjؘ/G[CODDjDuA7Ǩ !'T'=2h[13BEh%艈Ȃ@5断_⦆Ȗڿ܉H+I}vml Ro艈HPɳ1%7S;Brp\ݰp[dޝZ,/xj-D1 1w;DW!iZЩ4V8HySĥL?Pk N;>o)Gh""""j %h GrW#>v^ ,FwGlI!TeE%B+ 6SqZȞ1V#,EDDD5REOhɏxLk^3o(a1؄Tu:STsoɠؙAzGh,jƈh2 TlqӷSZm Sh b=[~[ #T,]U=:r!np(ьML|@JT F0<%(t T#>zN yCaOCpYhdK8Ӫw=jfȌh.=lȟY9@w^ޡV!?>hzvґ -Eyrlj™B ""Gȅio4辛w]M}so^tHpTXz͐-m:AQjuM >& P<ڣ$OA]UUpzǺum jBR_N2!tSCt*핊x Szꀈȅjc]իTywJݲQh.3Qg8mwz?&MDD&\TE\m|Kj,i!B?p̍pM!LjjTSmrVv>IjqC%rC@]%h29@ k[W;BHKqb2*CFErBيKY)z]1""n2֌W$V/4UE\1[P,nՖ ڵ_΅݋֣d@+V-]]<3vܧnJ&$F\ )&-h-h.=ZrKV#4c`zR>kjFcEe!V!Ȉ< E_wJo>|v Fui(p IBX,>Nf@E_ƄY-H*=GInHPf"'tǁzy^'!!Owt,"""2(Ujnv#65BQ m_1U{VD[j|]#h ]:( A#$όlk#za"]zY_ZVD?{wFEaKaf03:fcfffeffffff '˼GqMR,Ec=;kЌU*m߹!pavj$۱SBa(hi9!T2t |K?}K!RȎهB B%jSruu$>;-{s"k`.gB8!$Ev`~maChD;H#BHD]FDmG۵ !D()>AwyWHjN쮩k7C BA;\As"kB D!|@Г6zNd 8R^TD[k!Cf c!Նif$ @mol >m ʩ+xYKLS}a mvcXY/3{ @)xظ4 !ʨ.LRNpT~K UMFv%D; IgcXxY7Kg?1od[?r 2{ZQnm Ͷ(t"u_/ѨHz0T[KM'@'N[ =T<ؼFV@8oisV(|T 4j$i_UP8l DT-t7^3!QOjMzEN@42S6ϫidc,LxUAPQ6##=UΏu:NA5) zHHSs{ 1}aq,bq@C>1ՅAw#νi'.( zCHэwHsρt'ޗč@ikAasױ8ϭG/vVĉ@򅐞hi:Ytl4)3tbh_B荦֝C7 A|xm} zݗzCY BiHAlG hd$ !Lo=p; "mxKodFVHi,>,&S 3{/ *nТCYk7{*,2{zLqS}.)Ța<@BV?Wͪ|: @FV!V=R}D=3xYM!g7F @%~Vn7?eG'!X`4 {u[< >PezBڭFhY;'R7v6hY@=U ;yA4dPSqE}q7{ӽl6~x@:B\߿5-o?c+FZ:*?`?=)#lLL~:}a(?m*׵ F֍4@f,@~EqЇ?tZpT Sݹ:f}uQFhd>B eUgbDFsgwǯKt_at]|'Lu[O׎kdR)lfo'~S]뺂H];u2F8(bzqqqjQTȺ*?L|5Ft]#"sjlyIQ *+Us}+id1 4}3r# o\_,GM8-׵t;gvk4@ !->гM*-4tN;byO5-wT\L!4d@Bȼ@ԔjNܹ*ڕQOy>}wFBǩ8wX #'_iJN`2 ~V9=C9h.KYnt|OR4kKndBH WѸ:pfhaﹳz4T?hO{D7FV TO)MŭͦVnBVE/ez  "t"혠rÏzILkT}Q43Nw\0&yB(i@%~Z+3 c(B o#k@:@zqB(Fփ JTmmذb=!Bh׺bĄ,cB]FfW7z!Y/2k@!D#MHhNg |'kWm ao1LթB=E]A^QiS|=n[66hp1J1IU,[m1e8ff;̜ 33ǹ<~1^fffffl49ʅ>Uڙ8:=Kw`ѦBO0G/v~=1&D|:Č:&("w-kY%\\&ǀ`0X0;-B {BLy߷} \yŊq@\"DAnsL p=Y^|BiX`=EHi^t݄bSٳbE_u/dBj=-q>f`Ϩø&>j `D$`"!sg):fk7wִ!v5+!nbGj2KL'3I QnHkEBNY?L,11 1"Ho6+}+; ę׋:IbFlX+nƥ> LQֽtjkDH  $F5[sqrl`0n]PcU}oHh/ ՃDl|cEpZ~-=)j1iC:O"z:tsޮ~bh%@'EXSDlzOn`03cvK~zD?R@ v-B@`OPo 78"AAŚMbP0=2I Kmlc"CmA5 5`0nO:nOM`dV9io]}^]J";&̤[H27RZQtMyK5/vw )D_0j`IuK,5眿G`0nU] dWvgsxqH8yniZO7D NYaQ+ aNDM(dmY@X[ z~MeQXúb{M6`0tk3Tc~-Ep}@;{yCJzwJ!"=D4q P^>b^beH`{!mn.*Pc}]#υl XND7S ]m+@X>તqXi=my(Wn`0wIpٶu >`I L _mB/{q_am1z~|V n`qE0cj"wF78eB`DJ-K{M*\%ք,;l=DO`0ɴi2I|ק˰v bb0 E5< M.}˽/v`01:U "Yu5m}H͡*f$@gk3۲%%]4fV/iwt`&Y; w'n&`Hś褱8{jdM%Qi +1G " F/ %lF"c4S=Ec3G=a<6+a%xNt61 QhCLsZ}eNn(kyF%u f2ttW{ 53 wgOGc!l03ychnv1$bkb=`4 oi\{aF &g0φ g?0 ȳb`@&j[ͣa$)tG=эhՔcb0 F0F CtҘ[2@x*`Zw LiRx> 7TFw= Lc+^F9~&>b鶁`0ޮovUf[@o0@wg.܉[PL)բkcKNji!SA%2@hd(@dt@u*1tK`0 >Yn_K:uOᨏ+=q6gWe1`QBuSj-u )rdF`e4yTGo`&J>~vkOw/BPAlp Bĺ~eo)nM[/f߃9B?1Uɜ*vW}lI5_6E=ތ˪#%&Ւ2o D ȧk}$pka`0 d ;'q}m*L P=-FP `PfbfWD>&I!(];ެQ'zO>#M!:>Ԁ[n@`C`0  z7F6Y.mD=/1!?"X!"z:R4lzf@H V/I"3ZM{#K9T +)D`0w;8zΓpFgJM9lҲY iDfcٳ:oyL׊ Y_ X~MDVv"W->ax)Y;ߤcG/0 #3 EqX'TF>f$"#6D#pk`uCà tQOH7p[4 `G8x^ uAn!mU uG5ҮgVQ !ыv `X?$ Ec0`0 b_ ]Zatu=o5%"ʟG=' @aDT$Vt4nPR8!DUX.X40 #xi, ]D/2T6=-obP5h@3J˫õ8ׁ@΄P0 q  E_JΒ<3H10 t6t"C=DYxwycLg\"~OݽF ZF 8I`0IҠ`$D*O-zP:[ŜrRcH}a`ea_һL.} image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/enderpearl.svg0000644000175100017510000002353015224505336024126 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/creeper.svg0000644000175100017510000006350415224505336023437 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/ftb-logo.svg0000644000175100017510000002342015224505336023514 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/flame.svg0000644000175100017510000000630215224505336023067 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/netherstar.svg0000644000175100017510000002635215224505336024171 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/skeleton.svg0000644000175100017510000003601215224505336023630 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/cattiversary.xcf0000644000175100017510000035551315224505336024517 0ustar runnerrunnergimp xcf v011uBBgimp-image-grid(style solid) (fgcolor (color-rgba 0 0 0 1)) (bgcolor (color-rgba 1 1 1 1)) (xspacing 16) (yspacing 16) (spacing-unit inches) (xoffset 0) (yoffset 0) (offset-unit inches) gimp-image-metadata 8 8 8 629 417 1 2 300/1 300/1 1 1 LNuburfcat_hat.png!? "     %$#\uMMNNuK[k{$833>3N3^3n3~=7JJJJJJJKKK/K?KOK_KoKKKKKKKKKLLL/L?LOL_LoLLLLLLLLLMMM/M?MOM_MoMMMMMMMSc(  Si& }h& g&' h* -SS0,(/  C+;Ym?6')GW * a(;vC9 D8'$ eA9E~A$uS2  Do?- M?Cx9* MZBo(T۟)B%,. B%#e B\$  hb#~;$kQD-T\-1WH,5 Q,Bo4 7V,7V8V\N4Vd;;3V0;G3V;0 2V0;81Vd;I1V=Y0V=;^0V; /Vd00dǽ.V-Vw,VV,V+V*V)VZ)V(V 'V &V dw&V &V p&\'p((0'H@`5'( ('(5$Hl'<(d'<( 8'D(($=H(N8 =M&`|8 [L&eIK&":'FL*Wah-8:==0eh|,N=1h+%8=<.8q) <@$* 6 @)1=@2$*@@<`<5$1#$`/@<-8z.` 2@;* 8yAhh@/(=t8;@5 &" 8v@; %JK8@&$pc VG,@;*#\l j^$N;-r88-$@88D,z(=QG9,_0|8GG.4I*G$79?GE 7G&8G9"'4G!%3G m3Gm)2G m'1G!1G7n0G#!n1GE#!m!/G9  74.G)-G!,G,G<+G.*G%)G)GB( G(G5' G<G)& G@G!& G@1G& G&G&@G@G'8G(( G'1$.8AGB.EG'8@ (:G'8G@ . '0:DG'8GG@('8GC( 5G'8GC(( 0G(S< +==&f< =&kN1=&$>5=8'K/=:*]!=#-<>=20l#==*,6@ TA==#+OyÔUI< =')'`u .* 'P[M\M5$̿WMg^X$q g\U-*g\M*)6nnY)46( V>=<;Z;:98w7V7 @>>==G0<G%;G;GB:G69DG*8G!7G7 @ >!>T =bB <b3 ;b";b[ :bK 9^b: 8b. 7b!7 `>0=<<;H: 988`74654 4'('(Rp''˗Rd'$((Ph(4'@(('$0'' t'P''''''''''~'s's's's's'sd00d 's;'s=;;='a=='Vd;;l'V0;;4'V;; 'V0;;0'Vd;;d'V=='V=;;=t'V;t'Vd00d 'V'V'V'V'V'V'V'V'V'V'V'V'V'V'V'V'V'VPP'V'V'VX**\'V* 'Vy'Vy'VDB'V '8GC(+EG'2G'(MDG'1GD(G'1G'MG'1GE (((G'1G,8GG$((G'1G 2G':G'1GG0-G'.G'1GG1D G* G'1G/G'+G6G')G')G')G')G')G')G')G')G'#G' G' G' G' G' G' G9  7 G' GE&!m!#EG' G&!nn!#G'GG9nn9G'GG!!G'GG mmG'GGmmG'GG mm G'GG!!G'GG7nn7G'G#!nn! G'GE#!m! EG'G9  7 G'G'G'G'G'G'G'G'G'G'G'G'G'G'G'G%GG'GCCG'G--G'GG'GCC'G--'G(('GD'GE>,^'G/^'G?0' G&'Nb]N=L# ;`b'Eb6#K=?;+^b'Db^ N=G=N8b'Db6W=MN;M==N&b'Db` #N#'#M=Nb'Db=Nbb2#N#b'Db Fb6Qb'DbbC >b6@b'DbbD^ b:,b'DbAb';bJb'8b'8b'8b'8b'8b'8b'8b'8b'0b',b',b',b',b',b',bO&&L b',b`5[0`b',b5^^0b'%bbO^^Ob'!bb&)b'!bb[[b'!bb[[b'!bb[[b'!bb&&b'!bbL^^Lb'!b0^^,b'!b`0[,`b'!bO&&L b'!b'!b'!b'!b'!b'!b'!b'!b'!b'!b'!b'!b'!b'!b'!b3bb'!b]]b'!b>>b'!bb'!b]]'!b>>'!b! O #'!b^ D=H'!b`U=+==F'!bAH=+'!bW*ME=E'! b5Q=$$d$`$`$`$`$`$`$`$`$H$@$@$@$@$@$@$@$@$,$ $ $ $ $ $ $ $ $%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%654 Z4 3 2 1 w0 V0 /.-Z-,+*|)V) ( ' 0p& Z& --% 0o$`\-=`ß#|-ة-|"<V"P-/x! 3? D  0 \ :[-  D\@ 3 @$ TX Z #$%w&V&'()Z)*+,w-V-KQd00d'q2;@ HK=;;=KK2q==dXhGd;;lKs^; 0;;4 vRn;; 2%u0;;0ap1nd;;d`qܨ J4*|b ==(6&  ]#  =;;=O[=ZOc^DBH ;r+$ w%*`% NB2d00dǟ:Q[Rqu)|xo6G>6G15G%4 G4 GC3 G82 G+1 G!0 G0 G?/G2.G&-G-GD,G9+G,*G")G%G) G;G@( G$6G2' G G&& G<G& G$..6GD% G G9$G+.66,#GE"../GG""G5?G"GB./!EG@! G*054G3 G$ G' GA G G*3W.< GD G;$ G:GB.GGE% G.G->G8 ? G$G$EGC' GG8 G8 G@#G3$G'%G!&G&G;'G.(G$)G)GA*G4+G),G!-G-G<"G9  7G./@GE&!m!#EG5# G&!nn!#G< /G9nn9G#@@<+:6G!!G@ @>@0='< G mm G# /6@>"@.GmmGAB@7;@1G mm GCDFGF)(&/<@@.DG!!G6$/4=.,745),> G7nn7F<  3$   / G#!nn!/GC2!&#0? GE#!m! DC,-> >@ =( 9&G9  7,'G= &"=01@@>= 943/bV 6bD 5b3 4 b"4 b] 3 bM 2 b; 1 b. 0 b!0 bW /bE .b5 -b"-b] ,bN +b= *b0 )b3b!) bR bX ( b2JbE ' b+b5 & bT b"& b2+Y+Jb^ % bY=Y*bP $b;$#+K=L3$J= #b`/+YYL=LYY+Abb0 "bI Y=YWb!"b[+L=L,.`bY ! b:.L=L4GbG bY=Y2 b6  bYZ==C==Y b# b;2[YIY[+S b_  b#R2 bP b[?bb`3 b? b>UbM W b2 b 2`b]!6 b"bN bM bY #bG $b6 %b. &b!&bR 'b? (b2 )b")bZ *bH +b8 ,b. -b!-bT enbO&&Lb@ 5Db`5[0`bI0b)fb5^^0bTffDbO^^Ob&q̹ `b&)bXfP#,b[[ b0ob[[bY[ D̲3b[[b]^ab`98sC̕^b&&bK$+1@HT@=VF9=U bL^^LaSGm&/& A b0^^Ab]"+^Sjc\M*W b`0[+ ^\=~&$ 39Ã2jYD=Q5bO&&L=6bT\|o%8IH3 21 1 d0 4/ . -- L,$+ **l)8('&&P%$$ ##p"0>' $*( `*( :( dy( ( y:y( pB*LL*( ( $(ʯ p`(,$o(g&$(# 2 [dx#8e KK[dx&&< @M{22d #'A K =UWHv" ;$xxocn 62%0 (ez; #! ETd*fW([1i!@auH;Yzi/ @z VR88zy # 7=ux-*n$!*4^3hs#lY" `e ?[*-NHZ"2e[l*|x %%xu Sn:I +>sT1&*mnzB-2Ow.MV2 22;MV'D G8 ((8 GC((8 G$+(8 G^(8 GE(8 G5^+^(8 G0(;((8 G'G'(8 G6 9G(80/ GCG(0 DGG('G> DG(BG,'G#"&!GDBG5##9*  ?G$"&!G<&9<##"A@@?G# 6##.@= #?%33/*:.;@ %@>*/ ->@!#*@ +@$&5,!>=(@1 <%3>,@@?; ;3 $@" @3:?3@?  51@?=2=@.$'+@@??@0-@%";(*@@=8>;:&? %"*&-4@?::@?2@7%3 1".;7?@+0#  @,.@@.3@@-@@>!@2. $@@; $' ^ bN F=(N b]$ D=(N b2B=(N b&+=(N b`H=B==(N bIO=F++F(N b+*I R (N b 6b6(N bJ Ob(MCA b+$]b(B ^b *b('6bV ^b([b=6b#D dl.b^ [bI0#K!ee!Wb2dl.bT&3Q00/ZDDWb&#5W!eS00 !& 0W$ GD&%A6%nP.!^r&5u6|C!VƄ #bPxȏ˾? W+uoLL̥ˤ0 J SŢ<9$,9FE̜ 0x"ƴƽ V|9+ E G,'G<@@/@ /' 6 GDBG 63 9@2"@@?>&=@- 0 G$"&!G@@&(>?(' @8@ G0/GA@@?G#$ $'9@ 61 9 5 DG5#.@@#B.@  %*. GC'G< 7;(@9 =@ @@:G,BG 66566-@8@@5@@G$"&!A&@@( 2@> :@@ "GA@@#*@. 8&> >4 ":@@*#.@@21)2 @,  %!@ "!?" 7@-1@?  133)66(;@@/:@@; :@@.@@=@ .7.4 ?1$@@2@ @5$,@@3 @@@@7@>@@ $ 3@>;!#>( (33#(5@#)2@'- @!.@@2(5+!@@*@ $*9@233@ 8.@@:@@; $=F Nb^ PM 7E˾=D $]b2dl 0<]SB2bCAbZDD 29_#% =+&b` ^bI0h̔Ɍ\ =H` b=6bT;S̃ 8C( =NJ b^ [b&#̑o|F I*+ b2dl.bX3P9eS{˂ h̵@_  bCAbZDDWb0 u&u~ h] O H ^bI0(W0[T\ C 3Y8X:DD;iu332$23O3|#3~\. *+*d$CW}C(w 'j)1ya#XЅH<$ 0l| H $X\ (( 0l|)H)z$X\*06H:l|;<X\0>H @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @:h4N'ucatbgrnd2.png!? "     %$#O%uOa'3?uQQQQQQRRR!R1Z!\!\1\A\Q\aj{}1idϲL|t{,6GPPPP[[oKqvqqq~#3&%5EF @@@@u&:79LM[tI6Y`V+5R^[45QktY3*\3*nۇ3A2oq1Qx//;s]-,[ lF ,Kp ]"+ 9P~ e=+#7T zQ* (0] k)*^ ׄ(3| ((V'=!>z&"C% +[%CC74$#PeJC9T#DvaCLs" u&:79LM[tI6Y`V+5R^[45QktY3*\3*nۇ3A2oq1Qx//;s]-,[ lF ,Kp ]"+ 9P~ e=+#7T zQ* (0] k)*^ ׄ(3| ((V'=!>z&"C% +[%CC74$#PeJC9T#DvaCLs" u&:79LM[tI6Y`V+5R^[45QktY3*\3*nۇ3A2oq1Qx//;s]-,[ lF ,Kp ]"+ 9P~ e=+#7T zQ* (0] k)*^ ׄ(3| ((V'=!>z&"C% +[%CC74$#PeJC9T#DvaCLs"  8 -FII26 Yv-4uֱi3U3-1i0\!/K <-> ^,/ H*& ~ * J)܊( o'U&:&% i$L$.#t"^"A!! ~ 03`  ,>P`o!>>>;>O>k>o =p=q=r==,=I <Y+ ;vW+":g29sO<.7V+(6 @>>>>;>O>k>o =p=q=r==,=I <Y+ ;vW+":g29sO<.7V+(6 @>>>>;>O>k>o =p=q=r==,=I <Y+ ;vW+":g29sO<.7V+(6 >B>z =9=g <x.<z\;z;+;V;:8:9`8ÿb7ڶ6խ 5ɬ4$3LJ 2@@@@)9q_- 4(&9baW3(1)4APnjI>71-/@1 !B8@32CL]jʹ~pbB ;U]FEE?C:6Gaմsa< @ezLCCEWIDReڴ`C,aFC1C``WGFVk πjG.a'9*DalqhVIIV`l "/FlvaK:(#)6Fp3^յd8')DX|徃XFC73KՍ[IC6*4] h V8*+=T 7 x@+*/-` hG+&+^ \`K/!Ib3vpWCC71-/@1 !B8@32CL]jʹ~pbB ;U]FEE?C:6Gaմsa< @ezLCCEWIDReڴ`C,aFC1C``WGFVk πjG.a'9*DalqhVIIV`l "/FlvaK:(#)6Fp3^յd8')DX|徃XFC73KՍ[IC6*4] h V8*+=T 7 x@+*/-` hG+&+^ \`K/!Ib3vpWCCG&KR3U&,V0T&q6:&XV<'4O'd'2(( e(S).*j*<**n+S++V,=,,-{ |-: p:)9q_- 4(&9baW3(1)4APnjI>71-/@1 !B8@32CL]jʹ~pbB ;U]FEE?C:6Gaմsa< @ezLCCEWIDReڴ`C,aFC1C``WGFVk πjG.a'9*DalqhVIIV`l "/FlvaK:(#)6Fp3^յd8')DX|徃XFC73KՍ[IC6*4] h V8*+=T 7 x@+*/-` hG+&+^ \`K/!Ib3vpWCCFOKB@?:3+*/?n CSNE?95$!"$*JpH4>W-4('0jM9Cb-m`'%&*G>CD,3yh&%'-pz8CCa,3D|P/+>p=6RlųzUkчA3,Lw{jgbpc<1(AxИqZRkz]FVʋL71/M")$42'3փj`bpžţS6337xFCCA5,)7GxkgvǽfJCDNm Y:8Pι [! @(I) +`ijK$-GmJ&8g`SJE''/51&–I@K ɯxh[I/ |T:448OKG.ʹhA)Jq 5%*b=1024248>RV)-b̀GCljX>KPZl NCCE[nX11PkwH3=Ț,7C<7?Na G77O^CA610>^ sL8-*&!0>FOKB@?:3+*/?n CSNE?95$!"$*k~d`LCCTjJ,%$*10D~skgdWg[BImto`N˻Ϸn xHm`o.SN=46mοv.F^e=73D̤w/Del=;3@p.G[fH:g|cl/`aרhVad׻g]\it\L72GxɣlG51&–I0|8 ɯxh[I/ |T:448OK5}j"ʹhA)Jq (vhb=1024248>RV"J`54ljX>KPZl NCCE[nP%%7O^CA610>^ sL8-*&!0>FOKB@?:3+*/?n CSNE?95$!"$*eW o:hVLeX.G>m0)*V].;JP0+(6j/9PU0.(2|].?HàQ9.QbUl/]RwRG[d׻gYOT\I<--GxɣlG:+#?tbSJEDGOXjkegehvZ8+"'/Fn7'NoQ4$=[/JȿyE5;S.|ANuL7?W-ǔ6*)2pQ;Cb-se)'(,KʘACD,6ǿm(')/uȀ;CCa,6HT2.Av@8RlųzUkчD6.P}pmguh?4*BxИqZRkz]FVʋL71/O#+&65)4փj`bpžţS6337xFCCA5-*9IykgvǽfJCDNm Y:8Pι [ @(I!#`ijK"5blR80g`SJE''/51&–I0|8 ɯxh[I/ |T:448OK5}j"ʹhA)Jq (vhb=1024248>RV"J`54ljX>KPZl NCCE[nP%%7O^CA610>^ sL8-*&! $c'7k- ?ez03Y347=P>>>>>> u154 4= 4@3L*,"3W"2=C=2a3C@2~DCA(2JA/1T4*)012*8JB#1K@`RC+1Z`OC#1n`L<#1eO4*0Z6*0 J**/ 9*/ v5" - n6(, eA2+ aE@+]D5*ȃVB*pJ1)[<(gL!(z[8!'N8&'lCC&E:)&J1'%O1* %V3' %b6# %s@%N%h% O*$ ߮A$ﶋ o #|dw \"yPI{ S!mODKD !aKCCX~' ^LDCCN` '#5R~9 EbEېR"ҥyR3!#*x?0*!  *Ȳ=(&'' Ǹ~uvv  /@CCFeG, +Ru sL+zxR6 Z:T%6S2!a-(A'$*3@S_m}V% u154 4= 4@3L*,"3W"2=C=2a3C@2~DCA(2JA/1T4*)012*8JB#1K@`RC+1Z`OC#1n`L<#1eO4*0Z6*0 J**/ 9*/ v5" - n6(, eA2+ aE@+]D5*ȃVB*pJ1)[<(gL!(z[8!'N8&'lCC&E:)&J1'%O1* %V3' %b6# %s@%N%h% O*$ ߮A$ﶋ o #|dw \"yPI{ S!mODKD !aKCCX~' ^LDCCN` '#5R~9 EbEېR"ҥyR3!#*x?0*!  *Ȳ=(&'' Ǹ~uvv  /@CCFeG, +Ru sL+zxR6 Z:T%6S2!a-(A'$*3@S_m}V% u154 4= 4@3L*,"3W"2=C=2a3C@2~DCA(2JA/1T4*)012*8JB#1K@`RC+1Z`OC#1n`L<#1eO4*0Z6*0 J**/ 9*/ v5" - n6(, eA2+ aE@+]D5*ȃVB*pJ1)[<(gL!(z[8!'N8&'lCC&E:)&J1'%O1* %V3' %b6# %s@%N%h% O*$ ߮A$ﶋ o #|dw \"yPI{ S!mODKD !aKCCX~' ^LDCCN` '#5R~9 EbEېR"ҥyR3!#*x?0*!  *Ȳ=(&'' Ǹ~uvv  /@CCFeG, +Ru sL+zxR6 Z:T%6S2!a-(A'$*3@S_m}V% T1ۭ1 ;1 ǃ0 꼫0 ^/ Ҽ / ׳8/ z. ʤ. ڼU. ¼ - صC- ӗ, L, ڡ+[*Ҩ!*o)̯')̼x (ȼ/(x 'ݿ2'ֺ| &̨;%%ׯN$$͵`##κr"Ť"ұ"("R"ԗ!ջ !n!Գ& Δbۯ-ؕ zQޫeo ῗgʼv7 zR׳|?mrR!֧<"͕4#ɕ4#ʥZ$6%͟~%֭H&K $ 8/7>;>:3!2 1ȰlB. ܕ+p(G&Y3$bB/#fjFA9 H`jph[gx~ nLCIZ= Fj`VICLlpjIT z]EG__4IlICGղxFXߣvj`H>ɃRCk^Ej xW* ^SsGRsK``pKPa3"b`qSFڀksGeVObOlxg`jKNnRFk߶llZͱdPL*&VCDjnKPlD^`>|hGCDZvkSC^SJu\RqI7@sa`=Au`a_53 @S^-3x`kaB 'y,/KACe`jZ> X<*,?Dْg``gG+#>20FvRKLJL`Z  _PFOgaE,!9K[b  En``]K-*)@T" 'e~`V@:7,*)/?L`x IaF0**#6CT`ʱ$ Kek=*/7=CJLGDCDX $VR5L}jGJ|v 3jjg|Rje Lb`zn`o! @DNjZaz}' =qOGZWCIR[( 6fo]N`[L!)s܍eNCC; $ 8/7>;>:3!2 1ȰlB. ܕ+p(G&Y3$bB/#fjFA9 H`jph[gx~ nLCIZ= Fj`VIBIfjfHT z]EG__4IlIA@jlDXߣvj`H>ɃQ;SSDj xW* VBa\ARsKTH`TAPa3"VHU>=ڀksGeLS?^`>|hC96HdkS6GaCJu\RqI7@sa`=1XkHa_53 @S^-&tZHkaB 'y,/KA6LNjZ> X<*,??nMJYgG+#>20F}jY>8;>:3!2 1ȰlB. ܕ+p(G&Y3$bB/#fjFA9 H`jph[gx~ nLCIZ= Fj`VIBIfjfHT z]EG__4IlIA@jlDXߣvj`H>ɃQ;SSDj xW* VBa\ARsKTH`TAPa3"VHU>=ڀksGeLS?^`>|hC96HdkS6GaCJu\RqI7@sa`=1XkHa_53 @S^-&tZHkaB 'y,/KA6LNjZ> X<*,??nMJYgG+#>20F}jY>8 |.U `.YC.ū65.Ϳ 0.пv,-ο@@@@ _   4 . !'-/+# , %LslL1 )6a[&&*dO $J ~'! -z a4A9 ZtR8{SOͬE vX76A=WK tl+g5L~YPf>F?!`+(Kגn3WQ&l4P`2p2UD F2SV26"Z*F3|e!`+(Kגn,AF&l4P`%T%UD F2SV%fs6"Z*F3|e!`+(Kגn,AF&l4P`%T%UD F2SV%fs6"Z*[WJ<*4h5+?@+>6BPd65Z7TF=88@<852,9 )*;$;;;#D;!S;5<<;;:m:::9s99999999999888ӱ8봒8d~ng`Z8$! -<8 :8]857 7 )!N6*,50  - zW@,' bI9/% ":&$Ѧv]I:/(#D94ӚkTIA6* )C- hC.A^DCK.dpG9Fp/%yub`hv0.de94Nn0,EOGA4@z0 %CCNn1 /Gax16lgF2'XEK3g[WJ<*4h5+?@+>6BPd65Z7TF=88@<852,9 )*;$;;;#D;!S;5<<;;:m:::9s99999999999888ӱ8봒8d~ng`Z8$! -<8 :8]857 7 )!N6*,50  - zW@,' bI9/% ":&$Ѧv]I:/(#D94ӚkTIA6* )C- hC.A^DCK.dpG9Fp/%yub`hv0.de94Nn0,EOGA4@z0 %CCNn1 /Gax16lgF2'XEK3g[WJ<*4h5+?@+>6BPd65Z7TF=88@<852,9 )*;$;;;#D;!S;5<<;;:m:::9s99999999999888ӱ8봒8d~ng`Z8$! -<8 :8]857 7 )!N6*,50  - zW@,' bI9/% ":&$Ѧv]I:/(#D94ӚkTIA6* )C'(i(<( )] ), )6 ' $J 'pɼ ߹~C?b ΚJ jf1 "-, tԫ+ +gⰘ)!uoRI\'ʨ^9*t%#~ %gǀ %% Iov#&/Vi|-4ezR1*o`;屟)Ox8R׃)l 鶚))8 pޏ3p3-3t2v2l2(ʼn2L2h2x2|^2O2>1,1"11 1'1C1A1~P1t뺼2e2T2G27 1.o5 -,ΨV))ʲ1&* Ƭz9"Qư{(˵lÜ<ƨg 1Z[J)!!)2P҉Z<31>C2+3FbA2%xS4*/8GdyhbT!#*-AK^ՀSp@/+9^Ӌ]CE^L-&L]8,3TRA59CER"[DEX4*7^"sXFSx%E9Gg޵zE06k,IEVzʘX9Z,Gb՝S3*(C,p]!*+29S-6  4J2ҁ: K3qG)$E4bECGh6KCCR863Fa9*6b;*@;*P<$j<"<>=?>>m>n>>[J)!!)2P҉Z<31>C2+3FbA2%xS4*/8GdyhbT!#*-AK^ՀSp@/+9^Ӌ]CE^L-&L]8,3TRA59CER"[DEX4*7^"sXFSx%E9Gg޵zE06k,IEVzʘX9Z,Gb՝S3*(C,p]!*+29S-6  4J2ҁ: K3qG)$E4bECGh6KCCR863Fa9*6b;*@;*P<$j<"<>=?>>m>n>>[J)!!)2P҉Z<31>C2+3FbA2%xS4*/8GdyhbT!#*-AK^ՀSp@/+9^Ӌ]CE^L-&L]8,3TRA59CER"[DEX4*7^"sXFSx%E9Gg޵zE06k,IEVzʘX9Z,Gb՝S3*(C,p]!*+29S-6  4J2ҁ: K3qG)$E4bECGh6KCCR863Fa9*6b;*@;*P<$j<"<>=?>>m>n>>! !(9CCEWk3%( 9/**'&&%,>EWF*#2& pL7%?h-  a1  '$xF*1g%p< ! (W&O)! 'Bh&1*+:) "Ql'O2**+ /S([C/:[+^A(  'Nu.{N+! Dh0O/  6b2I* R2~6*&(W4Z+*$!*@33&*,z38#**+J3R8*2:EWF*#2& pL7%?h-  a1  '$xF*1g%p< ! (W&O)! 'Bh&1*+:) "Ql'O2**+ /S([C/:[+^A(  'Nu.{N+! Dh0O/  6b2I* R2~6*&(W4Z+*$!*@33&*,z38#**+J3R8*2:??EWF*#2& pL7%?h-  a1  '$xF*1g%p< ! (W&O)! 'Bh&1*+:) "Ql'O2**+ /S([C/:[+^A(  'Nu.{N+! Dh0O/  6b2I* R2~6*&(W4Z+*$!*@33&*,z38#**+J3R8*2:*t M*A g,t.$;/َ%x2R28b4辡57ټ9;= A$JX`I70Oeh``NC2 3kE7))*4L`Q /qT?-**3CX`]F K"aG@ENX`g>$|lZN= %|ND;"  ĻX=B6! pNK:APZsT0/5 ^76Ra``auV70/  Z2?T`l ]CH6!ԏTFg kFOR3 |LJ^  ]DVR k=CP`)-a^sM; ?$ [!2 V?= o! ߔ^ qE!ےY"!p@ !֟V+!D"Ҩ}(#j#Ͷs4$O $ǯ&$ߖ^:"b&""dB" 2hpD<"KrKC#!'Y~bC8!&^vL@ &a΋WB/ +dџZ= +x`^zS ӣ^,;|sIdS ՚X4+Sncbg vK4@G]aRQRD [7(!1FF=53A΀N*"!B:0./C?aE1":1$%0BUu@9%#?D/GJT>#A?5.6O_q9$ <4.3D_hڞH)%36.7Oakh % >/.<]\P'+E0.@]5?F;9LP %3(CFL]C "#$9FZ\ #1-,7GcJ "$|lZN= %|ND;"  ĻX=B6! pNK:APZsT0/5 ^76Ra``auV70/  Z2?T`l ]CH6!ԏTFg kFOR3 |LJ^  ]DVR k=CP`)-a^sM; ?$ [!2 V?= o! ߔ^ qE!ےY"!p@ !֟V+!D"Ҩ}(#j#Ͷs4$O $ǯ&$/ߖ^:"/b&"/GdB"/6TpD<"/DjKC#!/KubC8!/KyvL@ /K|΋WB/ /N~џZ= /Nzyp ӣ^,/\h~p/ ՚X4/Op}|/ vK4/=`fx{onoc/ [7(!/See^VUa//΀N*"!/@b[RQRc_/aE1"/=[SIIRaq/u@9%#/D_dR@Bfh/T>#/Aa_VQWlz/q9$/E]VQUdz/ڞH)%/VWQXl{/ %/6^RQ]xwm/'/OdRQ`x?/ )/D`e\[jm/ '+*!$/ULcejxc +./"/HHB[evx/ .-,/#/TPOYf}i ./"A]RQci|/ ./ /OaQUeld .//RZPXda/ -. //WVIY[e/ $JX`D+$;M[``NC2 3kE7))*4L`Q /qT?-**3CX`]F K"aG@ENX`g>$|lZN= %|ND;"  ĻX=B6! pNK:APZsT0/5 ^76Ra``auV70/  Z2?T`l ]CH6!ԏTFg kFOR3 |LJ^  ]DVR k=CP`)-a^sM; ?$ [!2 V?= o! ߔ^ qE!ےY"!p@ !֟V+!D"Ҩ}(#j#Ͷs4$O $ǯ&$rߖ^:"rb&"rdB"rvpD<"rKC#!rbC8!rμvL@ rм΋WB/ rлџZ= r̥ ӣ^,rȰr ՚X4rr vK4r{r [7(!rrr΀N*"!r}raE1"r{ru@9%#r~rT>#r~rq9$rrڞH)%rr %rvr'r}r  )rr _ihP<*$r ipr"rr polr#r qr"~r qr r qrrr oq rrr -ʼ;-˙~tQ,}gF- ,p:  ,i) +[)B(('b&E&2%'$M#"!_ ϓ  N ֯K翎 JԟIF؏E  C!ޭ!["ȁ">#"ΜD  `ă (~J=̞ PڳQ Y`̞T`ީ^ŸeYcŝ bcZ VprA[ v+F Xb 7j}Ǜ*j j$ȟ4ŢX 7D˫v!S—<"Bd Ô# ,$,\ J" #V v"Z ׍ } } ' ō4m A:9++8R`_F8rcc^>8ecjl9xw:;<t=?A/:/9=OO;/8o{ze?//8}}y_//8~}/9:;</=/?Ar:r9{zr8}rr8rr8r9:;<r=r?A:*mz2918טP878 92:T;3<=?@@@@9G7!0z&v6'r8' R4k*I'5pJ*^:~'@$^V5[(&Ke' 0Y),XNe*0b ,3k87s8@9?*r.z&v+V5' R)PgE'5p;lG+w'@w G`A(M(&KU $G),Xc;Lp*0b ,3k87s8l2" KE%M_0f" I5C&#W# x1 0i$ AX>Hb qK71"@| کf+ #WSⵒs^R=)   8a 3=BCA;#   3Gb 0(9j2 /s3 D4 *1O5!%6[8%@9?*r.z&v+V5' R)PgE'5p;lG+w'@w G`A(M(&KU $G),Xc;Lp*0b ,3k87s8l2" KE%M_0f" I5C&#W# x1 0i$ AX>Hb qK71"@| کf+ #WSⵒs^R=)   8a 3=BCA;#   3Gb 0(9j2 /s3 D4 *1O5!%6[8%@o788&99:F;R;U;R;!R;$P;K:?:(8 28õf11~180[0E/^.D̰- ,̵  b ; b ^ógUA5P~ 2յ^9 3d.s\@+   >|52?4a67o85:m;I<(g>  ףuVC1*% Xʍ^="  +qݟ`7 <a1 `#[*  ' I%S%   *3J)O  ,d.ҟmP:/*2A_=O<]:'y9 ?7X5l42 0 g/-',N* (=&e% #)! _ "9 I! !I" E# @y$>n% 7^&)K ( !6^ + (Ep/ 1Rx5 4X ףuVC1*% Xʍ^="  +qݟ`7 <a1 `#[*  ' I%S%   *3J)O  ,d.ҟmP:/*2A_=O<]:'y9 ?7X5l42 0 g/-',N* (=&e% #)! _ "9 I! !I" E# @y$>n% 7^&)K ( !6^ + (Ep/ 1Rx5 4X ףuVC1*% Xʍ^="  +qݟ`7 <a1 `#[*  ' I%S%   *3J)O  ,d.ҟmP:/*2A_=O<]:'y9 ?7X5l42 0 g/-',N* (=&e% #)! _ "9 I! !I" E# @y$>n% 7^&)K ( !6^ + (Ep/ 1Rx5 4Xǟd* B'ƭR'+ɸ/15  ><<853/xܨ1?T.$gخ+6Ǧ*O޼) i' 4% S$ |"5!oG!])9 Qs~ !"&|$%z &w a<%  )AD<+!H, +Ba]J- %$.^<)%$ .7QccM+%-)-F5+*77/$*..EgOD:51/10/>Nb{xhP@?EIIB@Qy]M=2/7>AII?96227@QYn}}kN?@IPM?PeVN@1/>?MYYRI@??@MQQ]lzxaI??@A?JmVLD?8/=@QZkhUOIJVZVQQ\fhe[E8:?g>?=3/?vM@3//7?DOG<3/8?KZ{uZBISq}vXQQ[lr^C?N@>0/4?FQMC5//7??@OQ_{hM?AK\`SQ^eeR?39Q@?7/3=EPQJ919??EQdlaF?8?NdkgR@ON;/4`QQJ@3/1?:3//4<?KQQMMVe`I?8Gap}eGA@7/3dQPI<2//2?7/37<?BPQJ?IYeZD47Qh}}X@>0// jN>@IX`a[M=23?;1/7?HQQC?I[dP=/=[r}uI?/ uL9/8Cb~~`QE?>3/3;MQN@?La[G53Fd{}aA1// M?5/3?Ib~zh\I?<<?6/9QQI?DVdQ>08MqzM7// U:3/3;EVhu}{udE1/5=??7/@RVC?N_[H70>YkA0/ ^@31//3:FSaoyvQ9//0>??7/3VoR?EVdR?07Fo~{R6/ kC??JVUN?4//28CL[`M?3/3??FJ9/Fz_G?IZYI72?Vz}oA/ H>?G_ujVD4//17>@@?=17??IVQ8/7acP@BQQPA1=Eh}{X5 [70?EfVZ3/;@FPZgu}~zaM8/7?KXe[C1/2IXQE?Nk}lJ88QqI/8?7?BFJS]kcQI;0/3>?AQbeR8/:;;>?@@??=2/380>DPY[WM?1/13683/3>IYeN8/FJOaeRB<2/AEB@??7/6LnZ3/3EP;//7lI/;?3 /0/1Igf9 /780//3XA4>MC>=<<=<72/9V`?/1I?PQQV]enl_O</8@2/pE?MR_eel}}u[E+/fG?FJLMOPLI@5,/eG?<?:4./u[D>8?DGA:83/3994/cI?89??AMUX[SI@2/8?DC?7/U@?2:?@Vcee^SI;/0??@@?>7323;?:/2/37?@?<==72/l4/7?@[~z^M;0/023578:<=??FQD9/~7/2%/3?</P832/07<4'/7?:/uD?;./040/hI?07/zS?<2/30./_??:/9?0./j?B?/8?8/273)/rHPJ2//8??1//3DB8)/ݥ}}{xusrt}}|{~𸘈yvsrsr|}}~ߤ{trx|~}yvttw~}~}Ǘ}sr|}}~}~}}xr{}xy}|}{trz}yu}vsy}}~}xry}}|}~urrw}{trx}텏}}|sru}vrrx}􈉓}~}ty}}wrt{ysy}}한}x{~rv~}}srsz~}~st}rv}}xrtrrsy}~}{|}~zruÓ~urs}ytrruz}ꅉ}x~}xruЕztrrt}xruxz}}ux~|srr ߚ|}{tu}zsrw}}{r{}r yrx}|urtz}}vt~srr }vrt}}{{}vry}|sxxrr yurtzsrv{}}wr}}ws|~sr Β~ututsrrtyyrrs|}}xru}}swvr }}}urrtx}uru}}yr}xt}~r |}urrsx|}{sx}}xrw}~s{v ˏwsz´{srrsw}{x}}vrs}yx}~}rrtvrtz}ury}|}яurz}xrx}srt}xx̟rx}x}zsru|}~xr{~}r~νΛ~rsut{}x}|yzz|}~~}}{trux{}truvu沑|s|}srsuvxz}|tru|xr{yr~΢{v}zrtvsrsz}sruȕ}rv}z}~~}srs{vrzǚur娇}xz}tru}zru~}zry̍}ru}ztrx}trx}}}tu{xz}}vrt}xrz}{}ussvtsvy|ztrz~rsxyur{~sr}r{zrtvxy|~~}}xrvuruzrrxrz}t rsrsy rwxsrruՎ~u||{zwtry}rs}{rx}tr}+rԗ}~v,r}{}yu.r|x}~yxuruyyur敄}xy}}~}trx}}wr׋}}ty}~zrs{}s r}yrz}ysrs{u r}trz}}zsrsy~x r}vrrzuuz{{}}urswuruxyy}}xs ryrtrrsx}ysruxwtrx~}ysrz}urؓtrv|}}~~}|xutuz}yrz~vrszt rԎru{}~~}}{trxztrv{ rԍr}}ysrs{zurtyyt r܎srz}xrsu{{xurtru~}u rsx}yxvut rury}|trtx}~}{wtrurx}}zsrstuvxxyz{}}yrwr{xrsw}~ysr{rzu"rusruuŕsry|t%ru}{rxutrsxzu'rw}yr}z.rsusrĘ}s7r}ztrus.r}}yry}s.rΚ}}rx}xrtxt)rtrrx}}srrux)r 1.#5;D.-$ +.$/A$.C7 :%.%)1.Y]; 4"-%&'Aca1/%&# 1JWH# =)$2+.7FF3F3.7"<.=F4% 0 GDD6#5".6=CF0,&.;B.  GFB.&#*CFF<..!.0CD1 GF7./1:9.+., 55/.6FI3 GC/./!.&0A" ?^G0.8QR2 /F9.'...IQIMT/Qux\@8 /VG@PIJA/EKa}{S=//3/0//7ERA/ /fddWHW/GQW^ceRO?</=?BJQ\bQ4/ /febQJH/@Mcee]QQA?1//8FQRcdS;/ /7feYQR@/?BS[[QNQOE<1/0CVVRQWehU9/ /DfcRQR@/?FQ@JRaG?CQQNNQUe[?0/3Msx]QQ`xjG//H[AKQQ;//1:?NQ^eQ7////BI4IQQ6//:EQPUeaI2/3E[edRLScdO4//G;1IQQ://0=IQQM@I]e[=//9L^e^KERcZE3//I33NQQ=/54//3?MQI?04EZ~Q37>@@4//AJ[P/3?JàY9/3PSl|g/4Ip`//3/7@`qbK?E\e[M??EpsL3/ENXRD./~P2/;^]O33DeuO2/;O>@Q^N?//5;@<3/./u}[9/5M[N96Adc7/3~Q28DRJ</0/./j}fB3/1BVM??@`s?/2lP//3<>4/0./UyoF6/:KL??@[xE63VK1 /6<./MnqI5/3CI?8HF5/383/;>0..-InyK4/4<4/1IurL;9@r@?2/2/87 /0Fp9  /140'/?n- */3Ge( */>Ic| !/1/>Gal /3</8D_[/1>8/0;VG /;=1 /<4/30 /5!,/7n/14753 /<?/3:AGB??< /I /0/2=LQJ?<2 /n /?=3/ ]/23/30 /./M E'/./5d6'/./Bs,'/.D~'/.D'/.;'/.'/./3688940 /059@@91/.!un rw|ys rry~{t r|r}x r~~r{rr{}r}trr~r}{srwsrrr|ursrrw~r rr}zr{}ur rr}~}srrxzr rx~r}zsrsyr r~r}}zrrvwr r}r}酉~urr~urr}ru|}}srurr~zrrsy}xrrz|rr~uwrry툋trtvrrzszrrs{}{rrytrrtu{rvurrt}}{urt~~yrrwvrr}ryyrvzryxrxsry}}~tru|~|rrrwstrrsrt}{st|suuw|}}urr~ruzrrrz}{~zt}Ο{rsvytrs~ryrrrx~}|}־yrururrurx}}}}Ӡurz{rwtr}rv}}w}ֺ{rvr{wrurs{}twӚrru~rruyr}rs}~~tssx򓥧rtvwsrt}ur{~}򍟧ywu{vuxrz}wru}󓧤ur{xrr}ru{{rs}~urwtsrrrsyrr}}x|qrtrzuutrz|}}rrvz}{uqqryrvyv~xruȉtx{rsrqrurs}}rtՈrru{|ursqrvry}􏲣vuͅs rvzpqqprrvrt}x{}|Âvruxurz|qo9uru{urszy~~}trtz}}zrz}wrqqX~urtr{srv}xtrrx}trx}}wqztr{uru}rsrz~vrtvz{km xtrrt}xrztrvwrv~zS# usrrt}yrst rsy~zxsru# s{|rxw rs9  rsus'r}- *ru( *r|| !rsr|l ruzrx[rs|xrszG rz{s rz4rts rv!,rxnrsuwvt r{?ruy~}}{ r rsrt{}zt r r}{ur{}x rqru rx}{rty~}xt rqr| ]rtrus rqr E'rqrv6'rqr,'rq'rq}'rqu'rqO&rqrtvxxyus rsvy~~ysrqH!ʘNҕD |$/T _^= ߅#i78o3Mz-_ +'r @z Vҁ Ӄ ԃ҅ Ї ΎˏƓ#8,T!x>2!ꮛR "櫞o#ⴧ$%&&'T(%** +,,-X.F/N 0 1 2 3#44 567}8P9+9: ;ރ>> >>=&<d!:L98+9*(8b( <8 >>=&<d!:L98+9*(8b( <8 >>=&<d!:L98+9*(8b( <8 >I=-<Ԩ<Д ;z:W9898Ə7[7ͯ65@@@@8 3X;,8 3X;,8 3X;,(k +L-2/2 54S7 %^9+e; 0 >V;!|8 95f4I2(V0 6q/ &W- "J,>+  6* 7)7)  <(:OJ0O' (W^/b& ,dW*$l%#dA) *y$T~3* -#5vS&  1#L93"%sn4 99 3 )3 8[4 V03 b 2{  !`e,o VI1e G>1] 8D 0T !eK*8X  S P#"/@Fk   @ b4*'-ACCP  ( E! $00@ d Z !=  = }7 4R #n  N* C*  :* ,s*!K*>)+l)W) J)9)()b)O(7(%([' >'0'!p&S&G& A%?q%2[%I%>V;!|8 95f4I2(V0 6q/ &W- "J,>+  6* 7)7)  <(:OJ0O' (W^/b& ,dW*$l%#dA) *y$T~3* -#5vS&  1#L93"%sn4 99 3 )3 8[4 V03 b 2{  !`e,o VI1e G>1] 8D 0T !eK*8X  S P#"/@Fk   @ b4*'-ACCP  ( E! $00@ d Z !=  = }7 4R #n  N* C*  :* ,s*!K*>)+l)W) J)9)()b)O(7(%([' >'0'!p&S&G& A%?q%2[%I%>V;!|8 95f4I2(V0 6q/ &W- "J,>+  6* 7)7)  <(:OJ0O' (W^/b& ,dW*$l%#dA) *y$T~3* -#5vS&  1#L93"%sn4 99 3 )3 8[4 V03 b 2{  !`e,o VI1e G>1] 8D 0T !eK*8X  S P#"/@Fk   @ b4*'-ACCP  ( E! $00@ d Z !=  = }7 4R #n  N* C*  :* ,s*!K*>)+l)W) J)9)()b)O(7(%([' >'0'!p&S&G& A%?q%2[%I%965556i68x6 G6`55555 55C5d5555554e414 433p3>2&2 222y2L2/11 10 0 g/ C/ . . . - z- G- - , , b, &, , x, A+ !+ + Q* $*k7.# 3$)A.;; )a. S:)1 (S3' h7G& &  / C@&  "F6+S  %C(+.&/    -../443-    ^  #..+)&"  ;-5#-+     %  .D:,71.,# #$ -JC! ##  *.0,# l(;@   #%%).& W'# G    V  ,9;<81+! i"1=@ACFF]cpO7  #(1574'g %.)  &/0.+'&%%#!#%'M   %6<@BFFB@<:760.6  "$&),..0-'%$s(    ] !V *440&b( $ !߃9  #P& &X-)u9   .mYMC;6-(%#! !$'4ҺXQH<7?<0/8XfJ6&/5??3R4<2/@c`E1&/3552?//43/2GeW=+/p5/794/ y7//:851/8C7/3??HQQNMJG@80/<?=6// \2/:<0/7PV@1/09>>=<@DHPNE?<73/6?;51// I/5?3/3/8?MQROH 8/?L\`B3 /0231/39<@EHIIMQJ s8/;?AKH4/0/f8/2?7 /3:;98753321 /r:/5?< /083/B1//:;/9GS^`aceex}l;/X5/1<83/7?@CDCHLSVXVK7/܀>003 /6?IW]`beeb`][YWRQW803:?3 /17;?CGIJMOQQRPKII@?@L5/381 /03356789;<?=1/xE3/0/9BFC?<1r@3/8MVVRJ8|L93/7@EIE<0![B83/0352/#mJ=50/&tP90/)׌[E;4310 /.誆ukc\WPLIHFEFIK4{xz}}vr~x)rՔ~}{rwu)r}ur~}s)rΊ}xruy'rutr~|}{srxv&rv}}u܉u{tr}s&rtvvt}rruurt{+rvrwzrux+r݉srtrxtrsuvxyxxvut r}ry}zrutr{~uru|yur wrryxvsrxxru}}~xsr{}{vrr try{srx~srsy||{}}{xtrv}zvsrr ۄrv}ur{}srs{}xtrv}~zwvv zrx}{rsvz~}|trx} xr}u rstusrty{} xrz}~ursrxrt}x ruyzyxwvuuts ryrv}z rs{|xursrryzryzrюvrs{xtrx}}wr|ssz}zv r}{y{}~xsrdžusy}}zs rs}{suz}|u rv}čxsuy}t rswz}~}}찅vrtxs rstuvvxxyz{}{sr⣁ursry}{s۟~urxxܦyurw}{s!xtrsuvtr#Ҝ{vsr&̡ysr)䱏zutss r.ƭ4 &..+ *DF@%#+..)   *9F?/ ` ./.  !    )8F<&  *.170    *      @#(*+' (..'    @  !&"  ).DA&    \ ! )    k  "' >  +)& +,,/41*%  .084+       (q  /n{  :w   !l  0^l   #o4  . z *.17Dd 4 a,$-G$K g$~"x%:%K %eT'('='T)p ) *=*L+i o," 7-, -9 .Jn0fZ1 O2&I3364DC5^ A515 5)5=q6XN5 3/4>?AJQQNA80 /7AMde`I8/./>??AHNQQM?<0//248;6/4?M[e_RD5/.*`/38?BQRQC?>;>?<83//7?@EFD@: /.$ /3?EMZe]J??<72/4?1 /,/:MQSXRB61/>?6 /,/1;>>71 /8?< /*/032/3??;2 //13579<>??</3 //2368:<?<8/@/7@HLMNK?=73//1=LQQK@?>5/ /1=BA?6/ @ 5 038=?ANM8/./& NOORVSMI@??;83/.)'// QRZVNE<63 /.-# ?><852/,  /144 /?L /6R{ /0330/2[w /1330/37F /3Ryl  /5H4 /023368>Q z MQSXd~ 4 a,$-G$K g$~"x%:%K %eT'('='T)p ) *=*L+i o," 7-, -9 .Jn0fZ1 O2&I3364DC5^ A515 5)5=q6XN5 3ru|}~~xs rw~xrq%r|}}~}zsrrtuxzvru}vrqe `rtx}}|z|}{xurrx}~}y rqW ru}}}zwtru}s rm/ryvsr|}v rk rsz||xs rx}{ r*rsttrt}}zt rrsuvxyz|}}zrut rrtuvxy{}{xr@rx~}{wtrrs{~}|vr rs{~}vr @ v{~~wr  ~}vr {z{yusr \ !r ) r r rstu rk rstuvxyz}yrqr> sux{}~xrqr& }zxurqe^rr zvt rqnW& }|zxvtrqlP rsutD r} rv{ rsttsrtw rsuusrtx rul  rv4 rstuuvx| z 4 a,$-G$K g$~"x%:%K %eT'('='T)p ) *=*L+i o," 7-, -9 .Jn0fZ1 O2&I3364DC5^ A515 5)5=q6XN5 3f==55m4M4\3l32W1(0/F|-|6, + *( `)< ,)R )g )>) ) #i# $b* *W ( )J ( )< ) %* C*`*  *; +I+v+A!+U,-C^,<-)&. yև.113568:<=>>>>>>>G>A7D>6V?6E6V! 5w'474W 4423` 3' 3=3N3[3n$3)3/ 36 3>3I3 T3 _3 j3 u3 3 3 3 3 !2 2 !2 2 2 2  2  2 2 y2 t2 b 2S2C222 22m 3P383!44R4044y 4F5(66b 6>7"7q7O 868G>A7D>6V?6E6V! 5w'474W 4423` 3' 3=3N3[3n$3)3/ 36 3>3I3 T3 _3 j3 u3 3 3 3 3 !2 2 !2 2 2 2  2  2 2 y2 t2 b 2S2C222 22m 3P383!44R4044y 4F5(66b 6>7"7q7O 868G>A7D>6V?6E6V! 5w'474W 4423` 3' 3=3N3[3n$3)3/ 36 3>3I3 T3 _3 j3 u3 3 3 3 3 !2 2 !2 2 2 2  2  2 2 y2 t2 b 2S2C222 22m 3P383!44R4044y 4F5(66b 6>7"7q7O 86835֚474՛343Ӎ3 2 `2 1 %1 j1 Š0 آ+0 P0 r/ ܛ / '/ K/ k/ . . . . $. .. 6. ;. A. }. ղ. . խ. Ւ. n. L. H. B. 9.0.&..../ / /o/ B/ / ڝ/ y0 E0 Ҭ0 ٿ0 κ`1 ǰ,1ž1x1A1 23P3պ3Ӟ3@@@@+% p$W$C#,#G" j" G"6"+!#!![!A!4  &h L 7 $gJ 5#vL4$eT E5g8g $K 1[ 6q! %a!q"1"#|#c#I$0$&$!####$#1# 4#3#/##X#L#$l#  F #  8 # (e # K "  $`" 7d"#>">" %7+ W# fR!#Fs2   +% p$W$C#,#G" j" G"6"+!#!![!A!4  &h L 7 $gJ 5#vL4$eT E5g8g $K 1[ 6q! %a!q"1"#|#c#I$0$&$!####$#1# 4#3#/##X#L#$l#  F #  8 # (e # K "  $`" 7d"#>">" %7+ W# fR!#Fs2   +% p$W$C#,#G" j" G"6"+!#!![!A!4  &h L 7 $gJ 5#vL4$eT E5g8g $K 1[ 6q! %a!q"1"#|#c#I$0$&$!####$#1# 4#3#/##X#L#$l#  F #  8 # (e # K "  $`" 7d"#>">" %7+ W# fR!#Fs2    * b)E).)( (r(\'H'2'&&%t%V%*$$#A# "[""m!!x (7 2c N<h8g$Zi o !_!d i   $ 5DNRi ~   *X} 5 555T 66~6a5&L5I?646 +6$!E"j "u|"/""t `"R z'!% d! WN JL, Anf 4@ 3 &k7 X6 N /[a 0 >>>>>>>4&&&&&&&&&&&&&&&''( ) R+ $9x7D6 5V55y5 43D33r344E4 4υ5Q54 4ׇ4X544"444 444~%8[9C9(:c:&;<)<=F=>|># ~%8[9C9(:c:&;<)<=F=>|># ~%8[9C9(:c:&;<)<=F=>|># t4׿24ӫ4ː5[5گ5͗5c6ޫ6ʏ6չ^7Ч7LJ7նU8ϡ8Ɓ8կN9̓9ظz9աE:ˑ:v:ث>;ϓ;o;ү9<ŝ <o<4==^=-= >>@@@@ %2"[" 1n!Av! Cx!7q! )e!X!G! 2! :!P!b!(q!3 6 "L  / A %x !g 's +~4 ? 6u !b" CE&HRQ)02 6 " 6 , 0 9 "C L V  ` g#xF  ##e+u" /āE S"  Cd3 I" s J' @" ) eA, L"@ R?4-*["[zFCJh" pps"+"1! #4!*5!*4!(3" 2"0","g"'"""'" >"[" 1n!Av! Cx!7q! )e!X!G! 2! :!P!b!(q!3 6 "L  / A %x !g 's +~4 ? 6u !b" CE&HRQ)02 6 " 6 , 0 9 "C L V  ` g#xF  ##e+u" /āE S"  Cd3 I" s J' @" ) eA, L"@ R?4-*["[zFCJh" pps"+"1! #4!*5!*4!(3" 2"0","g"'"""'" >"[" 1n!Av! Cx!7q! )e!X!G! 2! :!P!b!(q!3 6 "L  / A %x !g 's +~4 ? 6u !b" CE&HRQ)02 6 " 6 , 0 9 "C L V  ` g=`z6HXkz &/7=EKQW\afhlgb[VPJHB?93' iK$ Gyoqy6;kSO8kDA@G76)*+E7**/5(**K545 !n5P5  756&5V n4n  S545,!5De4^ F5 *52 v4K T5x! 452 ~4TT5!355  x4bF5#/5A  a4x?50/4P[5# :5? 'u4uA57+4hJ4,,4[ O4$)4O  T4 54C a4?u4<[3n" 7k4DJ4'V4` v4<?3&R4~ j4[ *4?  ?4(b4e#4? R4W %4s W3"*4-g32G3-';kSO8kDA@G76)*+E7**/5(**K545 !n5P5  756&5V n4n  S545,!5De4^ F5 *52 v4K T5x! 452 ~4TT5!355  x4bF5#/5A  a4x?50/4P[5# :5? 'u4uA57+4hJ4,,4[ O4$)4O  T4 54C a4?u4<[3n" 7k4DJ4'V4` v4<?3&R4~ j4[ *4?  ?4(b4e#4? R4W %4s W3"*4-g32G3-';kSO8kDA@G76)*+E7**/5(**K545 !n5P5  756&5V n4n  S545,!5De4^ F5 *52 v4K T5x! 452 ~4TT5!355  x4bF5#/5A  a4x?50/4P[5# :5? 'u4uA57+4hJ4,,4[ O4$)4O  T4 54C a4?u4<[3n" 7k4DJ4'V4` v4<?3&R4~ j4[ *4?  ?4(b4e#4? R4W %4s W3"*4-g32G3-'G  +I"S-/Ru OwJdnAľ!@-!>-"'-#-} & -n + ,a) ,W" ,L# ,E% ,6  ,&  , , + +b  +E  +2  +& *e *7  * )~ )^  ): ) (d  (7 ( 'h 'G '3''&&l&X&K&C&3%%%%%%p %b%T%J%>%/%$%%%$ $v$d$W$N$F  $=  $1  G  +I"S-/Ru OwJdnAľ!@-!>-"'-#-} & -n + ,a) ,W" ,L# ,E% ,6  ,&  , , + +b  +E  +2  +& *e *7  * )~ )^  ): ) (d  (7 ( 'h 'G '3''&&l&X&K&C&3%%%%%%p %b%T%J%>%/%$%%%$ $v$d$W$N$F  $=  $1  G  +I"S-/Ru OwJdnAľ!@-!>-"'-#-} & -n + ,a) ,W" ,L# ,E% ,6  ,&  , , + +b  +E  +2  +& *e *7  * )~ )^  ): ) (d  (7 ( 'h 'G '3''&&l&X&K&C&3%%%%%%p %b%T%J%>%/%$%%%$ $v$d$W$N$F  $=  $1  4444444u4e4X4I4B4?4<47403(33 3 3ٞ2ו2ϗ2Ƨ2ô2λ2~2922 1| 1ׅ 1ܑ 10 0 #0 0 0000////.....--- - --.//001114>L=R<T;V9X8V.(OhU& 2eT 5Y|\'Io i 9e 2q!ŏY8+($ ")2;X{&Ǽ.>30/ @@@) u_? ܸtүdþ=ξ^"zW4ü`II&Ű`II-ǯbIFμrFʼoֿ v γ||vL  ȴzvR oG ɱ]# ϰ3äW õ0о!ȫsԾy=k: C8_6 M493 W362^3C3 459759"U<3 @  I ~Z8(b"ȿ ,c.@8:>9W~4 #6Mk,!.>Yx$ &6He $4Ko"$Ep +@q10P5 7W9 #A= n q!s!n!b!P"7"v"G"+v#A#$`$ 4$I%"b%*% 5&E'X (!p (+ ) 7 * > + A, E, I- K. N. O/ >0)1c1 =2 3 4 5 6776654 'X3Ak2 *^1K~09n0#]/A, )n(X % N $ :n "">X! 1On  @^~ "Dh=b-R0g`,X n q!s!n!b!P"7"v"G"+v#A#$`$ 4$I%"b%*% 5&E'X (!p (+ ) 7 * > + A, E, I- K. N. O/ >0)1c1 =2 3 4 5 6776654 'X3Ak2 *^1K~09n0#]/A, )n(X % N $ :n "">X! 1On  @^~ "Dh=b-R0g`,X n q!s!n!b!P"7"v"G"+v#A#$`$ 4$I%"b%*% 5&E'X (!p (+ ) 7 * > + A, E, I- K. N. O/ >0)1c1 =2 3 4 5 6776654 'X3Ak2 *^1K~09n0#]/A, )n(X % N $ :n "">X! 1On  @^~ "Dh=b-R0g`,XJjT9$kr`I4#zmb\YPYt """"##$$%%%&'(()*+,,*b&.Ko!/J| *J|'Cs # Z+F K+J  9+R '+l e, ?,$-p -2 .A .7 -#  -l -G ,z!,Jh ƘjG/'C9 a@0 /RȮX=( 0sw%]eklhb^[O>,K@  >xX$$R~p"'Gq! (Ca< $:Jez]2Fb~v!)Gg7  !DjR>eg:`1vA We!v/< JXh%~1< GS`#l,x5 >  E K L  L& L* 0)d1!L/T7.o1 (.2 k- ]-x S-p G-[ <,K  1,C (,= "k,> Z+F K+J  9+R '+l e, ?,$-p -2 .A .7 -#  -l -G ,z!,Jh ƘjG/'C9 a@0 /RȮX=( 0sw%]eklhb^[O>,K@  >xX$$R~p"'Gq! (Ca< $:Jez]2Fb~v!)Gg7  !DjR>eg:`1vA We!v/< JXh%~1< GS`#l,x5 >  E K L  L& L* 0)d1!L/T7.o1 (.2 k- ]-x S-p G-[ <,K  1,C (,= "k,> Z+F K+J  9+R '+l e, ?,$-p -2 .A .7 -#  -l -G ,z!,Jh ƘjG/'C9 a@0 /RȮX=( 0sw%]eklhb^[O>,K@  >xX$$R~p"'Gq! (Ca< $:Jez]2Fb~v!)Gg7  !DjR>eg:`1vA We!v/< JXh%~1< GS`#l,x5 >  E K L  L& L*  <;;;::999888775 5 4 4 4 3 $)  $"  $  $  $  ${  $p  #d  #W  #O  #G  #?  #8  #2  ",  "(  "!  "  !  ~ z  n j  b ^  O R  = J  ,C  `>  A7  /1  #n,  P(  D"  =u   9h  9V  :J  9@  89  81  7*  6# 6u 4^ 1L -C  +9  )/ &' $! !u  ` N A   4   *  # y  uv nd  jS   eC   `6  S, G, 9+ $)  $"  $  $  $  ${  $p  #d  #W  #O  #G  #?  #8  #2  ",  "(  "!  "  !  ~ z  n j  b ^  O R  = J  ,C  `>  A7  /1  #n,  P(  D"  =u   9h  9V  :J  9@  89  81  7*  6# 6u 4^ 1L -C  +9  )/ &' $! !u  ` N A   4   *  # y  uv nd  jS   eC   `6  S, G, 9+ $)  $"  $  $  $  ${  $p  #d  #W  #O  #G  #?  #8  #2  ",  "(  "!  "  !  ~ z  n j  b ^  O R  = J  ,C  `>  A7  /1  #n,  P(  D"  =u   9h  9V  :J  9@  89  81  7*  6# 6u 4^ 1L -C  +9  )/ &' $! !u  ` N A   4   *  # y  uv nd  jS   eC   `6  S, G, 9+ 2 2 2 2 3 3 3 3 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 2 2 2 2 2 $#"ҶAֱeӾ רAű S׶z +Ǽǥz| * ¬zz~ M Ť~z 2 ”YPvz 8 ʼƧ`IUz~ J ӻe^| ؂ ι¾ ! " ) 100 +,-/;<;=<1/ԟ9֥P7۬Y5߶k4|2̏#1ڞ,.? +͡t! )˟o#(溌@ 'ܩm%ӚN@@@ƈ(I3w"Vn "A p ٛ ޴B m ʎ& ך+  k 6 q  ݰ ǜ ݺ$ Ҍ ާ ޱ$ ߯; ؤC ͓7 & ٯg ˔AkБ@ް`|/ɏ4͝= H @ @ @ @ @ @ @ @T1|3[ ,8@GUg}    TO  R4R5SSg* NNG "F=E7kW]@4a2(AWRI|b5xXdq(  ?qb Sd:/ WW`s F6 +Z?bL1*=`GSx Aא)8j G`1  SS  &GP&  !!"$&/T1|3[ ,8@GUg}    TO  R4R5SSg* NNG "F=E7kW]@4a2(AWRI|b5xXdq(  ?qb Sd:/ WW`s F6 +Z?bL1*=`GSx Aא)8j G`1  SS  &GP&  !!"$&/T1|3[ ,8@GUg}    TO  R4R5SSg* NNG "F=E7kW]@4a2(AWRI|b5xXdq(  ?qb Sd:/ WW`s F6 +Z?bL1*=`GSx Aא)8j G`1  SS  &GP&  !!"$&/#N% v&4'W'~''-' <' D' I'W'A'7'-'& ^%-! b  7o "2De(2U| $o  R P 0gN%^+r!  !m"#z##:# K#7! k(~ #\ (AWbbkt~xoe^UI>7/*%   ,  9 VL-  J+  E$ v= ]* Fl-z< zF! ǁZ7! E1 ! "$')+. 1 48;>>L-  J+  E$ v= ]* Fl-z< zF! ǁZ7! E1 ! "$')+. 1 48;>>L-  J+  E$ v= ]* Fl-z< zF! ǁZ7! E1 ! "$')+. 1 48;>>, 1 0/ - , )(&%#"! "$')+. 1 48;>>1'Ÿ{tne_URRmʸ~i\P0I̿vUH:34- \ο~cH5$ 4žoY<+ %%   +b .F1$k;&e=02 lT Ax *# I= 'j D$ "E @x  (( qK O  6+ 'S #'1-[ AZ4&~^ ?[!  2>  FV  )g ةR  :~xuqmldYQI@:51-(%   Ph1   ,ej*   /pp)   (qx, ]: ,OnJ$  >O /n=CuR%nb(le :O4q-R&p/HA 6! =E GR P+LRE,%d2`T)-pE #'s/$xa<(bJG2*mْhD&hl.&1# 5b02 lT Ax *# I= 'j D$ "E @x  (( qK O  6+ 'S #'1-[ AZ4&~^ ?[!  2>  FV  )g ةR  :~xuqmldYQI@:51-(%   Ph1   ,ej*   /pp)   (qx, ]: ,OnJ$  >O /n=CuR%nb(le :O4q-R&p/HA 6! =E GR P+LRE,%d2`T)-pE #'s/$xa<(bJG2*mْhD&hl.&1# 5b02 lT Ax *# I= 'j D$ "E @x  (( qK O  6+ 'S #'1-[ AZ4&~^ ?[!  2>  FV  )g ةR  :~xuqmldYQI@:51-(%   Ph1   ,ej*   /pp)   (qx, ]: ,OnJ$  >O /n=CuR%nb(le :O4q-R&p/HA 6! =E GR P+LRE,%d2`T)-pE #'s/$xa<(bJG2*mْhD&hl.&1# 5b2 3 3 3 3 4 4 4 5 5 5 5 5 666666665 5 5 5 4 4 4 2 10.,)&#$$ˡӳߝ ؟Cװ+6ͤA 4á71ٺv2 ,̫g-T#U# Rº|kD#  -KzóhL3 %4AO^kv|}}{zrfZM=0% 5 @ @ @$Ƈ5#߸r "רY!̕7 i إM̔91m'ߨI ٛ- !Ќ(#t'$J%2(4(?)K *P+ R!+ R, K. @/ =0=1H2T3a4r5 6Ȏ7ט-7P8| 9ԓ"9^ :՝:m ;͎0;D <R = < < =m:h4N'PrismLauncher-11.0.3/launcher/resources/sources/magitech.svg0000644000175100017510000012035515224505336023571 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/squarecreeper.svg0000644000175100017510000006751415224505336024665 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/pskeleton.svg0000644000175100017510000003363115224505336024014 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/gear.svg0000644000175100017510000004100615224505336022721 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/herobrine.svg0000644000175100017510000003600615224505336023764 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/steve.svg0000644000175100017510000003340415224505336023134 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/clucker.svg0000644000175100017510000002762215224505336023443 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/sources/meat.svg0000644000175100017510000002216715224505336022740 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/OSX/0000755000175100017510000000000015224505336020247 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/OSX/OSX.qrc0000644000175100017510000000326315224505336021433 0ustar runnerrunner index.theme scalable/about.svg scalable/accounts.svg scalable/bug.svg scalable/centralmods.svg scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg scalable/jarmods.svg scalable/java.svg scalable/language.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg scalable/new.svg scalable/news.svg scalable/notes.svg scalable/proxy.svg scalable/refresh.svg scalable/resourcepacks.svg scalable/shaderpacks.svg scalable/screenshots.svg scalable/settings.svg scalable/status-bad.svg scalable/status-good.svg scalable/status-yellow.svg scalable/viewfolder.svg scalable/worlds.svg scalable/delete.svg scalable/tag.svg scalable/export.svg scalable/rename.svg scalable/launch.svg scalable/shortcut.svg PrismLauncher-11.0.3/launcher/resources/OSX/index.theme0000644000175100017510000000022115224505336022375 0ustar runnerrunner[Icon Theme] Name=OSX Comment=OSX theme by pexner Inherits=multimc Directories=scalable [scalable] Size=48 Type=Scalable MinSize=16 MaxSize=256 PrismLauncher-11.0.3/launcher/resources/OSX/scalable/0000755000175100017510000000000015224505336022015 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/OSX/scalable/settings.svg0000644000175100017510000000426015224505336024400 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/shortcut.svg0000644000175100017510000000156615224505336024421 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/screenshots.svg0000644000175100017510000000246215224505336025102 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/notes.svg0000644000175100017510000000271215224505336023670 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/status-good.svg0000644000175100017510000000234015224505336025006 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/jarmods.svg0000644000175100017510000000262515224505336024202 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/status-yellow.svg0000644000175100017510000000211215224505336025366 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/java.svg0000644000175100017510000000361415224505336023463 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/bug.svg0000644000175100017510000000510515224505336023314 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/custom-commands.svg0000644000175100017510000000660515224505336025656 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/OSX/scalable/log.svg0000644000175100017510000000225315224505336023321 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/checkupdate.svg0000644000175100017510000000272715224505336025026 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/export.svg0000644000175100017510000000420015224505336024053 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/copy.svg0000644000175100017510000000225315224505336023512 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/minecraft.svg0000644000175100017510000000156715224505336024517 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/externaltools.svg0000644000175100017510000000156615224505336025451 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/instance-settings.svg0000644000175100017510000000375515224505336026212 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/language.svg0000644000175100017510000000651615224505336024331 0ustar runnerrunner image/svg+xmlPrismLauncher-11.0.3/launcher/resources/OSX/scalable/centralmods.svg0000644000175100017510000000236415224505336025056 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/news.svg0000644000175100017510000000224215224505336023512 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/refresh.svg0000644000175100017510000000211115224505336024167 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/new.svg0000644000175100017510000000240215224505336023325 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/rename.svg0000644000175100017510000000213115224505336024002 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/patreon.svg0000644000175100017510000000215515224505336024211 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/delete.svg0000644000175100017510000000431315224505336024001 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/resourcepacks.svg0000644000175100017510000000274215224505336025414 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/about.svg0000644000175100017510000000222315224505336023647 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/proxy.svg0000644000175100017510000000244715224505336023726 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/loadermods.svg0000644000175100017510000000215715224505336024674 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/coremods.svg0000644000175100017510000000224615224505336024355 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/launch.svg0000644000175100017510000000340715224505336024014 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/shaderpacks.svg0000644000175100017510000000302215224505336025023 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/tag.svg0000644000175100017510000000237715224505336023322 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/viewfolder.svg0000644000175100017510000000210715224505336024704 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/worlds.svg0000644000175100017510000000572015224505336024054 0ustar runnerrunner image/svg+xmlPrismLauncher-11.0.3/launcher/resources/OSX/scalable/help.svg0000644000175100017510000000721615224505336023474 0ustar runnerrunner image/svg+xmlPrismLauncher-11.0.3/launcher/resources/OSX/scalable/accounts.svg0000644000175100017510000000202415224505336024353 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/OSX/scalable/status-bad.svg0000644000175100017510000000175015224505336024610 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/0000755000175100017510000000000015224505336022241 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/breeze_light/breeze_light.qrc0000644000175100017510000000364315224505336025421 0ustar runnerrunner index.theme scalable/about.svg scalable/accounts.svg scalable/bug.svg scalable/centralmods.svg scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg scalable/jarmods.svg scalable/java.svg scalable/language.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg scalable/matrix.svg scalable/new.svg scalable/news.svg scalable/notes.svg scalable/proxy.svg scalable/reddit-alien.svg scalable/refresh.svg scalable/resourcepacks.svg scalable/shaderpacks.svg scalable/shortcut.svg scalable/screenshots.svg scalable/settings.svg scalable/status-bad.svg scalable/status-good.svg scalable/status-yellow.svg scalable/viewfolder.svg scalable/worlds.svg scalable/delete.svg scalable/tag.svg scalable/export.svg scalable/rename.svg scalable/launch.svg scalable/server.svg scalable/appearance.svg PrismLauncher-11.0.3/launcher/resources/breeze_light/index.theme0000644000175100017510000000023115224505336024370 0ustar runnerrunner[Icon Theme] Name=Breeze Light Comment=Breeze Light Icons Inherits=multimc Directories=scalable [scalable] Size=48 Type=Scalable MinSize=16 MaxSize=256 PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/0000755000175100017510000000000015224505336024007 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/settings.svg0000644000175100017510000000133015224505336026365 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/shortcut.svg0000644000175100017510000000172015224505336026403 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/screenshots.svg0000644000175100017510000000117515224505336027074 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/notes.svg0000644000175100017510000000122215224505336025655 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/datapacks.svg0000644000175100017510000000226715224505336026472 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/status-good.svg0000644000175100017510000000102615224505336027000 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/jarmods.svg0000644000175100017510000000270215224505336026170 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/status-yellow.svg0000644000175100017510000000113215224505336027361 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/java.svg0000644000175100017510000000415515224505336025456 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/reddit-alien.svg0000644000175100017510000000132015224505336027065 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/bug.svg0000644000175100017510000000070515224505336025307 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/custom-commands.svg0000644000175100017510000000107115224505336027640 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/log.svg0000644000175100017510000000067515224505336025321 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/checkupdate.svg0000644000175100017510000000110115224505336027001 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/export.svg0000644000175100017510000000112315224505336026046 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/copy.svg0000644000175100017510000000103115224505336025475 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/matrix.svg0000644000175100017510000000265615224505336026045 0ustar runnerrunner Matrix (protocol) logo PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/minecraft.svg0000644000175100017510000000344115224505336026502 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/externaltools.svg0000644000175100017510000000111015224505336027424 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/instance-settings.svg0000644000175100017510000000103715224505336030173 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/language.svg0000644000175100017510000000112115224505336026306 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/centralmods.svg0000644000175100017510000000042115224505336027040 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/news.svg0000644000175100017510000000063715224505336025512 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/refresh.svg0000644000175100017510000000315115224505336026166 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/new.svg0000644000175100017510000000104215224505336025316 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/server.svg0000644000175100017510000000075715224505336026047 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/rename.svg0000644000175100017510000000156315224505336026004 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/discord.svg0000644000175100017510000000173415224505336026164 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/patreon.svg0000644000175100017510000000040515224505336026177 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/delete.svg0000644000175100017510000000111515224505336025770 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/resourcepacks.svg0000644000175100017510000000125315224505336027402 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/about.svg0000644000175100017510000000075715224505336025653 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/proxy.svg0000644000175100017510000000127215224505336025713 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/loadermods.svg0000644000175100017510000000060215224505336026657 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/coremods.svg0000644000175100017510000000057315224505336026350 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/launch.svg0000644000175100017510000000042315224505336026001 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/shaderpacks.svg0000644000175100017510000000120115224505336027012 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/tag.svg0000644000175100017510000000114215224505336025301 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/viewfolder.svg0000644000175100017510000000060715224505336026701 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/worlds.svg0000644000175100017510000005032015224505336026042 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/appearance.svg0000644000175100017510000000160415224505336026630 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/help.svg0000644000175100017510000000125015224505336025456 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/accounts.svg0000644000175100017510000000234215224505336026350 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_light/scalable/status-bad.svg0000644000175100017510000000072715224505336026605 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/0000755000175100017510000000000015224505336021203 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/pe_dark/pe_dark.qrc0000644000175100017510000000347115224505336023324 0ustar runnerrunner index.theme scalable/about.svg scalable/accounts.svg scalable/bug.svg scalable/centralmods.svg scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg scalable/jarmods.svg scalable/java.svg scalable/language.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg scalable/new.svg scalable/news.svg scalable/notes.svg scalable/proxy.svg scalable/refresh.svg scalable/resourcepacks.svg scalable/shaderpacks.svg scalable/screenshots.svg scalable/settings.svg scalable/status-bad.svg scalable/status-good.svg scalable/status-yellow.svg scalable/viewfolder.svg scalable/worlds.svg scalable/delete.svg scalable/tag.svg scalable/export.svg scalable/rename.svg scalable/launch.svg scalable/shortcut.svg scalable/server.svg scalable/appearance.svg PrismLauncher-11.0.3/launcher/resources/pe_dark/index.theme0000644000175100017510000000023615224505336023337 0ustar runnerrunner[Icon Theme] Name=Simple (Dark) Comment=Icons by pexner (dark) Inherits=multimc Directories=scalable [scalable] Size=48 Type=Scalable MinSize=16 MaxSize=256 PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/0000755000175100017510000000000015224505336022751 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/settings.svg0000644000175100017510000000230515224505336025332 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/shortcut.svg0000644000175100017510000000215615224505336025351 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/screenshots.svg0000644000175100017510000000231615224505336026034 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/notes.svg0000644000175100017510000000237415224505336024630 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/datapacks.svg0000644000175100017510000002617115224505336025434 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/status-good.svg0000644000175100017510000000170615224505336025747 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/jarmods.svg0000644000175100017510000000503715224505336025136 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/status-yellow.svg0000644000175100017510000000205415224505336026327 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/java.svg0000644000175100017510000000572115224505336024420 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/bug.svg0000644000175100017510000000270015224505336024246 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/custom-commands.svg0000644000175100017510000002537515224505336026617 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/log.svg0000644000175100017510000000216115224505336024253 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/checkupdate.svg0000644000175100017510000000210215224505336025745 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/export.svg0000644000175100017510000000406415224505336025017 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/copy.svg0000644000175100017510000000167215224505336024452 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/minecraft.svg0000644000175100017510000000173215224505336025445 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/externaltools.svg0000644000175100017510000000215615224505336026401 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/instance-settings.svg0000644000175100017510000000230515224505336027134 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/language.svg0000644000175100017510000000642615224505336025265 0ustar runnerrunner image/svg+xmlPrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/centralmods.svg0000644000175100017510000000170715224505336026012 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/news.svg0000644000175100017510000000126315224505336024450 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/refresh.svg0000644000175100017510000000154715224505336025137 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/new.svg0000644000175100017510000000174215224505336024267 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/server.svg0000644000175100017510000000134015224505336024776 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/rename.svg0000644000175100017510000000167015224505336024745 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/patreon.svg0000644000175100017510000000172615224505336025150 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/delete.svg0000644000175100017510000000621715224505336024742 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/resourcepacks.svg0000644000175100017510000000211115224505336026336 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/about.svg0000644000175100017510000000146615224505336024613 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/proxy.svg0000644000175100017510000000224415224505336024655 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/loadermods.svg0000644000175100017510000000212415224505336025622 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/coremods.svg0000644000175100017510000000200615224505336025303 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/launch.svg0000644000175100017510000000213715224505336024747 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/shaderpacks.svg0000644000175100017510000000542715224505336025772 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/tag.svg0000644000175100017510000000302715224505336024247 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/viewfolder.svg0000644000175100017510000000153215224505336025641 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/worlds.svg0000644000175100017510000000544315224505336025012 0ustar runnerrunner image/svg+xmlPrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/appearance.svg0000644000175100017510000000534315224505336025576 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/help.svg0000644000175100017510000000606415224505336024430 0ustar runnerrunner image/svg+xmlPrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/accounts.svg0000644000175100017510000000273615224505336025321 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_dark/scalable/status-bad.svg0000644000175100017510000000146315224505336025545 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/0000755000175100017510000000000015224505336022053 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/breeze_dark/breeze_dark.qrc0000644000175100017510000000364215224505336025044 0ustar runnerrunner index.theme scalable/about.svg scalable/accounts.svg scalable/bug.svg scalable/centralmods.svg scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg scalable/jarmods.svg scalable/java.svg scalable/language.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg scalable/matrix.svg scalable/new.svg scalable/news.svg scalable/notes.svg scalable/proxy.svg scalable/reddit-alien.svg scalable/refresh.svg scalable/resourcepacks.svg scalable/shaderpacks.svg scalable/shortcut.svg scalable/screenshots.svg scalable/settings.svg scalable/status-bad.svg scalable/status-good.svg scalable/status-yellow.svg scalable/viewfolder.svg scalable/worlds.svg scalable/delete.svg scalable/tag.svg scalable/export.svg scalable/rename.svg scalable/launch.svg scalable/server.svg scalable/appearance.svg PrismLauncher-11.0.3/launcher/resources/breeze_dark/index.theme0000644000175100017510000000022715224505336024207 0ustar runnerrunner[Icon Theme] Name=Breeze Dark Comment=Breeze Dark Icons Inherits=multimc Directories=scalable [scalable] Size=48 Type=Scalable MinSize=16 MaxSize=256 PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/0000755000175100017510000000000015224505336023621 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/settings.svg0000644000175100017510000000133015224505336026177 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/shortcut.svg0000644000175100017510000000172015224505336026215 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/screenshots.svg0000644000175100017510000000117515224505336026706 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/notes.svg0000644000175100017510000000122215224505336025467 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/datapacks.svg0000644000175100017510000000226715224505336026304 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/status-good.svg0000644000175100017510000000102615224505336026612 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/jarmods.svg0000644000175100017510000000270215224505336026002 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/status-yellow.svg0000644000175100017510000000113215224505336027173 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/java.svg0000644000175100017510000000415515224505336025270 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/reddit-alien.svg0000644000175100017510000000132315224505336026702 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/bug.svg0000644000175100017510000000070515224505336025121 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/custom-commands.svg0000644000175100017510000000107115224505336027452 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/log.svg0000644000175100017510000000067515224505336025133 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/checkupdate.svg0000644000175100017510000000110115224505336026613 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/export.svg0000644000175100017510000000112315224505336025660 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/copy.svg0000644000175100017510000000103115224505336025307 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/matrix.svg0000644000175100017510000000265615224505336025657 0ustar runnerrunner Matrix (protocol) logo PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/minecraft.svg0000644000175100017510000000344115224505336026314 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/externaltools.svg0000644000175100017510000000111015224505336027236 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/instance-settings.svg0000644000175100017510000000103715224505336030005 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/language.svg0000644000175100017510000000112115224505336026120 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/centralmods.svg0000644000175100017510000000042115224505336026652 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/news.svg0000644000175100017510000000063715224505336025324 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/refresh.svg0000644000175100017510000000315115224505336026000 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/new.svg0000644000175100017510000000104215224505336025130 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/server.svg0000644000175100017510000000075715224505336025661 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/rename.svg0000644000175100017510000000156315224505336025616 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/discord.svg0000644000175100017510000000173415224505336025776 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/patreon.svg0000644000175100017510000000041015224505336026005 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/delete.svg0000644000175100017510000000111515224505336025602 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/resourcepacks.svg0000644000175100017510000000125315224505336027214 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/about.svg0000644000175100017510000000075715224505336025465 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/proxy.svg0000644000175100017510000000127215224505336025525 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/loadermods.svg0000644000175100017510000000060215224505336026471 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/coremods.svg0000644000175100017510000000057315224505336026162 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/launch.svg0000644000175100017510000000042315224505336025613 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/shaderpacks.svg0000644000175100017510000000120115224505336026624 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/tag.svg0000644000175100017510000000114215224505336025113 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/viewfolder.svg0000644000175100017510000000060715224505336026513 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/worlds.svg0000644000175100017510000005032015224505336025654 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/appearance.svg0000644000175100017510000000160415224505336026442 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/help.svg0000644000175100017510000000125015224505336025270 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/accounts.svg0000644000175100017510000000234215224505336026162 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/breeze_dark/scalable/status-bad.svg0000644000175100017510000000072715224505336026417 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/0000755000175100017510000000000015224505336021711 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/pe_colored/index.theme0000644000175100017510000000024415224505336024044 0ustar runnerrunner[Icon Theme] Name=Simple (Colored) Comment=Icons by pexner (colored) Inherits=multimc Directories=scalable [scalable] Size=48 Type=Scalable MinSize=16 MaxSize=256 PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/0000755000175100017510000000000015224505336023457 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/settings.svg0000644000175100017510000000260215224505336026040 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/shortcut.svg0000644000175100017510000000200315224505336026046 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/screenshots.svg0000644000175100017510000000213515224505336026541 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/notes.svg0000644000175100017510000000246515224505336025337 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/datapacks.svg0000644000175100017510000002640215224505336026137 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/status-good.svg0000644000175100017510000000221415224505336026450 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/jarmods.svg0000644000175100017510000000367315224505336025650 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/status-yellow.svg0000644000175100017510000000211215224505336027030 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/java.svg0000644000175100017510000000574315224505336025132 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/bug.svg0000644000175100017510000000250215224505336024754 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/custom-commands.svg0000644000175100017510000002577615224505336027332 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/log.svg0000644000175100017510000000215015224505336024757 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/checkupdate.svg0000644000175100017510000000212315224505336026456 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/export.svg0000644000175100017510000000416415224505336025526 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/copy.svg0000644000175100017510000000213115224505336025147 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/minecraft.svg0000644000175100017510000000165115224505336026153 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/externaltools.svg0000644000175100017510000000200315224505336027076 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/instance-settings.svg0000644000175100017510000000260215224505336027642 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/language.svg0000644000175100017510000000635415224505336025773 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/centralmods.svg0000644000175100017510000000233415224505336026515 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/news.svg0000644000175100017510000000126715224505336025162 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/refresh.svg0000644000175100017510000000156615224505336025646 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/new.svg0000644000175100017510000000176415224505336025001 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/server.svg0000644000175100017510000000135615224505336025513 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/rename.svg0000644000175100017510000000205415224505336025450 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/patreon.svg0000644000175100017510000000162615224505336025655 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/delete.svg0000644000175100017510000000621115224505336025442 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/resourcepacks.svg0000644000175100017510000000241615224505336027054 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/about.svg0000644000175100017510000000221215224505336025307 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/proxy.svg0000644000175100017510000000220515224505336025360 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/loadermods.svg0000644000175100017510000000212715224505336026333 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/coremods.svg0000644000175100017510000000216415224505336026016 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/launch.svg0000644000175100017510000000323215224505336025452 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/shaderpacks.svg0000644000175100017510000000561115224505336026473 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/tag.svg0000644000175100017510000000352215224505336024755 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/viewfolder.svg0000644000175100017510000000215115224505336026345 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/worlds.svg0000644000175100017510000000504015224505336025511 0ustar runnerrunner image/svg+xmlPrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/appearance.svg0000644000175100017510000000607415224505336026306 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/help.svg0000644000175100017510000000674215224505336025141 0ustar runnerrunner image/svg+xmlPrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/accounts.svg0000644000175100017510000000264615224505336026027 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/scalable/status-bad.svg0000644000175100017510000000150415224505336026247 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_colored/pe_colored.qrc0000644000175100017510000000347415224505336024543 0ustar runnerrunner index.theme scalable/about.svg scalable/accounts.svg scalable/bug.svg scalable/centralmods.svg scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg scalable/jarmods.svg scalable/java.svg scalable/language.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg scalable/new.svg scalable/news.svg scalable/notes.svg scalable/proxy.svg scalable/refresh.svg scalable/resourcepacks.svg scalable/screenshots.svg scalable/settings.svg scalable/shaderpacks.svg scalable/status-bad.svg scalable/status-good.svg scalable/status-yellow.svg scalable/viewfolder.svg scalable/worlds.svg scalable/delete.svg scalable/tag.svg scalable/export.svg scalable/rename.svg scalable/launch.svg scalable/shortcut.svg scalable/server.svg scalable/appearance.svg PrismLauncher-11.0.3/launcher/resources/backgrounds/0000755000175100017510000000000015224505336022100 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/backgrounds/rory-flat-bday.png0000644000175100017510000013555115224505336025454 0ustar runnerrunnerPNG  IHDR%ŗ0IDATxtcۇm۶mۘ:µmclJS\cjv;V~i]ֳ߳2Y(B!B!B!BtAx"B< 77Wm8·=m_(B!1c86zXjK`< n!8;Yv8~c.]p'E!đ^?@U w5\nGL<^BqdH){Y %WiMpLg8o#B6WEoXj-"☐6]V 7X!G,iwDDt}v*v&$mŞJS.BHć0회obWdHCntr*YD"∐͍ls\]%Y R'|n!D|D}Si~R% ɷ\"u7%ZJ&{tCt4"p > wSB"Ots,lݫco31MLeWx"<ݱ;dľ۞LI.Έ6H \!_gW~_>xxY1U=f= 8kQfFhM"ՈHJ|"x"ؐ!^ۣRچlW(B! 5.)B!<7xd\GHKMB#Cr6rL=z?"E!YϷ@^:&O*66Ę<3AS')B!FfCi>F}5/澬ψ+&(fuK!?$/Qe-eUFHyT &F{=]YS["_<~f9Q_7SBf" TIUL^ώizyU΅[)B!bBV#&.L]]!X!ɱ&5#x_k!BHX,k>W/"_ΩWx"IECc/Ef/[11עķt BȦŕgI8#]drqbDE!a-XFlu;tTS_w^u]c@A $P[w+([3]AB&LHڳz֙Lf&省y0C+tZsSwh|u *7"d ӅVhG*7 5Bw!E- f 1`lD<ԉ`GNb&62m5uN,, D'1YfsygLV|+|-]@|+|  o _  5*ۚ֓Dk[I9i>vޞvbU.M]RwdWYt-ۄBPGgyVn$RBB^jXHUmcF vBEkO"t|7jn5RyBtɃ]7'τ' gyY#  zFYʔDjkL z)sCLZőBTWC"n;pIJlJ>l^H 7py_ 5.E(oyg\&,Tdm)-2 4v-`/$r؎@$O'0*Ň9kwčKk #"Dw,%.f   V>ٞyG&7 *LV qCDpgE"׈dwDb$]ϛk'kvqb%ى Ūng rNX"۸`iap#I!  cmׂ=|NgyYDr}[BJ"\c1=w"F =(,C"oۉɫ|cs,[ډyyLQ 6dGzRCָ)A![ҠUV"o_ H(eBB<̳ g~B)se&5fU-Ƶ&Df gn68?eB;+Kh[T߾ (r:|=UpVy%AL*W}S="}?.#X;+3φ$D+O_ Wڋ ~dܯ[(1Qڱ5s5(!T#@ÓA|%c=zhrH= jԦ8^EUƒ$!f2{Rx+Cm%RT* $Սv^Hdf;NRɢұ_3^p|cv_==}5 ;F؉{]Yuv uA$uimk,H_߽II6OxA{gyVn _Bp@xK8%؅Ĩn&%qe$6oB Hm Z( C&O'7~>[P֎7<WqYZ{Vk f/+oMI$ӌ}ZI7KD$"R3SImeW2VC;1O [ ^Y䅼"3@qK<}#HilHDyYXb/B;aUyion=#O.HM6B]AVĵOVd&WTl{HTtu}3FzطOQר v|JT}/Y42!.b+Y1I!h3vHJzqa)}|ҖHD"XB-,3< g0WxNF@|xLc"ؤ5r+/WE[Iv׽+n^Gמy%^i;X,I_*"$3(g@~/ V\@$̵6{j+ իU#i薠y.rj<7Ux+@<3xu4-ϛVkMb"iFzb#ݻs{q"2\x/gc/Fxg&+!CWϰq\|"bl)RYXܲ8> ,lc6u}PUCnw8Ґ yIA2/qD:.ONۖHb]y~2ۭƊ4h;٣A@Ҫ!ș,iT !&3< @~'  ېaStaፏ?WYXnb\Eu" @V Rk9 a\lwfaJgI?3  < b=gi(Rcc&ޒ6c cM٥~q?!93<+@~*tN J<=ۦ Ȃ\1.Dav ϳ .9ۿ "T20nQ»8QŶ]D/{L8E1ݑ<.eʙ==<2 "K޸x702WǝoC Z .>g^V-a 1KD\*pJDnδ삑UX{U05mmKlRa,#oeDQ,2Y:׎%::6,BU輑ooӆonYR[Մ,tjYy q yxxtkR٘D ޶{u}E"!ADHjExg$m‰Wm+&D2{DTg;S=CFaQ< bt5!v ofcC⷗oV[2ӑy<q-Ṫ8{ܑ?io>9@}Dܻ nR*6PD:7@O >f""}:db+ć < kkԒZ8ԆwDrvW`nֲMFJ^y(dg PkMɁvb(UBF{lBL57 iдёҴuqu?s! ^) ^3^d 9,\gsWx^6 oc\=ԴB p̯^ )Mljj#!AFvł_g,xxτůFU|Lhێ$gZdj_}iȬHַ'ŹsyO0d=w48#"GHz/9.OZy!F+Bɑ}SLmJs6?Wd"XςlN6_Sb!x ,,hx i~.ҕo!s\ 9, ۊVlx' bMmgI?\=^ ~/LϽLq>dg9XFD,. jSr5%̂Oa|D>3Ͼ'Cwrׅʹv$B֨&.&R%% ţS3OypGsʗEV G*p~Rq\u&׋ )x#fd0 id밼I+JޑLx-#r+ms nh!qAԧd'G!u(!zA渶 ޽nݫF(.,zE'~-؁Y3M駶OO:STCcPuiZj6Qk#p?"N$]Wl1=ldd5jgͫ+>T!]!W / 2eQ2kj}H&{yn"X Żfˇf)Ԍ&Wt9ei=vٳsǧgO'hsq?:8^EnRP+9~b`FJ.dtm˞Ǣz t`,Ӵʋ"<;H" UF;]A'VLj_ f;wg]K٪.ֳeMv.!> IgB;ڧ UK$'3Ͼ5!r)ci!D^G<.,H=*G">ZUZI<~ }՞Ȅ;v'><ȯe3^:8S1,caȦY,w`7isGvpZв6GH.a!3Zy!7nSLU,tbM@0ig*c%gߡ7궜):^9ᚇs\rs_\3uk\ !]ygUp",|Fۏ,,BY1.(0N!r${8l.?z G0}XG[=`}Qf횛YFAVwMzjjUSaK-]:߬΍W/R*W!idy )3Hs6wL76HsXcjD W wz$;,,(Hd.̳ uprQ$6Ŷ 7 wBZLgkaU<2|R aet q'{gQ pdؼPB]͈%e$O$cq6kϟm=ɾzi>Ӿ}m lV1SOM!RKZmfxFɂk=&魺Yu,"G5OWA/Br lT^gD-,`|f&m˛m<֢^ :"5x'N|}~z*v<ؠܦ"MHmiK& Us>~l^Olg|Zleo捙6oSڣHr yiV "ޭq1i<@2S9;Zx# D2kd]d},#)JD #Yc.xs8o˵m!@!A]^ ĉAq_AX d""9-t̳ B$-gi=ܐ {}O0PV${(|w1F6 I%ܐ@/~Ulճ<cg/n/~Mf4Zn+nji1u[@{Y`Yu :G9m=}|a Q `-hˎDu1ttq}lQ"\!&2|pM2 ǂ4pCiS?3*T.[odҔ" H8PDD>܇ixH];e&Y̏?vD iќo`0%6NMyۑLd&#U׹I /;@3N=|,=n.H^&mi.-)oV$}HK329$鷐 ixn<*o!OYI&ԇ<'ExY9&'+c KL,#CIFdǂ&|4\џ_R)V)Y"8#f$uƳ|orE,Ik6sxk#CkhFdi1R}xtEUБ]vBl5SHOOyi}[DrT:3)LX)XՏ@ǫ FRb2;"K>jg:#CaX"j:5-ԉMF L 7FxY9-8qg_I判xcjՒ RWڤ)^2X?$)iQYԈ8r!Ի(UyRȼsDK?-$i-Zb+0EU[܀Κ!D槕jJE/#[a_n{%}(L V%2Hzd=H huIgH^<gɪ*L|+!JE& J{5<{N5q3VOI;T9{H&K&i,{z,' CY6ch+W-}\m .H|ݑ>B#_.ֹCT'ejb'G̱KF[XR sD\.hOTρp٤g2Ggy#卐`oZa sҀK/_&Mx},a *0{K ¦6zʢ@VެQ^'tznyVDr@h.dURuGib?δ8o/.Gȃ]0$bAcbr !Pމd*zNx߆ly H]ۜkgYJk9PhGJ ޻ 0$= CzRZ5XxPb#<,rDRmCƞ"<[˩gXnNCo*Z; bC6ODO{Krf^ZOByix*tٵJz4taN&+ۑZ b4A^OAҬԹ[iケ-`KCXT ܼp.4g\2Ʀ K@ "s+x.\ڂHļ} $@=KGQB'Z7?P!B^]/-y,؁d o<*b$oH he_QI1_geiczk5kdxdݙAOBi_V c&R=/+jG:Bh3Gv58yc =^MޜLPUH`_Cb+ѹYAr`E~LXN|R](|(" cdNxRI&IH !mA&HYnp?#Po:o`ZTRu(Zjf'kG|Vql_雀XZ$Kj=Q Kjf jB$x$xbwD¯"<,ܖ32w_|Bp#PyNU=tgLSbNcT3!d[&Y KV(|:I;HfHe=5&#kIKQ#ʾhY\d.A.IJn&yIϛy-nF&׶l\jO=թ]r_~4ݪ&7纴BZ' TC(+6EWzK:-ZúDBq.Tp]gfN>w*^E &] 3~q|a eŖKԮeID4cdװU D]Vɤ-#F2Ԋhb%kHRH4q$Bymy5k$rEZWZR*x i&DR|6#-y銯%dWױV 7w 5kipu " iWWKYx-_BWIZ_wA"Mh_6c-'@6 7FXd2Ri!Ω"J$ԟ,8 rȘqT]OF!Ap ŋs "x#TGIm_ϚG"KUW]eW\q$'?5%h!Fܨ'# Ȅ7L,DwVZQ_.bK/?GJw-Eq`$\< j_"eh5hI J-=L yEDڭ\$ X \iOӉ.F?;9Q34˸ǂ̄'E'j޸ijuihc*L4nZg4vM HϿy"D\fkŚDua[U۲R֌geXٺU 뮻='"s1WY,M #VnWBvYXhԓ'3Gt@0.I(j-Sf]XXLeA&B 2|vfVaVZX& g*DF\0W>3 Q>z`R}+>L y"웕TtoJ$6%Y00N2Qr۶+PLi[Z*!_OJ7;LHU;+U A$DxYI? ^\ϥӺ?Vi쁒(B@mhcLzhOg+C$Ujyl-Ą Ʌ̸CРEƝ>wv ࢭyڝ߾eBBN[*&r@XGDo~CL) q=Dm/It9~skyiumqPzAyBT}mIk/H\pABxsUթLكE xJ]0؉%S\(@rUOT2hOȠƼB #mϝ$]vto}"?vlyF$z,՘ﭗH$^q_~ C"UCQD"Rң^X1yB",J =خx%KD$9ci[o+5Lj'LA]WDrG'm8AT#x m|?x%oߝ?up3$iV"?7<_v.=jJ ƺ8<[o]rɹOr5"֕{Sfrm`qyBod`ovK}*R KQD@n"<60yv ]WF& $p Yי2qI Q۔7P#&a=65YA2H抬wywJv]/&ϯE L]OW\<Jn^_vosDnq3P 1ؤMy""!bhDJLv$%뽵yH ?dBj3t1é9 3HT Esy9eo=RO _'ynٱ->2y~$Ϗ\U3sܩ(8 e8mKN$ <(ʟ$ Ǘ-7A UlԮ2MOkc#o,J2puNms26;3BD" » jB5&1SCR@5Fn?ϥh'B {FÕN!dʉ}5}Z;^ L˫#sOZf[)Fc/hz?pZtt˫^X"RY"/,PT6MXP絊p(H:юݿ(qHgYjb-˲:HpԻ s"|9=eZ:Fux%1#w <t M sCdagH!n]fSBb-0SG{KF,] Zp2-i;oծ#?w y#o-Ҭj]l0Xh'yk4YqI>֗:{|+kuJ?,qݷzX}rcdh[5p@F:bܩ CKJ$|û&P"B%9D'M4{iOnҬf@EQM\sKk؛~[HSB b]+G+0=]FO -y/քѸǖJBZ(/` )HNxxte9ڶAY>E*8=^| k;̎ncK'/LzZ"Tʐӄ>jB-Ъ`O]o\MV?n#Ny]RJ+뛅z|B_GRբ$i̅J1μ${{`[0eq?:eZ<.+~l[){mk*]WYHaYY.* 4uHT1Gby o=B\Xoj4V*9B(!yXec SLc eX؍q5eHSP/z1# M޳!S+BOvdHd3p"2qq,$?KwPi_zwi)kʾdv(e MW괤WZ$" Ȏ5 Ҙ^ B>#mt"*'mv,{d0ۗjWfX+`Q#˾Z^ug"?O Vplwѷ1Yz,px x"o?X9i( =V_Y;+u^`(Hå;,;VSL.HM#dzJ?Ewn7Xrr?ݣ`cN ը0` ɱ@_=X6Z#~CxswRW7y osһ>"O  jxCEdCZz7rd,6ĉfoKaw/Ϻ-/A$XH鍋7z`{{~hྉnNU0夶CtHG߽pA_zRPygH _DE-LwS>zzd«偬O r>,=|A ydc,+M4RځC*ӌ:@*goJy"DAsA2nw]bѧ=/"evh`Kfw ;wK1Fvw%!=3u PɆ9$YͤsjG :IK@&;_ eU)S˒U=#-ot[?)Pm_}uu.@M-.,b\. q$@i2$g9}̑LѦ-ٿ+3C%-v:"D ;n27zfkTך#N"="?kP'8B&/Zm @`n _hlX[XVګsIȹ D3UW)de.O|䯺_6L۫9(ಶ`YTnL"~́!BOW= H31S^XZ;.d+QH}T{cgC"Qq0q:s0Z6fL0ů?5@$"'V0kƽo4-Rt .xѽm ɽΆ|<ېP]71Lގu+my6q ~S3&u^_SXb zZ CѰZњ;ւ9?"*]ߤY{cGs;&Sl^I¼ʉlD$tġ78!RPYjSP,=Nh#LXaLvr`eG)kܿb w< ^+Q?R?\\t:H$H@teQz< ۍ#_#rt1z ]f"=gPb>p ⪻޺ KϦ3'9,NJbd}٠_}" eɽ_Rd/uG] Qfl9Dg&~]rp'r)&xFy^3)`'38&k]ۙ)RDrn}e*̺ !?Y'#=dAw"pCoSɺ]`X9<;i~}8Mم o5x7H 9@ B C mYUfetHNl=4 SZAD%#N’ŜD (  Z v(3YdJ32W~MEAϛ@@ktuTl+t~'g/38 k0ː4hodM@ZxHI ϒÒ4mu0NudoON8( ҝ%f h e6Uc*O 'x7H%HD<%cp-р|΂vBNe>Lm={&DmD}u=^ɓi̥q8 S8;,qTqZ.sA&M٩2\4ѝN&ȁ)W͐ ˃| ~ۼ~4{>Wu^`̥EpdP|g&{S]+.% Yr5އ\pѵ?eKI-%,HZj2܁F$r$ޭM$a` > d -e'[*UsxKK?)B B\56 7& Aɋt\-$Y׼8@)̤4,~dBkpe|s*ў~P6G՞ :?P8>[VK2. %bchz|ii8;ÜY? mˁDpV8w8k?WBiߥC i:ڸʕw%m "(8ֈDbϫȺ1!.і8<"E` nD@0ZYJ%aȞhUx2K2_\iX3>;m$syx.$״T )B xnUg ,s;ؠZ#ߵrH;}eE~2wᚍy+o[')w9W#C"q Q<{X7T\4i@xYi8UmktAtʡut5N'АfkM޷ @_` >d|E"(gIЩ-qĻyD?v~$Πu@R*QLj $uRT֫d3)z튟EZE@^k{sԨc-!G^ $bI]YCwb>9R: y;M_%ʹr߱̄'iFkbւc?KɹEfXt%)lpL16P-"'[\i WdnU. c!:Aؘ(+ܓ+Z|N֟tm##^ mG 䅙K BU wN.aQC3 bY=!%dN.y@ڬwDڐZEͦA|;"uDK2ʁ RTӇDȲCr IL$H 2T{HP(rSD2dj*!Zd9694FY#FPcwH[D!TRg P@G'ŒH>Sqa=2* (h~9ԼҨ0~oZ8gL:e%TYEҌD\Nrd(L8EJEN+y%]42X,Y lgQ"{N,U j,Ȅ5fGvo&)= aw5x7H~M,DAwTBaFsxWvri W阁LȨNlf l ρvP+Es2;lHĢ5uEAi_()=B銶k*-O>u2M2E, SOF+,8 $O7"GG$O u6L7XiaZ| SqxjnJ̡G͏0K7i>  ||l cK&7LB)E0`8BP瑖Q{UA Ӥ$xΉ\ D@ˡȏ{ r Ylkf+86 nL|71cv3_H؅A܈@[Er^AⓖLZ씭N,~JH5U7=-pS0L&\ecru{\x%q+s84$2pd%8ƁHVRDl<Ŭ#r !f "rWR!GٍI^f#8r{GG$Y'`Et#aˆ( zMaRtZX" #Gr)Rz%(y9!4\3K+i12~>4ȩ25B&ɳ_Q:Z[R pcnwrAbdI8 aM?c>؁ḑl28/5f&ZhH1w|rs%c: yT;NͿgSU">mehoSzLJ@n)+8b~SmXmB3B!#_& DH rLuQs~IԼ v&i1b@sj~O~9(q Ty-ΰz$X/<61ȏ Օ 3r.,`/D0 㴿 x~1>r(/+? b"LP@Qt]8f'g"H$i>@frzA/sv,k@l"=W64L )2Y%zu{Jn/$e.t^ >`H~,آ$R&| VAC痵EI dS9,(߽wGZk-Ǽ!)^J8F=2tߝSkf.!I;mʲU:Ljk@q:KWL{uiCVB92o#-8"B&X]7TCQ,2ѝ_2ld`Øk 2ኼ\kAmVf:342Y82u ,Ѷu @ƴTҪg.{~6=VGG$~tvO1 }G?˲\" LU>+> P"yh$eL(qA&ruۃ'=^[mrf!"Q6G-CG"V#bRo&C?#s'r%. z w͔غDŽ(G)#a<*Ȅڦ<]|˥Ҭ{,IqFmEZM 6x7H~N8Q0J {9@gj߷cy1D(D^31;D#B";o5WHa^HvhqqĻyDB`:.Kɝ /4tq1Q~~E؁;kg#$O*{M pX6+dȅ* %ۑl.ꛇLUF"F*6x7H^M$L?h_{i1lMM5 ]`tO}qA=M1{Ha2 C0 giE7i 1C]LI0DB8lsr"[GG$%'g/T(Wdldlg k/by$@f|dHDMSMÎ=L-%f'皖5>+ F 2K"")>x L n|CX'㭃y ᝖K{gP=XV@w" %2${)Q iaO3f)sPNc{g?@2bLY \-("!#uy"o/X߰w^ {M'0`##gZ򀥊_S~Zi ~_=:}Epf7He9$: !&zD I]AV!<ZܒH/+Mf t~]DBp R! 2=#܎2kp/NOCG"_&zd4?&އZ .'Yſۻt: ޢcAߋ+H  `-!rwARzA^ HJIQCU9鲈_nD:G$?!H"@CtE픕Dk"?HH9!:hՍ-L!![cx>}|Rg%csFRXcRquyC(n-x7HC(H"x`m2ˆ[,h {b<[m &c= Z FjdV OٽdAH1 MXpBVEY+p{MHalVZn^YkGm*q9a}ک \{V+ZX4Oa۱FXG|IDBV.vXCו\91,Q*Rh KW:Cxe%l4BUlh(s1ؼk 6%vK("[`NFNL\^-qX@ǰC 4gWK3[V"s8<"s#6đVPQ_cG3 o;K]wW`rBv"B$}{܌3\0;[Vo?DNB i,CrL]~!):F Ac~qĻyD!,8 ཱྀ= 6pǔGGhqw;Cd-gOƝ7%- 6>%yZV`oW@#]'-A羝1`qέaqJsB .rN{~qĻyD2#aEH!OMDZfؑ#FDK񠏟kŵYo!g &q?wzwǀ@Β߳#$[^.h/8 c!I t@"QIKl^ maޑ@c9^z`1g;n$䷅xA=`Os@} ^EMdY@.Ƙ/;C?{qwv?gȻ߭9 Q`Iü~ %-;QAFb^Xs#%Ԣe%?1W$D:Vho ‡7zDOBY'/W%Ɋ_-sz:FflS{sy$szhsx`sbpIӤՅ e=@P*s@(*v y#s\w.-vV^a쾑3̅@\B3zDP+xla= hufȧ@BWrЪ&-eq΅L59O<&\O߷o9}:FT: =_H-uX8wdl3cE*d$)>S씴.Qt]$ "G$g!V B;Y1SY{j)k$EXh#7L5N6G֍0tqHDυbgSkǂ~zK{2#d$HCA`˘"E6"_87/~cERHǘDJS/0O+Fچ lf.vA$c)>^$2m]wr79m v(ׅe񸒦hf G| nZY F4w!7.X Jy?tC)4{:O~J :fVskFhwزVn:%Ief># <b[n }> hs~xi$0^?EV&5d25 Bo%aN(ol_]7\Iha@6Hs祝_>hٗ jL~fEREXzÙؽ-ݐT\ioz bQ_xT\RIJdsfh!zU}VF}Q$IiMIq07 (9mHqL JD@Dc|Eg2Kj 3"LOz%?5iˇHSdnkHN HJ[e9;" mO-Z7l%Æ@轻wI?s|pU֒h ~ERH~^Ȋ40`8HS{Pn1e 7Z$Y^V }~OQ6Lc7?e(z$:|b#K$wStܘlVVC91S~e'FBʺ1 nVRx;cFS3̾R9<q^44N_e2 :M/o>c55/L]C[3w=auH,t)NLIXS&{R ۈ|f{m4n” 7kȮͩ-KZwC -:O@"d%STJH6(YK Lo.u6H{۽x HGU!щc̏8(̩-ZT:T:97j(TߺjʜgID"V$7^LOYDSGK@3D/=ΧD`NvW#H3 ? :FY{%\ԟeUҗf4V^\UL#HH2!a_~T1Jm?/k7G: 쌮bsǶy} DR]K@OkK^slI$G1_'P"slwЕ\Krr32 ҕHc%E)cbŀsYTV2ÿ~F;8A},a4-nF]lu6`2g.4ҹ^<̊Z36 sE>HPx#ߘo8 UOlϩ̭ZD!*J+mDj̮d"0i4ey֋D)d@¸("9B"[ݳ|DN􇛅>͇)vfE/xDLVmL!Cyj$ەdB+p.meoR5"caf'x}15t*uB,^CD1?lz;U7K"%e"sB}83=j\5#OR|pUBLC.̶Y>%,yZJH@&D0+6C<.;:w>o+0I+xiŌ#P֭̒4_lu NqVţ>DZZgm`fh1g3USʚ2B"7I$o NS׼)s;k54&2Pj?>9P" AD,$DBG^NfJO_uw~e'P &RaV,vdP0n//4wOf"_W^ܽD/<O",!)Fh!ܢ;1^,*%,,KN/r;Q@$}ymmwW% K0ܿ  4bɧ};{Vv#2_sbW^hLӥY2,9i&g(rsAQ3" WLO{ 㡝pҁH &r):}2^)P1C~G&n =!zIY93sdOʬ!5ٻt44WKję_GvBZ%6 5 LoÜNK"n;o)w^C,XijHv_b/?vKDxv\ܠ(B@,> < "xZz@drb_V &ry "5r>#ͬcf\MW'e^l#HQ!Hk>a/ `Jxɟeɽ^8?֖(B" &(W\tSwmAPGĬ^I՜4Z&~2[Si.@: 2k6RS,3t1hϥdC([jVAZqog B"`(:j[OǰSsy \lgO?' 0ׅ1+_ W`SCݛ룶!k"*Ȣ92qrVscuu_n7A"?0́ \w-Pl{odW4H~vEo^񃖆J | ~Y"1RtBsk άYA$|fSf'!DҢc#ӛֹuQ)Oc LxNYK&͞\pbl^r 08W<&u4,zF$8GGZ:`rvh(Q˰@RghܥKfX %Xe c4ĻWanDJϥ ?bk ,W,;9"n7# FhlYa]%-vC&1cY2=ⰻ.S#2!#H 22(-йnIEba3KM(=G<ܜ{NYmŖt"7!I ;îz+i6)u/1?Ak~_GG M2W4 wGi!\pصK&K4oIYH/ڎLꝊk}CS 2t/x7"*mˬ%"C$ !˲l.y8snӵHΝȤr9Ny_ȄcKDbŘ̄͑| l3c. ܟ:Xfv'h#7/0cm$ē5:E>sοR|m>o|Oqrq^ ]us_8Wva_|bڬب9S6D]۱~iKsfpUѲH{ENyosNDT\7v4m:u`Eq}K`v#HGR.ٜbέKۂ \ޔ{W[g^E&[>]瀮iX"Q~]f;#Hli'X諯/S[gvum(8w$ׂ-ѝ?J7p8D•`-fI&׭u䍵H&F"lHzv X']%dCYIs8d3|ubLU;=oK^h}dt-{egN@XDbmRA6􈤍ى![RF0JRZi1sx6fp}|!N؈X󷴙 CAb{Ak t^[~Fwo% pZ0@/OZEn+"e,2$DXN6bwGyb=pApcp|2IԍJB! QNJA xu_pN]bFDkLR\yDWzI: Ɛs/_8`GpdqL%c8R]/\)yQy`1ߓWW:0Iε#lZc@$dgH"Tb %Xn-tc$:HjaRݫYW9bZXL `3Gr}|$3L8׮[%㔜-2Y DFRz"6RȤ8ٺ،z䡻=z$W(5AGâmڇ CBWaRU($ԷN"պ|W]YʛuIt$ p5rDER`*)iaN-$b`a8ígr9{ԱU,n²Z  y$AXʉ jKw_ |% f^l PJX; L>6߉p=swFo܏DL_4շv஛dx`uD"r#E$L>L@ ]^HSFWB v;d#\~ۻ w6>?%J@&s8xkI4jy ?#N{L7sR]pl]6QʏIDJ &QlX&|<̐#먷~z7b(fkyU/iψۉH$ ܌<[,ߗ"Z{!Z," <8B$C_+{+n!+~fJ\%gO*{[din$ qzIcei"YBˋ(*i9t}Tyragw':P#H{,ٻٟ2[dL#茄v_4Xu'Y\;#Rz6AK *DžF&no孠!7Ms;.FA2c;9:=DLp(>{%@`'<)4 Dj}:R}jVMܬ<ƐbHDEUv#_p $ 9\NH%.G]KJ vp]Of$KPzٴh߶G @d~*y|גH>ER ߰dÌ.zԐ\L>HqBF9uN*2 JrJϱa$ba8X eӼŭK'e&#~?YqpӼlYI|=_I"<\@$K|}}LNd$no숄%Tu$*qəEЯ9Ip 9وdR42!<^-̬QiغښnDVD ZY~w!(um_0G6.(wT^\rjoï(V/xD2H0gwL|daRt7G VtgI7@ܺaO>`忝\K:")EƘIr DCQtp} ҼgSӱ؎/"@@"E vu{л0NnA"fUٔdx<-Ҫ61="˚+yǜ2 EpHOk+zDBUr:{aqf%UIڽq"J!!Жa,S_jcCmODMaqHĶk}wLZϤINuHK$5d_6έ]P:u~M*O&Ur:p5A op|ޞ9 6 I9mOn3!:Hb{J[ Cq݊N1`bVwN2ɳԡ9b$͵kHIf{P}붩/~x۞KD͌S2ئ6ư7ِ"DBY(s!H'8XJ;ŧ| ~!ę%"*mA\DXI0 eP2݌$DRl$)#)k箮j}Db@hIbXD$!St-\jvޮ{|':a]bjG%? bE$PykGOt?fI(+p5 zXI?Y'!%A8 *^$b!2#3ܬ%3_D<緜9]U!JNjOl\RK$V'Ic$3[VhgaIT`mIhIĂ.kvwՙ_Swm{oG&o Kk;]if{)BS:IU" :(o`"F7m<$r|l<ͤ]r%#7Xɀ2 oW 4v7:E$!F2#%V݃[.HN%XcR~[ێૼy 5Z&_NŃHNFgq_uoDNd{uIBY+f';a\YG[߿^9(H̀H PSpPcN&C㡓 ye8^9-JplG*7(9Ai ͑fb C$vqZ~Zخ!uNm|CyL`"{ d2%BiN]JJ[am4޳;$r)O&+_<HDb}v޳;Ƙy,|/>;%vcx%m2[Z>VwZGi>m{[eqJΣ$Ry%0HH1[CF&fRb.YJd2M×A"d# va>BF$@+j?Yɑ!k ʐbqDxɑxFSW=fH FL{fVMj|l]Tg`' $ҙD:J0-'{CE$d$'fELI췩-Yl1ڞp[0KޒչL"fa\mJRt▷xwo}/syU+j~chxj~OLapmCDD{o>mYfVD8VBC"~߿G߀4#|^x2% нc9#~ELŅh$m`gAO?"]@ըmˬ[@k `22]$n/~4Qy??c\>K"W"Y43g;ﻏ# $2B>{V' 6uѮ4b9Yb%# ?ݻ"lf%Fd_b2[kB8 m z^ߘ3skl0sa/́oy?gf~ތ! PFcO_$ wUDt,>Aes"1^+u2iy@%/ECo$^m0:50l1\q~afǂ^{"Z7+|E.q]_6HND&d#w2 Dlor.8 `TEڦ5%V0:WSsobFۻƺ{]گ>=eٿ9n\Ъo٤nf xB M}kkxH[?$<_-:fJnHZ =+s5gH@*Uޏ.N~'ɺc};ygl5FUD13Q[ '] {:v9za}. TN#>DhlA$Wzk^zW aI"8몣._Ĉ$7cHLɵwa[xݮO1  Ɓݟfbl "dffLfzRaQ d֘ nu5\ F(VPx Dr$|'HI*Y297=$vX/rCwqp_?5Ҁ@˽6lEKF # )_g+-0K&{ "$z4A6#>-Dr::MVt@"زk:U,`UewZʺ_20fiՉvG!q~\0v vx(:•V:qv8!At-J,̃#iD(~I) B!;uj(sbpY9sCaNvy ibJNI[~۷W}1j$mvK$~D&WdE(f!7~ǖWč( &\U9,kw(__gt1NJ]mvBל7\QfIg8gD0I&=W}q$K$g=~3#G$?/ &两rI!uv1Bifq2EV5\srØj6^xHKZfSY."'dyd@9H䏒#zdM -@?9uK: Dy4 ^@$a92UvMސEʃ"MCg$EpQ*D:= _OHɯ kdBvBFJ]TЌ4f"A̤ct<| 8j\Ubd<#:)\҅TpAdZX(Lz )Q*O'EyۭdF\kO58fL1ʺC$d$EGD߶\k)̩\hMۜX'l~Eq o _ks!ɯ1 LHftP#LL~[X$ԇ@ ͳ;”NŽZX6BLpIWҊ~-)=_L )K" @: Qݭm7㏺d.L6G+B`:0 ?朵h Br@Kd'v3L_Iw@t]o۶m۶m.6׶9ksbL4;{Jד!=oStާ=vSs1lT'&/7T[VOթtMu:=} ]Zz9lB$_3|ɝY@B)ȩ4tvUm^ɘz"=Hh8v .75|UQd?i0 R2;xhy@!zz.e{S⺱GX \N SBH7U8;nainXnWSͼ|K+bk$jmo [U$_dr;|0'Ѿ 1tv%Om9d:gX$?/vЮ!\~I$b9DҌMo5f`HHdl L9c11|BJ?&%bW}93зUף~|ٞk Nw(Rno{HD/㳫]Ll6lO"4o 9 rmR>xn*!7(~uIpWj{UX ER5sy>BQÐI>2lW ;$IS>`/]f:{@(7tT4pr̞׿jP]EQ0Kk0ނ STIQ1ȅ2K!snH$M)ہ'߃ԈLvpd6wS Jw!s}%ʝb`gh^tQ 0< Z 5 q}]!a^M1Nʉ.={2D<"\I ^ P1q,[s4SN7W,L+A,>+F5x~O9?gg,{fz,G>7}ֹ6W!W8RRJ̅!!-#gOߋ $In}`#Ew')۫f*)o{k7\ ^UH1֎'Q)rtvv8JopU c9~A8 2"DF)ѻ])޽ed.p껔䕺"i~H$";RxGh  a(-P[rS!A|`΃r^NGdy@0\hzyꪔsPq(DO9;(DNS~sn]BeXB@ ᳕E B"H$ܑr޺o,0 3~*%Wqg٤6L[Rp>M.t`A@EqEpzk+n[JӋ(tj %Ժ+)*%ѳ瀹!'$-<ۃW?̒PK%!)zU%f`m_Z .UXɕC}9FJ"eD2[sYr)ְ s d!NcpD(|`Q"T  3EH25ťB;hZ b0LLL' 6g@_G rGe9@zzB,9xJQ`>pD.'iT\n>D"qBŨ#/ HrL.yU%vp;۹(c`b /GHE RG Wm, p B=[aYcrGߋI$x*>V 9xT%Z$>8¼<P,E}*RpD M hjER Z$5RH$ƮJ?P"= K%lHB=`0; P-!eB &DQ{XA)@20A T德GI<'H$^!+c%V!1GӺQ;WqiHn y0K.`@Yhkq~fI$IXDw*1[*/PޒꪣF,vLi-D"-dWHFTgٚ{qu)-`hhj+ZբA'ed|Qf鱪1rBQ""`P5]RM,1`xQ_[ѸTn "O"H&'rgBL eUq=ˇt50h)4˕=_OtXH$FYՇЇbb5w@$Ql۶m۶Cg۶m۶m[;cf9/"FFbĴWbDTV2îl{oHF-LIa <^g^[< T`/ TDCNo?-5dtP7:KR*Jf#'7;/\(ߥ_g$ޟg`Tq.FAmOW{dex|\K %/,~+&:S 3r$%zr/mU0Rx~Wf~y,;d$}0I4 G^eS''f}\Ly/`\&Q-ŇIi)/Uj9㫇ztHҙK4+f:*x*WXr /a|KL> 'Wgk yzSPݎf_#2ٱ"脣?ܯ>+裩.ҙg VW/LNmv SG,UV/pp!w @)NER+A^fv3߳cX*f~R?m@+n"L0'(X(So݈;RNf7CX?飓2j ,9+Li.XE >!CUĶT̵j&clm)Q68N>R* ClYhqpѱjt QSSQ롮,rRoUc^򺕔, |g͗7{1Z^bJb̐B S}ueoCe,FcsV뒬*46[,4:BЧbGʢ?3I3fH)ːKʣ١vC.荕19-4Ά\7tU_q^y&f-]f,PBݛ|3DZnUyyhdXȯs.S;P0ФooZjb2jC>W*_`!bZ:j"x(աhȔ*|a3>3$g_;F#wfw_]۶8nlѸQm۶m,jf:M.7g;=& Ow%tslM}!5P!ôRSYf5B\!4p3@ٳNګyai>cB@9G3w>f,B\!v/}}ʍ6( ҿI7k#Db8ȃ!{v7VX6)lTV'@!z*H7pHSCBAq41`e$gz^}u[a-!A"(% y~^ܬqgݰ14r^ro~0Y* PE !D48O~Vk1,3V'ܝs~l2:@ !ցj#6 Q$ByE,?rs&.~֨5bڝ -b&kNeb=hb;JMpqhŚ\B6" l61qbPJQ Zv+}BڹƳ7]Eѣs$B5ū=;:PpZ%DZ3? Dm !8#ue98 CuT!#InqԒZiWj#yOֿq̡jb~c.8?hH+ض$ /(dْgډp8z;Q~"Rٻ;SV51wFV"zgzQOjGa3^e" Pngxvrye~s?z+cG8qgS̩>/?/og7O[W^ESri5@c5''YT8nDDEQvF m8xn.1u$Ƙo i}b"'G(˒*QnNU)cū;WQ?7tWHvrr,%1FnfXxc^n b=$Cv48m׆zRpnKR͗-:܌[Ø{jS38biHQAn$ۣ6V#"SAbq"r"٧հp3q?2(;=@1Y2ƴR<pʣ xGNQWRƒ̧\ؽ1v|s^/GYh̢(D&^fԹ@X 1cLԘ{qgyLprb}FU;캫TZTj qs1F Yd-NOecyʗ&q" soc:f8"Z{#U;;fԵC2RnW1k"r3O>CefIʪĉ xމCJ@1wҖ1 Lf>-IўkL[QEy'eUcdRk.$.cAbIL}rq7ֻ,>zwRbD$>LN^ Y@1 cX+m<^<^pgNZs<9GR8X3mԓb.ܕ{$Ƙĝ9[}hh3X0wOQ>1殜H1G'7rq@p*OOeY.H1-ؘmLkLô&I{xG.0@ c 1rOCS;=y<ҹ G' ܔ ̼gK\B3kPOj7c 106Wd3TuP.B58_` H0&" !Q5Ha]$ƘP;k~|1;p[-FO…WAKpC:#O]AbD'fӏ!k,8[ 6>H<</0뒇l2&c XOwfb23*PC!%Hy@0ΙH1N;q_D3m.s|0ERCUTanu*RU,H;둻&cL<:ט~yP`" 4Ay穧PS@ cL~5ZQ'.,i F"L ^l /A9q4vg c(VBsKsi:K0F* ?K4{,*| )ܾAbI}HN[,gbT@.|=fڠ ܾAbV#O: u>qÝeKxxPWT %߅lFZ0SL1qce,ϱaiXtpi -(}L(`2_U%I$Ƃӌ?Ϋ˪ʡmk> [G{^@pH}=, ܖAb`="~˅s,H| xۍxq꿜l7PJck@ cn 1_>?ߘpw'=On q@0x*>)@n'o,H1(wgIݬMHYD[r=![['H1|i;Qo 'ȭ[I+mn Zs˂#@X>|;)D=)|̷fHCBiHs+lw"#pp$ܒ 14Ed;ZOkβ;7ucuj!' )a|ȭ Ot dy13c#,Iɳwxp~rI7z벿S.Q,DT*FJ-忿v߬qA190@̸Ǎh= 0*[_~AD%_n_n4Y^j#!'W.0ݜĻE[!jdT@ ΊH\cn 1FnG~b|Wo8 x IWgI θ/~7_!O3ҐޒiDQX'qJ#◀C406c3f':oldݷ7_=OC׍_'ό)] /K1޹ZvdCۀN}cmDc)|Q{xys2OB;{XVq4>g;,G]Jё*0q^^om V.01>}ADBȓ(V=OjY"E-A\>D:y}cij!?ؓJDHu["P )i$t@2׀ <06 c:|t84gw0boX̮m+[l VTRUw_3*FQʼnUD0Dּwb]U}ɉ\ _Np Se{Ѩ,c%BKD&dM9Iqi}}YJ-KH@UNdF/k $*¨}0Q|VxuDy'JT;Gpn֦7W^b ` Ȁ4Al=W}\,j>PUe xxcށ`{)y__y;0sܨJY(H<CP^ٗpesa,MHCu}_{Cb;s( "YB$b<2! V=Yo"T.CyNLlq 4:sjD{+QUX,^bF |8|( rI+*yz?DH'U˵*ꥭeY-\""S:_UK.HQ c< 7j)z~AH{A 6jx؟L z-#;'PN4n^Ny0u{w7/E:j0OܚƒwbDEN"rPp.P D8qyeEJ'&kI3NܢwnDSp7􀾪NZř^BQ{;YVs5̘M$ƤE;lT;=lf-EJUED`TV!KR>a)=* zPOwPv\hL0gHD-GBt1-zL133eڱch3V:ѐFI݇{ww}+]zPyHɼ;bշnEf6<}"TH_5{ǤF``e30/*R !w-x  #*EÓ`2;dͫҧXtUѰ[XX'=-, :+q%fX`yYowx.;NP& rP0+D$bWeabF21JDh87w+rVOXAĶqqҜ/ s)VW ;("6!Rd,#HGbE[ ?., VĶ Uk?Yd"8qWFXRP}P7 m?D0=U~@ )? [6G6H:I'{ HZ Tss %e,[Dc NM0GqczZUx>ǣs2nZNfũa$yX1wnrgy$X%XWaZ}s#W_Zfb Aí`+(qYuAWe)^-e$GN[HV1׺<* &[ZedN F.^uF˵020CP2c鱫R( F5P<6vJ[>dL?%.C%€\$>m.h$/+cX*p͕/C? .1ODt!ԭ<#Da%<> d?M`~(FL=uolsMjS4cle.Lin,1|`rc #&88Z#jEcע)ic]cC}!V 9Vq`c=cw߲ BiLԭ#Pp`e8[#ǘTAUE l6 sr^ {C4pAKKsU`7(>kf`"*xNXHnp;Q(Ez5͖Y:78ȝ7b9ӱa 7Xŀ#H<{tqqH8p`Hv;i3@:qXǓ0ʓܘi{jLRO@߲2d: pKvOF߶@$]WP MBx=CIo( kX$ŠߖdN`(/hA8塀+Hӱq ^E!ύZ qYdk2A\$2L"ъ:IXo%2'қEI2 Z=jRY=ۚc'|7z:Q\K[՝R Outɑ#$xT\b {uc@ "l2z4a !n(~DgP's4=ށ~pe \Xv_2CsHˍ.-bdR"%H̙ojX@eG%HK&9ynlh?+5v C0"&Ge lbQI2:҇D|F\}%9*a'(v8H&=R! !b JCj0(_Y<&sكMɇE⸶9-s ! MDn5 V3 .n(P9nhz0ܰ*zP"(4[ ςUx ω;WX `BnG{Q\!d,(%TVͣ.ɛO_im7v)|:M"gɝ(x$ !uM9TmFT({. }Ĩ`"܏j;z) ;p2d$@2?VI~ՑC$x Ցђ󸢯5̆?py>h#k<8k|熁J PO[ BX,(3{{[zD#L̈́|ɥcMwIbmk qK, e5f0c\PߡF!`2'nO1y_ 8PlxȘC6g. h}M7-yͲ\X|2C ݜR` ɏ-"ݪ<<8@HH]WuRP1ZY-թ&_u,gVꁩ!vyܥR@Yj gB3X.'ɡW|n6$5Jb-uIGYA,Z&l]!bɞ #6.)>ְ7KA_V\!Wu:#o=ETCDa=bXRX~;_xrZ.f BPڀ,"^ϰ{ib|8>??[f"nYbtŵ9%iiiZݡ[Gȱ>(!JHıXY翴Q"m b.R-^pZezJudšLE›[013(R., zY3|ZE::L'>IQ07-زtUcmu+UwJ@G0Gy,X JzSGԍS#)&F3/̍R TReB +1bp!t:A/@Yu]|Nk;e>l&QF3כ"jm ng[Vݤv>WrWWx2}i'B7`2z$BKjgŊ@ @gnvH"^?kjAD@jEcel UArrM ^kH$rCk()!OONT9b*_Ys*%V'مGsg"#"OWD`ھ:QǷuwLR2,\7Ö>ǐr0H0XHxi-#8~ߨ<1 .b&$'Fc>b\-d0"4 kj:@8"#8su*Uj@8K>clǐ%AG-(BLIWF;lb|X"RWs!MKr,>-W`$c䵽`(fm>/ޓ}}| ~IȘ7<9(gJ}zG\kE}A9r0y<~Wo\7{\鯽/Wtn^_8 ;;ܲ_qYA-<~,Vr8"-2;=>6D:; ܅^C^vX~W^Bt~+n2)B*p[@IyX#x_=Ͼ\uߥ*mwޭF5s`ϗp(|HsX-d&]Tb>Q 4]6vsqq_}ǯi7/sWgƇ^|n.NW~˗ J's>0/@A'; 7JVKzyp {6["K> ŧR %&OGjSWjNCZn) !QH {i X$D0IO$`{}+VZ Xe¥1_@ ܀V0u*@U{ $tk7nh=9r$%i4Vsم5X6:51 $tiq.hYbbE"JPEUVXUQõXZ@ZMX1w@%@=V[tIy:\1yd2%ʷE3oR6g>$(Wש^}-X ½@(G-*]&"  w򎺵kb9V#zMAsc^BqyZ#y9c8V0)c{zڥo}ڥr엿}ح%qH8@ z! @ ,8J*nQ+5>A Q=R4 g$"!]\KFf6Rbz @^|rO[UwUN -2C̫D…-KRlW؂lP_ cS$B"uQDS.U| Gkқ`W~?7޺y1+->97y"kM/U3poy_k_ [!+t C~4?.DV*":/W!<ȶHX=C+bu-[W8nPvNW‚aJN{>-ܢo=cI3d?!kD7G:t߻%o>#v N@0P 5KϞ_ p!ȫ-h䖜K' ghe,@ɇ\,G~ę) 4W#S7$ľC\ =fenJD6` (@8MD,u'O/@'y܌vY$ ~9RFޑ5yM._+'9 +}YJ'9IAUF%M"Z[ߥZB)U)_dH .NkJ 1ORoG(ESn~yJ%M7\f%)}/Bz2o;fXk$@ l t2 ]l]EX{f XX7k/̝Kꕹ޾fR8*KI @\5RNEV*>pF(秒D{{J:] iAAG#[P~i1!mګR}sKHt0P@"IK&7Aʽ]*;:Wyɑ3 q@#H}7y|o"Wa,Ό _4oܠ$? ;q{$7Nvx0=3/'A\1#Yh^%RR?>^E(H'8|#<dYbAl[Lj/V,텅lbjT 4+E!*K e[k[ t]Zfp<ʤ¨0`mY 2/o^ 2 / azV"ff+>9;D㶚KZf7rř 1Uq͜E]&URU¹`]2QiSnjn+R{#:<).P7juSC)d# r0@G~)(cmB ?9fM o!ȧwӝtW0R2RAD=+(1=zMDb@G)ʁU<9Z.Yll[ {<~qę,IsDx|ix(9ksA@/~&] Kj&W]- *v@,꧱8eSQ9磌 yyN.SOXFaUaIzX?rJ!o"X0<^1rT$x7p?,eZm%,"L"ڮ&u ʤC 耱 {^;|JDB @נn,T ɏSވHOWFS戡Xiĸ: #mc*)}ͣh$Jp%vWB2_dݸ2N#qZ(`rwnp$\InWCLkA1ukө=԰TCΧh(GT "kL_-nH˕5 HɖT kGm55M,WGhDz= 4 uX8BG1GZk.crliq,:cdT~"N*Ն֏KA *N~7-dllu;;N b QwM,a w2Q*CT3.ԷYPcP7*d0xh>וv#/@QmWqs!2,l}S$`B{#nqNҰlC/?[Q_k9#ȏ8HU'c<~LrUZ Sݷ^?We)! W *u ғ.Ϥh|8"3Mpc9|ȫlp [BPGK ["̓E@ / IR!+ -5gN'* +? -jٛair\_~_>sD .K$v:ǛK 5h8@!4wWqR^<1/ 2;Cqų.q, q ) kצڹ1)= S"e`} (yOG.=0v^lheCk q6@l'K] ̇yqe-LgS$?sy(ʸSU;ezyD $3DL*jWu&@|<4'$%%{y 'IN]&?(<ƑM hÜ+k<"wS 2vGFE[E JF;>]Y_me % m=G WCDf xd,L xw~8e0, \Yߩ|y%E8@,~kdqt͛څIX!PG睌7 E/Yb 9ӢNKg]P҃X6:څͫSf~2K @N PK;>7| X~-YD$dl(ELD˵h׳XRV\_]?G~#/8@t.&W&s €.[Ey9GuΒR&,D(Y$Ul%@StA>U(@VMo\0A^4%S?ᗡOǍ1}<k@"p%⑐{*l 9f`Nk\O;@S1dQJJ#~/ï t"znl85?9uҵy cVBEhdKއ@  |G3*iW̺*5pH|E/W,7+zaTK}JG"݂֥QdcS!xVv=Rx-Dt*!jKR)%l4Y?2`h^]th,R4 41qMC Y:Y SX1 ]㈬k%eLN/Q_/u +BD -P>_F\F>PYI< K%~N,\'xDWHV7ҙvג3bYAxu, [/rA7s= Ҵ074joxrC[n˘?YZ84W:\ p'om7` x܄0f-c8< 7c/9rɺVȈ7{~x;w2 㸡SXm6$yMb¿!k]BLˍط70>n~VNDb0m GByK:.-Q~/iH2g%ݴMf5b[=X٥v*qZ\5DjbavBTekfpsE(9' OrX9r(2]H̹od~}I683 X[>{Į|Cݐi79*Έ1&a̠se䉈pֈKD} !) u<DIAMdfx/ nT2 A/UŽUJIF@PT1P }zwV7斔K9QjtWEaVuU$=[ i`.(7sPKJ8$X(xMڭ5lP8.OO0㑌q$bh"A E8Y 1Ar䌈$5!n}VטPO*~^]Stk[x D" 6 F9x=x.$YeYFXeivjxNr/ַ K=z+@K5ZZnF[ $B= ͑eNժ2CbG jd7 s d4$gCY~ Vܰ6}N.sty$P"2 dd~?,#iS9#G8FN׮/dcu|r>3Ӂ+h~]vukaNep2lFPpA do2XykSFQPnн|%{lvEոLOy(e%% =9@BISqc[\t-W(?u&J(huWi* B ((iYz,6pr.Ms7~)oF:DuıHܺ sO}Sp{ ncBB`0ExMuDkuSֶnNS겂R5Ƌ9PfkceKX:!zDP +^[k‘T-#_=4iBָP~Ϫ![,Gᴆ#xeol\:?MFXcX>/ʖnL, ?  >8N "P9͕Gv°\Hqdǯxǰ1ewVo\5&MOfޫ>0هA&hd8-n"F E.p.%-N#ɀ)7l&+bV7򒡞Ni('Xh#Fs38L䥐EBoK=U 3DkIcp O;FcW%̕v@6jdǰp.&a/:YP^%J}j,*$f3Id#'| 7l*+?&IPD VV \ZdׂԺmO]Ҏ8xʫ?WȱFNkpA!?.CRWx9tqV5π0d-!S9, /jTk4nBS ~wD>}tC;b .-X!H$ ܭ| pH! 69sMR:B"\k) !'sIм}B KJgɑHɨ^E7i/MϾz~C|0*'ϡ_ԥfAJ?kGNPr$M )#7Zݶ4_> `@Z,gJxrH0PcJ>W[KE(+XX x0HD nZ9O%NZՐz%Cr.UAk5ރ Ij}n n$禆~A;d+Zq~sS/]qD!*r.Eʕ<22m ,#P x(HqKu >Id=NW(O }x:`[R-* >~P[*D%"FHvmEd]VEoGvGnzq,3d04Iowv,;yF8~!ʁ~̓_A} $ir4TO鍗xWH.-&CߜIG5Z(#:‘yUDPju ?<B:Zw؁b=JۑIc<1׎^pa 41<б"NRʼnh0,I5Ґ mM<$dW ],&-rL ؽ7,ߐ/̢ؓ - `Z³p72$eRR$}$74:V0@)tJҥ<́YXkw6'87LTl%i[O@2^m %>e9Ѳ'r-s*Bq7rtyf˹RY H}NV Gܤ??lW!>t̡P]`$VAt~2#x ?aDy蠔ߍK8J4Q!Rb27fn]~ <`U΂pmc xcnP !"5i"ܐI.3.9 JI8#',y\ygh$OGFAdiy DX n!S  [PX2-nc"@d9:o(T%͔1o AR1>L~VA $ɽ"/ fTCc PQuwp`XQ"A}l$,;. ̧MK8sEj @n;,x4K? .$G7@'SG 9ق_BҡeY|CVE;HMF- a' Ϫ*|epΤ$MU]` + 0*݅ˮp¾Jš*lpŝ@r ]q :3b(p!>M2T x}lJ R;i9\8Xq#VrG>\dӂ` Xa{I1=Z@3[ uhUK_{#kpX% ^'UsIB $ZGkmڲ(Ȧ8j KA=MܬhO ɱt@l4$#+h&$<,4*-?·P&1J])<=+kW~b)˕-SJsl3_WdVJߺeeNOʼ|W,-i`lQЉU:<#at8#QW) xɑc"1g~G\L1{j9^d a%hov:|!> ];_1"DŽIuqU qo0xO^x_HmsoVWs[tC?F _xB#,>uj<[%OQMrwϊ2>&hYsG= , $Q$+]gKrAe~f9 Hж͖^搼b7{ D I$G uYVQ7\p}[?{ 7ס"Vjk=*plRΦQ ܍L:,`riC"VVw`vಥuNae $sk 7D_l) %)X.3zBQT<7޲ ak궨P/%J{)ձX@i $6z46a!6k&` ၭ#* iUyɑǹ99(8 SK^wLF\(wahSj\mPPqq,\ 4;%q8Z"oȱ=zu~t: th8c?)K2S㛇^Z |c[_p&3S#%J1M^ωf?ÒßSط}s\Fs)I Hek"vn>ΈUDdp R's-Au#s o}ٚx|rwr(ݸ4I"ԅ;rcP $eV ]zR֬qׁlS[sM(da)ݒ V{Q_@OҕÓ"z/"(0.u[.]^Đȱѥhu^1 4˲-v]-g mb|>}m}w bJE,M W.o\ q-Zt9{uݨwZ]5:MzނNC>@|UHwG 9+eqA'S82d|X!O6g&[it-J^(Oߢ{?|<ؿ2}p}ǟK8r>%syznS} m/}nOn-Í2=Q^m':mVj.C5h[\sZ{H22wGW+t<Ϥ?$(։E)OrV:A~ZV:[Jz/~+ x(6}Dw7V\i`~T*k mrH'G 1xb""+_"J[! rn69nC~@_8&Sĥ+@!ZREb < \brd 5-.̇EWoF\EZC{<+U E2#Be5V@QgKiI% , .uP[ޥFS['&(&x!RiVhMp_WX,,"p?>±ƆL4E@TBR3ڵIv;HǏD]ƬpI63(y I@8rKhql£DUA0ʯl 蟨px^x}t!BVs - OgLpcv깩Tn <4 0462{ݜ߾^NHz/~ܜDD6+Գz 7t&D"bQpJ 7S!ާ(8> {=}"kbP#H< AK|A za4uQC1R[-]]vK^alxPr&T=}[uj3P_"ݕǨ1]iB!ěq[{`j5T,838E It|FHyǟ1(N(767A7_эkC4>~bUtU;VVwO`} ^@qpqx=8ӊB=DpmrXxIՀq$ NOz/MKHLCw G "+]C'.X}B5^Q R"?L(*UJ*<:VFBFE^ju VF浩 ;(Nx]¼y{{y?)TVOL(y~/h< r}ZBDaO;y9w!wkyiS.KX -a/H:37y](G=d? %FU @(pæRq$DY:,.[TxVdG~ ;^n: _mKOÁ< wSBD.޺*H6v h´ tq*GX=B:LM!;%|J?]g X}z *!z;<}ޏA` Bstr^/U-+pkC474IQF& '3 ->ҩ8:z9jTOIҾF~|pX0<3}9w˛9 K   5a.@ʣRCY_ Pqf3Y8_Յ µIm6 hCb\YݨJ-s9'{1UJM!_vU9Xe1x7Bk}HϽuG3Y̭vIT( p8d] S9.>brĩu#7ncDH E+DQ+x%\ y(%ɐ^]/Rۡh4b4-i<|. x]Cr+AP |!ܚ'*XojiJX!t}{Wxɣ[}~AT4Lk)GO Y;;&JL9JPw֐SQZ# Fm.X?Q>s&s<9wOާ"ؽWZfH&]G!CԜ=BZ#wr|e\O4(Qx( ʽ KڮEy_;ʕȑgHBƘ8yd} Ւ9AX "G 2Q~]VvjhZ*(un{?Mv9Dd}c$.V[Pw*ppFYϠگlOJC8چI{b1Z*|ru~?QO!*X%YY & "s27$HB^j{|03JLFF̯UM D̓mN?6B o\[7^*cyKrZ # B-p=]bgK?NW7/DLx5A!J/X.9M>p`ߛS3c)ue'ÅqP@q)ֺdYVXƲIO04NʭXӅsuU+\+p)iD*rI R1gpHeEAg<ǥH$䇟ܥI~P5I<_]|p/K.x+`N"$ߓʾ^] )Ԩg~Dv> lK[ 9qOrl6eW_c:+?X hB97$*nem˅j[|ׯ4s gp}GL]f Ⱥn?.WG{cZInmUϵrVq,D 5H$N8@ #W8&GpiZrߦvP onX#ŠNXj Ό[2$5w.n(9 =J<|ADģyv(Œ (nǜ  ?_N.,`zi+U ˎ.,cK``s9ɥrΕL[崓/ӧ/fnzX-&l 5b|_7@_m]oߋP6'sk}Uuk $cL,6\[W1>ޤVR`.Q4T |<.+%pd@䋸27]poxxffipj^ +CsU` ‡_&$XcYV $.5L9/A&sXU?Vqα2:B{q`厤Rޏ!kbei#* Gͣ"`S dwV(GX#N_`7BEB*wr# V y]l&~(1?7N0 Ȁֲ/FwEq<{wCHި1A;q̧L̯ѧ. "`* $%êcЉn?{(Ar \Oܰ6{H'tOnxԚgϿ, 2D\Vȡr07~?4WI\i./Np}VO6 fI>o!ךDT&G 9[kZHKPpS(6$.cb2J@`l(䙵HP.;?RZAňUrs9҉2<6AhwayeI7+)7nEa}7(D%#1Wo!E8!ꥌK\ai[dA@f؉TA +FV&jdQ2c 3C8FJ]~ɟWs(e76"-""Q 2n8PT;gIFzy}|2]aknȧl]& Sh0jiLď!"0χdsr ) NI"!T[B_]Z)}vo,WmO߻C_{R$ ƆHqr8΁˜9>} JG؀9j9:H>"PbKf٧@󋴖X-ID)E싋GX;kz#$x x=\Q>L"B[ͦ` a/}PBѹW{BadUyPr4< \Rۻ?X7KWxd%>ѽ<%k%岽)M|4wEmx'E2% ,Bw3@J %.׻KkpmF'fNt&, k_w:;}WD8@֪6% x `.{*VlJ,CKt? >5bjI>v@0E̕խ2o23 㘰{`𻪩(V20n"8+mz ʗ4 P8&=K}pQy'IAro(  Zږ\|~+qm}/ :qd_r?#54;36,.jt6ȯ#~egM#QUiw\^KI ^y \]*W@7v4Y~O,[fL&$1~efw,~Da0$YdY鑬vUݺ~7r3 XF]Jyvs RQ7dƍ4ۉc+޹xsYfw"zCt0AI@=AQ+麟Ȣ- ˿hJx 1 ;#V.I$k sl7H<A%1̊2OOLQɂ;27 X9itǘpsq /ՑYLL/uvyhb/?ݚ[箓 Ɩ++\p :z~P͙as5KZ `eѢz$Rj0."pлKO\O N%Dad"5Ur>Ӎ$]h"ϼ; xҽSb'>F4t#=p)㛯^ąc;e9LR ĨoU/`abS{ : FgϿ[㳨TVy'hS|'=H4FY}J3syX3b+ZON%϶z٭`BwڴobII= )a[Yswe oA0چ^ΊybtUY ]9Xyh/ DZ!JF*iSOCԀՔY ꪔW'X !ߩZ_[L&,Rnq@RBXȯ%zC1L$,XYo\ 9Qe:Y_WWQ^.ިWz,T`lHw  ="fPK@j/ъ=&v.|> c~L-s, ;TP <蔫mw- 90΅Hf9wT-a||h#rD*`:h BɲriM.ި<]騡K Wk}TF)dq ,}Mm=F$AK,d˨ZE;!ցte4sQlxy/Zs 婹ܻ^pnhe$i)5-4@PBTafTr(K> i20 I._t3Rv4U@MޝI@BЋH$Srk._G6ۀ/ۨYbd| KiCOGgQDy=eJޟ  ib + M2okMMZ(fwevP]jz_C1tt#F$F$*߬Vʘ.+5u5bm H{V^ Wśѱ7$8${=܇v4v٬ LOeS-,qGcx3JC$]t}zmW@4:`!n,l6UWQ[!Ym "cdlA[RJrۀ@[x-|E~˯w޹2RB}KIRZkɉ knfJ^p@he$f?ajc%dH)9hZM. %%}ulB$ 2?ZMڳZ*^e{j$A~\>VKe  5LƧ8x0z:S&pό|mN+uL./p锲Y,鄀w+t+Ǝ<&fiM@(h1Z~8v$=X~f.^cn!q]΍O٥w:(Ukxr  䒑 :'ʾ -- 2tr|$][:#e5gTR5847fjJg[썿'{o_A+Җ_ƅ^G4a^*S#g(qPgLJ,`n.KΊh~N-AZj$0@i$@;zzz%~6gai,)k|.{_ R7p}bF:8Еb]TH'&ֵj 8y U/!MDb) GO~-m6X-gQMMU78CFB{@ZH P !$ịB3dR4κWb2ڬ-Dhh8xeBs%^'  {Àv[0E%5`t!+<ԣ{VvAo^\ɹ:;a-2:Ybz't!Ju`pڭIXa9" 8|Vn ma5갬UԫETJ9cdlƯiIifRQ{ZJBOӲu]EQJ] =XXȑ+'E%uoAlD~vd)FȮNY@K\zS~O;ȸL>U^<'HK#ޮ625+=Q'Yjea-n);`#ў4.WtgYs N3=Ba B, c hg/ RDmezUN2 zRBogr $w+uyrD\NCa *LNLth ,\^]af紬D٠P"!6+AAIڈថE*y~[Mx!nW `` -Q !/L9K|:86ێJqW+ysN,eI])ʭioph]DLÈ.,ݸSs_ȱb 4T< q)7 &$דR\M)N3P_J.K]ՀFwy &^xUf8]2T'gG:ͣZͬKVVsFiv5e~ !`ܐRalpESPJ?6:ΞAV}vR>ЗR jcey2$ߝF-1ܘ]ȇ5h$!Mӈ)0qg|Ƚ$ʥ2nL"ݸ0;aťef;{P{3d#fiDkp=ps(,W|N 籃}[£$/}"$Bm$"Clzk]\2:rL1vk(h*hVP1yoz:~\)LL/-KG&ъ;E #]"D#2<s(yC)Nh`T AIY{! ohpA-\0Q@Zم; , p BL,]CqT:"bzS(Wzss95xDs_ӟSx{:0S?N@E&"_c~tvuahh7)$iO#0+Aa8NS?CGV6o7  ĭti=] =xMĕ2 vv$I9J,R]Yj'C"$Ͱlo8` 48%-=o@`%&I^}8r$^u\rW_:=L:}>YC`-vRo.p=^ ut@{[?k˲8BTqOVl2Q@7|2VN܆!""V֧VHiՎFݺtyRe|!N&*vvMF7NQ+ e- C`3Мi=[baaϟzJq)sP^Ә[, %3M'nc| e+A-d'*N<' UAHg0쪹FLh23LK ^v+c2BG H /!pOAgd{^2L9qߦ+AD7!QI~%r<4؉W]H&=-i{4ͰHl~.3$rgc8|K96&r= MK{=^*3 CMwsq_Fd3}&`ǎ`J%%-)^-v>lVx>t7rDT:Z+# x qC'{ΐz&(գT._1f2Zwe53 T*P/ b+2Ex@7҉O~M ',} $- ĕ2S`itjѺ263a(Gv}c3(ʨjd$C<5׉EuʀTbMffٍ-VMFRuonx扣 L"H%"z V\Сk%d&7֜ et!.[}T72"Ef \Ԥhh"a0Rqạ"pvbPud":n.`]\ƥkRL~\i}D))@+/|'O}@!,=R*ָ~|5}۩7G~AD9BF+>/OaG5 $H-Xd2;WJ0 (:169O}?̿TĒ}J[iYK)@i^ot4͔֊Ƚ(]gukd g]SgteϽ6.\sVThIFLD0!ҍwôe%\g f#S˿ТhK  Rk$3}ڐjI]斥2N /ݪ͵ťeNXWY"--f7ģ!1fqY˥#)x5k\ڏA8lԘQ;=le:N"*׺;3Ov? V? LE"KhP|8v> nśvl+hByZLAZ&sRrϤXZY#zߨNB#..q9P]h`l g{qP? '<%SZ=rJZ5;y`x\ylп|@A3g/`Z<*5.Zoh!2 2$CG@d)΃RaP?Dzf{a2s{& /$IDu}pжѼ8p)EQO~ܷ)I]$I\ )qi "/m;렆0KU4Lf I msUX9!˥p9̚%QXYw* &AX M|;j8>?@%a,}I RT`пvz~D-_uЁF#ThIK7ݔou+:cq(}kO9:Ԃ_0V샌;HjQD Ϟyx+HB=725tE>LiWiKI6nLޜܹ͐2ɢm,_I=&a͒X'Rkfrs'sNǥ]6 }hoi:b|jnZ֢L5tGꇐAek5Ikfj j%G 73o ec!Au/}H?7_ ;LA -!ĶoA!Zڜ3`ЃRًd2GN 5L`AY.R%"fCi&әu= D<^goʍ)bR"+ –[C2J1$Җp)fL5IOVZ䲖0Whtp{9$?ԋ\#+jd@ IsN촑[ZB^uu1Kz z zͽoFh{AA' ;rs X.`XR߫.7M 3N.m@Ѵ~jJc_ kވt:5cOG =m8wiD'WgtoJxZm0^j2+O uГIlX#aGt$01CG<Sggdb>-dKg@Zl y݌w0$"~$!zτ;%|B#T*N.aD0݀+V3=t ji$+L$\*2[T6|521?LeISQ0 A,(m*tD@\mDPSD*~ &uk'N;?sG#CH"X@sK̥[Z2rEVVa׫'3$yYMzM{-ap`_:3꒖\Wn7:uHQyu:G+VP@U M* #A\lM; $080 P) u/@KMt:%SC/zّ>ͫ9 Rrɯ-8"2YÒB+)A+ϨB(&(8ng`ECZC RD{ VYKhC+@o lGe-Hj}g#tkҒch#P(HȦ2jR1~I_ҩ\bL h/mUaHPY&ڊn =Nг`ˍX,io>2[K?c7dKTҝM{%ҿMB Uj !BhG3٥ȋ2j*d_uE"S3Kԛ$ h~E#7C8}U6X8@Au^PMI> LǪ p(V]RRqu4e5m $A)eP> fS= a0t q_@߳B]"-e`U5juN+znNѦOxbHZ%n_{M}`0 fd:pAB)l8;aI!Ͼ{Uzt_o{p&>* 'TKe] Hx!V 4tJnnkŢTpX*Uqz)^xenjfv~u%UEb$0 K ɀ .k͝|7'8> mDZKMLn: άX@>~F!BunLg8p ѨϚ2HDLNJ zi82W A݀p$/ f;])',eRm5D(]N-t|c'=Bix6:?WEN补LWg&gNЁAsjj]}ū% -rrdtbB' HbNҴ alnPG[:J,"2An*el?,ٿCD&`zC5$ ʙ 3AÌ$Iz_-dI@“EߠZ_*o $HH@K@Amt 5a2%}I$rEJvgPoee5vG?+/j #<@0 c H f CBm)t2x:ym)$e,/ (ˏZ= x_TN>q$?;J5(VrFO{ts1ω;N&SU:S4J-p]3\e(e&)N&@%B0 ¡0.pR~?| #Is N@(lt!I+e6ٗE?uߺzMvL"% ?0Z5֯C)Z"1"v:N  =wj`zsp a]SQ) p{c\"[l`oGͫ3$HK-}4m1 IHK ohn?$#>4-2f沴3)Dd*[ԏ=4 5[BW;~@NHn=$3&1g P{B3~<y~S臞D"}KDL80{X8dYֿ.#-@NFâaCsݨ7Lp$@s#rP],s">O_:O2~nĠ;_+"W5zydI~_$M`f!ww/aX?Z lJ5Xq-,e,IKShe-;n:%ltham$.g~9vxۑ;t>я;<<|0T&4 ⃕e[0M?swHLnuȫ]6,ߓ(+y@~K';D/AY}k\x4<~D_.Nl4ay,V>ɽA͌J߅=#Rzy ~_B>V#ЙY E=w K9:(Y;? R34}Ff&<¾iwgDA3/ qҳkG"G(A)W]Uo4I z c1ݿd*Et+զHyn2|C{*O'g>Ź Piѕ]٫u @qI0=N}.6hfk-È;8 ODALfq le='>9?r|q|k"1L܊h,ՑF_OjLv^:Nrkrskx4=0`7a]> և`v8J!ć?#gΔ03T^޽2}*S\" dΟ>};3~+<'iI_ 0uQ xJ~}狟3gbCn @ow'"JF# $nPMLzߐi  11RglaEdfC"~?Zble)0YzБIRAGlϱ@$^ it4VA  nPk>K Bv;|xTr$-3;%!!OG;2r)_BtO;bΝbnngLj:: !'&wT$I.M/aH?xg~>O}]Q2'ؖ%i/2Rި.muW5UroG*JfLEE}Ş>~{$:3XY7k'|w-ĜT;HМI:>)QgjD207LBH a: v MD `36#r|oD`O7l܏P(lN̊/~oj`,JjU0jvV΂P=D:g2Gd$kcF 9NWg{w }c >rP_vT(+Ě篽qKWy$#z8&jFR5ٻF"/jFzB˪#͍p9N ,H^ ǃkā^gr)A xjGث%M7fqFg(鉭} ƊilڿOaԷᰁDSk{ӝtҖǛ/XװE["Ί&7wb-&OéMTީX1G5&K~"7gͯ~K%VΤp 9l*[Xhz>wI{^x`GΎ- rdF )`ʛctV[ `]s$.t'F(0Xl<-u#Qu]rRRիgͳb]ƍyL.Cgf .z1s[Hv N}mXG{Я j=ZV <ͪ,] 4̉i9d'N@./E5GEabjeajLN2SL5|6gD^;0?WPw[7.]zs]eׯ74s^M*O>A޷EI4rYRn9\2s<›ԫ"ҷl4zo_Pخ͡FD~!L:3=AQ:v@LKP"'] v}~ (ό:05.]@zm~/hN2,Xn60lOby`񡾾<-Ur3սP+#<]úA@@RYMNӣ/Sǿǎg:3mbhݝ<( Px_Jq7?v ޕ%r^W0fTʨ #M(+oY4xַGbMix$x 'p]?@lL!ʽZ MΆx!87y4m:.e'Hw`eLr5>əEvў&:?5,\[3 hZMZg>Kqg1an56&[K#}=q]ȰN3[Qee#=YP§5,O?}~L{2@J~m/կyv+ل(Ff:3SрedGÝ19-8dKtsbevɪN}R@rr9d=uޓnQmodτ ]]X-e-a1N?8L> ;: D_}ǎ}R{Ъz+_R/:.br$\1>>C6p+8g~W+̽}e)mGX$R;HwR/[h[^*+${,B$-΂`׃H7${Ci.)͓D|xɛIJ(D8&f) )A@H*\ʕk#[=]V[西R {ӕKppE=6J4mp!0%@(}qx4p'}- ,ag 4to ' am"TN!{ʕAهRolbգ2Hr A4ang >=vHK7Њ 6]Ahe#zGǎ8-ASw+==@nXg!&ـmkOb[#$y P=dYlk.\/*& S:%X]jG-])Vc3`߳EaBso \o;.$!Lgp'.fgB<ZP%1K1$O en7)L YX^A8*,Yҗa!Qe\RjEBC9U),+k"B-%u'W凞ǾUɷ -5-)Y-Ch06177G.}R5pUv9#q lWx8`j˴'qۍ) sǧun>^}w2=K`ܱBTDR4Qq3Icw8U 6 :8'Ã} 4ћ_B^VHLHЬX])%Bz@t.q x=hO'A‚FbaATeP[2PE?%@eZCueP&C3SXU)>3^,m1^(:!Kk6\2t KOt::.Oh?[{N8x4Dʑٙ+7ϾWrjDsh+# _C̰{.\yo* !n>n߀FGMCVʹ6Ӊh=m +b.mchg#ڨ偘=i0v)k#L֤.Ve(8L.Myd@{lDX)7G*$ѸtnۀiCke76 |n!ǀZ7ޖh kXT@H/ԅJFŕVj (MFc==L&ryHN4=SJ &&t }Eny)ez@C\G)ݿx{2ʔ _^Z/'sMT}"YF(e0 W";7&^4|iUoώXxquvGsp#bkĦaD:qΆU5/a77"G537/dKz`N6_ DcB+z"m [%1r#$^ 4 .-b\v{xRcixi_W]Z_8@{DۤP"W(+0a02mFfx E 917%Z.q)=bhm RYɤ:55j@+V{}Sg`{ 7`ݺvmfzz:=+=SuLȎήN V^Wg+w.pJ+odH˱A!=t4 ܴ57Q-QHJ^n| "A;–)ZӤM 4O~RɘO`_~t 7$. r.sYɗ_{[PiOoaoH?BpL Ndm4V΄tFxe&F҃_"i?N}< t ^}RdR/VOB{%Pn#S^vCqP^%%,+ 0auԇ X8È:5kN[tFU89.kmA*׶1\ ]\RYi5֧Lwk0!oBՆL{Xt'ƚm:`eWL݌oJ`>WosN/xNN1eB:/ ِl6ǾK)AU + D\쨔KiHօ2UUE._ݦa_sР;}C|/Wk=-@ҡ҆!R HR> b!fD;LFJOӣ._IN4C Ե},S7y@? (ol:@o.'P.^7ϖ4-Irl~EA72y..Je`O=3Vڷ{ie/gnpXLڍQHEș& @V .7*:υ/l4t z}ے2D3)"RcJ|>_⒗D<14^ ~ *sNf?o}}$;T`Tj3cl fi; $z?l]qT6bB>$cAM)헸xh7mx)Tvl<15\ $2$|H8IӵYWa4:X#1=3wg_zU :h`M}{$zx!!Al \f7X>z3Ek4ҕNR[a%8ހhTH Ӗ FG1:6|ħ!\#z% owӡ>`攼TF!̓t}leQLFfD+6ƸFX -48Ѡ}E-0,*ܐ/xBpkjz@K\x"rei=l?v<>*?~wkmzm ݝ5++J}|%":1PIig6W">\zbݥX5@u </ &4 cGcaaeh 7F)A7e-4NY < ~NhT3q,dK],XXfHIAYƑYʊ6hƃ "[!j"g[%j@/6\PO'̤^)zKk@F7տ"0 0*zNKhjVLFO@B2TRNjMvj3# Nkl+;3ǞqK/T 0ܾ*E|̰ ^Q;7RcK^xy>k;FLpd B Gd_Zi%z ƁncѐfI6mboͩ1Whqa̲B(G'Ncq75X;yo[7Zl Dha0+d_[r#jt-lBkoֹd1Pl,LCL.nΠQoeKGߝFTįQ=C;Eo_?Kh͕dl%aكd G0LeU!\ro5 p{ѨG(/|c:Fc_RʀV'5eMtݫ]'Vk!W3zeR (*K+܄wݿ<=5ص,C`Z\{TpߕWrdK&bd| R } ӗmKG-Ni| O<颁4&05=˥Ã]tl>ޞC I#]wc{[? ۱,fQTѐ@~>ɏ}mm)^D,'K;s/X"i9frdIj~!RʛQ5#!Mt9k73xz;?-3rWnL@/L3={#à HLG" QL6Xޒ n7֔l9VlӉ#q$2/&WT=5# 8x(>|?I34$nM0-pӲ DP*8[RYEBz; ö5]&2ŵN{zzD*lr91 :,fNx~8 Jp%XeB鎽q>7`x%풖pU5 bAY3 7=*"6L/Po6fb6%Fi̓g_>qԏ}'qAq;wY/8si GD[pA7کŲiyf7v, ktUy{[}ݦ%0'iru8JMP%`i*pʶ$Z` C,COGG;LMCx>DJ*oH5K:#??W ?ɘ4 ð]{$h{/;sssZ;!5T&VEiq$+{.wGhYP};!iO69 m C-@O;Mj _lvOe%pga6 &(U%ؠZ! |+Fqq#Ρ'X\,'srdjfqm|鼓 V#}ᓧSO臝}O>~ʌC{އ7G+_}뭩KP׫ ] $ &3Fb E-AY%é H2 jhmKH˖7N}{"TH&UHh(0 V]3ꍺT5:iY{,`8\lZcY}2XHOt M}joE'/yq|4}u >nM :L Hut5NyZ6ᰩ ) ^'fff#TWXh4je`)`ÆCs"P2bkNؑ?zС:>я/?8,[>̲L c 1,ۡ̒m13QY'nǚ-۲us#㕺+P8:?7*p7Ea@OloΦ%d969IZ !cvf3Nq-Hދ V(V*vkL7W]mfMJg-NONɳSGN{ؿGDa6!*2qHNu%jٕ+ޕ&A{5&&8L"<O\G"]_[+fp /mq`}D\_SNOlk-SD(mX9hUpO0 xޒh2 J93JH`uG!SGwA@N^8y܅DGZY -]\ryHJòO{SdhE- |4j+{ 6W١<*'߈rHf4L-lRSYB1;;I$pڳ UBw AfBn 2i.3|DCC>5ǷXө[';q"rQE"SE*$($I7 [INeR&kjLJH@J.:3lT_@1#I0\t;%@i]EN<gѥS11PGJJ: %"xkK`bW&'.A0LYx#hQP6* ĂOdC47H˒ܪUJI _sJ䊁Qb1Ll~~>%?uw$^0GEH,pDҲxCiyNsዯeV^:JZ1zիI"-[6;``É0-0Fv:(J.^6@K!&dWXdw~"2at׮gf+"O8tj!U 㮾ܡk:k#qdk{F4D!z`8ڀWǖK$$7GTUZh^49,+ uUeDJA1<7'.#sҤ|23=tgE%6 d߿I ƋGw9A hUY3,)X|2XRf)\!\Ey(wܨ_VxnVKK,NmAGHS 7*)w^жwؘV(=n7oĄ;(qZhi=ƎVZ/~{ɳy/ Bg88OQ!HBGl1L"x TxoԖtM #=Xs%zޡe3.'+qO]HWkʆ[ī*8= 1፜~пs"J̑L.0/KU?L$ٹчfkR~GQ H:2J|d{YZRŅ2|BHq_(1+ڲ, Hks;pn_`{os Y6]ng!'2Vmz BRNB$% G%j+M"]JD,E͝&bG"iEY֙;7c/Miր0qw;Ȓu{:k'hn.˲j8{wq:"RXZk&TEXJ̒,/=c=JbMo{2A |A6rD9sG3jۢV.Vi۹rնҖ=xZO`{cHeZe Ro#\ڬ)qGRR~aG!yoj#ifz5UP*I59/,8zf$d(Ӡ|tQ}l|:z <:s'dQNQIme$ .ԖMSpbqq~$ήƨVW %WHB.@(7Lwqdj<\icO~N/|tbZj B"!pL7A7ng71}<RMLVcK8zz\ F5-B\S-뙋\k׮[g/υ:*+-&LRk N<Ҽ53~ -ЯUg2DD_h!ypLJIf(GL-ܽ=1ѱ9Jxe NmEUzJ }ںoQZ-kܲ- '΍tZ>HJg+N|~:x{߱ ZN[uNow$c)-'`< SbwNZ{$V\T˩iG%LO\E;9).ޯX_A9iq`pHwJL0 *++z*,'>{鸥%0zm5xzphv;B'^ֹWJy@=p}?,lK-\ˎp坚d".MusE\ p5oGӊP$olK ON<;x=^ۢ)0 !vni$ k%NM7T;{u՚4xκJ=ݟ~vvHd-# `tٓ~为ia1|MT0W޽{IRd͂\*)\K7KGIeE[#cls 4ԣgK !lpݥuÖ' r5-R/3stERYWpDtM, S). ȴ/đ B{=EwdzHֲ)k[L jK ssNlnڪ2; 2vUuBt#ڶuؾrsvq{-䬠@arOuGЅ+틗Z;装[@$R^R`$<>,"D+qc+ڔaxhFxGg|zǦ̘7JlP@0(f-b`٥4LOLy9^vGJz7t<H+bGxZхۏ{1ر{lG:Q-mA JL@$ b tTRBfa$L# ,ﺦ!-3%x;4nphdSC] UA_ד祍͵2[pBa$)Wڹc{ߠ- /bVEP,$)0>s )gNw'&rȉO;H)e0F FJh╸>~^tBbn(8 %ݨP(R:#jibj˩UREE1ˣ2Ma~. B|- wa/.])L Ih˦&o-=H/(Pe%&kN&V/kH0 n"_fƢaceD n~}KRR|vnn%V, B}`P u'LqW,epDKuU Bp0ͯCzASh9,~NI+wEj%Vr-u yŮtrGPW]k!z{-VՀbEu7ItsI|il `J<@3˯17[DZ|_~w[ InA-": ή|HDe0%3SAӈ6&\9G(7p-%%\3@Pxp~)\p"{^^஭)p29px?7TUL64Wv"> ߺ]vV74N UYU----[zi}d|k@;]'-Cpz+1qеsѡcx!m/ֱ'BItQ I'.vp*q=؞qaz ^]MȻWVCHrY@k+TJy˥cwA&y_q.z_K i8=m㴩{WIg!/m\c.iLidlW0:&Q[].! Bmm~9X^\NXT{y,9af5<:r<lAm`)c߉{BEr𱺪5"XrՈNJ r#S߀`~)č-^AD:ڹ<iwLoҴ.bݱ.f.UR\L@gV6n) b.REb+cO~>WV{Gaɾ0!nc!ReFRu2|a{އ/1o`} Ą^UIC}%G*Յ .|>ia8T_84>[0 l1É5|k`j***_|y\>PnDļ ~&N CGG28;qgJm=R{g/وZ' 9дeIJӤ{`9X5:1oKؾy.[XT8mN,EBp Q Zdm}YYs;lw&.Eu.;D*AG$ic4G h&5O4>C~Ņ_{ܺya jb”uu^^7/v.Q0a,k"*ypq ?8-$ c`\oVļ`Jyf-E(.>rD=ȭĩ"svꬼ4S[a㎗X_W%s(%lDՈBXT'lڸri5tR"-BO<;6qRY`͂X3b74Im# _Cp:e%sCc3\WA|r |;G:ܶj֑;!Ӧ_#Q|d'恡ꇠlD6orDlS;5Eҕk\TW&;ku,B=Oyj%6~|Iױr6 LGs4 qn/BQʝA-"Jk^ۯA5mS3MN/O8<ц/Sa8Vk~Ii ϺNRB9y%~ Q+KvŦE_ D@HxRi>~->,JHk DԒ@o@^ncDŽ J x84m߲J:_ys M\{ɮvutFW"+jydtZ5 Q݃ vJKzj݄q%||iiJ(΋Ң;H 5Gp4>>fr179s'DQQV'8E cJ7OUtՔn/2s5v/p w Iƒ2(;DH9|'paԍTV+R\g'}ѝ_GlBf¾`bj=19-ǷkQoD#YkH{u)zjF1Q7|2QpsC}';κ̓)DthuZ${S| 0hxuL[+xq֪9}E9IwuUdIy*,̣Rab)-ձ%o;"0 ~}weg֑YW[^izN½?#H{dEra1ğ9?O# I @8#G_($"~5B$,kEtͩ&MPEuCPSqqft=?_KICBs9]AQn47lZ)%G!yTQ^JꩮʎTx+- `1J$,[SA~kwSmKUS)+mtUA-2D4ǎ_Q?I 2zUyю\Wyb}c}9}E$o\~Y4y"sLL jʩ!~q=v/t.PowqvlV:pVͺͩ/'U-gx?{uG34aXh 2_\ }=1(25- k\0Gb$CKb-HP9:"-1N7s !Q)<n|' u:9MWZR>^-x?[,|ܺyTUR0JEY R>98 XU²Q<~2""Q)~}5&(szpJE8QNj&3ߕR)M0l(-))A}uU7 씖i\cq9ȿc6 oj fin" :xuvYS㙙 &n04r!a`=2 _()|46#EIz- ۩OF%ydis~pyH⨄SC_6&x>f&+Tpm+4tdYEi떍"Ym0YOv1/]jՕC8yˏ^WOLV<*aK+.УN4+"|kY!FHcXR baAQvQ WJ *Xvl+2nC!gQ i'ht|rjx"z_1-JnpЃ/(RpNnRFԁ!F uMRl=P_|UÓ||}DfkDNA$f˔Q"üR  ( \7ihhՒ>`gVk[ALB ;%Qi6TQ7$TQ),G0ȊW9Sߓ-*@&&yolb$}HRg9P50!by\Dej;>X([ݽ>LaRKJp4Vb] )9T[U8~lVTMŅy˝l8U\\SۡCC4b`N?lM vDP[N._$ +7Dqen{^p#Tin0#ܪRnKXIA3Rejk**+G$I+D"XOli|;)P-vv3,ȜF1>9őVrD40g=yXC!rܽ\ξQ'ؙGZ#`U{7%E&ڀ`h 76m**,|l1h5I-A%‹I;:۱P:-`w& r1D9>#D'&Da'U50AkKHjJt<#Ǔm_1ZW[vK1/@#,"ύG.4pHgϜO9-rhqQ{r˶ Z9 XU?Ξ/uK͆a<+lUw҃(ƂX۶rH ɇa %&$[@hl<XHwPÉ0Ʒj!Q`kw̋6! _\Ww7 s|@}\x_3O{ĕȓg.;OD &΍mmު+|Gbc5a1w>_KK$ `{DV""M%Ayi ǩDEawEy",݉W[4"Y5_k!Y6B*Q71ueK:",)CiiZHrUM)VkDҨ*BWXHl\wIO=2ҒĻXU:_oBY]Z÷JH\`b4(U"Vk`8΂ImAf"9Ӳ~ L^?SgD /"n߀+MҸ6nr {xtfa]xxNYFHӑW^7<ɾVK_RU6o@`q@{`QloXQm_cѺ\D-ͯQ](a't* y`!uƦZ 5KSCxbǦM^u=yHCC Â-Rʾ@chb߉ ;:Y ݺg2zXJӗ:t`ȓ;ibrj'$k%UeT_W K-M X\ѣ @Q5xKcwwQޟ-)tlFccm޺CvMT_ڑ'ALuȲ2$E05JQSsOU44<Ȁq7Y]}RO)?FX_ÚSZ65/!y,&ybQA]NPef$E 5Kϊ'Wy%$[A/ihhƇn,ȱWNS';Qz""!x#@DR`l۰w{ = 0JsS3 Ko2oKpXG68)r8]U__3gϯSv14M44i0"߁"E#s=G.7(\[}o~ot!f[˷R2MIssb'x"H(AID%IJêakIZ,'QtCLө BxxF"3ǒlǶZ$i6 27?ՕQu{௤g[/?qJg2GP48TWSeO瞾:*N]I5*nƒ3hrbQ(!#D"',u,tE?ǯH"Gpp kL.W*BaX}=nftO”ZHzHح<}~gaN|gη'o&,(iAQIqIa?܋c湖n [3ުFfyzG'Nv[,(,TX3B$% ΀=RA쐎$I3y𘳻o@ "ɩ 6p,EK0BB1:>m}>;"DH4mń!HIixT$T&BZ7ڀ-[ʼnPQ^G5LLߒ12J?'lуص4Ljh9!т`MI} n F&-W;Jk Q$Br!* ;޷_X@0K32v}DČo:$ih|IDC874{ ȋEE%p8Nqa> ;6mhTG4þSA'7RcCO?h F,.W7^}nIT\;ɤ_t1fo|w>trA/"B-$X`Mnnvج|tSTst(Qú:jnZW=~T^VJEQ=_&qsk_yH奅$nO}%($U/OOzn-FIC6S[R|tj6 O O!" a!"BBht|F&)fU0uREw&*).pLbz< qII7!yf/_ga0 /5厣ƝCG$Xn7  +*B Z) )]e7^7^}j+RaA> XӟD"I[cg$E㲌\=_blPς U}2:tiվRwJ540,*bT@kq0=^w%seWMHy5'wvBc]|նj"+ F&ڶ* Rrz?>Br̐ B %`%QKw\^;K.]Gy [`fO]=9*(ȷ;{񰟼Δ0XW f\d]?}B' {]#Ѱ8 ץHd%0ؘlq>"Ϣ 7B@ F_Fb DhT 2awq.,)) إ xFihch!т`ݢjaV bzzrj6342a-EålXL\Nn *:؈6I,,F%de<zN ,<$h40SZ{}-*>pZ A:J,70-GǜiDx ]. (KMyTWS.ޜE"MQfx<ɂiD@ag,ih<h!K&GTzdLI)X,Nc֕tyy5w33"m eQN N묝H% 645MEUU΋QxnP#BPi_[iDFD JиBg1Tr Le@q-:j]o^l k:z 7<.l o#$!֌c>R^uUs>cn!`` A^7YIp$AHk)-Lj}٥hh-}AP~| \ &DNjNiie >FǗkǶM2EE5O&R415c9{6u1W(rVUۛ)$ +XX WmNoh44Vhxrt`%`JaF۹=oQo~ ?7kCT 8 =fQ{FUy1v+MH442Nv}B+mH4KPY}rLME6|yiB:L=ǰcq;4uy|z4 ս$:"p%`-QSg~#Oݻ/^tCL /n0XAHa"rr7?ySIDrjKC5t2^?ZTIIcquWwveѣuU[SDA}=Bi )xD+, <9D+y !4NmihȬ@U^T֔0CS?w((G{zrA6Qn8()̧E>Hc46nD',54tDG*BF1ٻiChp "u ^\7{ hZIx$ R<Xk`ktmF NNQEv ]#`3q%ڲe~*4˔|嵗,Ic{ׅ#08"cac,&XK~b E;D FIC44 $8~;~?lybն !x .6)S)919rFY(t$>%y_ݥ:! O(RZ:aV_Izzz_"}%^LBEXzo|Eo; ttt "cZfם(-OzpD=WuObXG $!Zo~#`U"tNΧF&d#͕v|\'ܙ%so;@&"-N?&FRnsڼ5 ]#pj:ƚꚧV|MM5ՕRNѝ^ '<V~Y$lS)+!K{jXYO- -$m:M nZ_3hFZaD?2 >?\ύONMF:zOSv'+6 219Mxٹv'~[1b`vEDCC F<ƜX/j&^:p$JA%ܶ;:Ǖmԋy2(lNǸ*_[ʼn 9 x,vut zzicgiQ]Anݼ ._ Cܕt*FDC# ^!@+ BLɞ\\.<6+m49=KHd @WO Je`h =ҁ'YLbքky ݽBq+XE흽!q5DC l ,tvI;u9ALn@nD*SJGD( !ht|pعr̂`444VENs 1\MM$dS8F 57CD ܾVHXs$,fR:G ⫯L,mrۃJ30Y Ջ۷mz]_mN (RW*zƪ "%&/OҒu5U5OC /ĝКg"Pp~H4!&>C̈ |ip#5mjܶyc᷾ۻ$|*[~l|}(eu𬲢b=?4ݿrRwvQIq+`/ۻI0c߶m۶mm۶m6c wߚtϓ:BF@Lڡƨ j tv tY%&&*uLMT] Us+%%sOfߖŦx1%Ofӹm㚬߶i\bW$@rjXqUO2[v#Z.a M[lLL~bKFW/5ƔضXWǻQJƛ7_{awO*SjtO>p:jw^ewD}Mޚ i.hb^u_7c6}eaEXoiU^^3"uJu qM7ϷN߱n'Oߌ4g䟟]}w内Y{,R*P.#a)ꨆɑoO=Go~xõ۲i@eku` RwlbU_y+k`$ӰV>j6\ޓpuPZizV=P_3*+%@evs寂TV  *\8iS8,^pIENDB`PrismLauncher-11.0.3/launcher/resources/backgrounds/rory.png0000644000175100017510000020532515224505336023610 0ustar runnerrunnerPNG  IHDR%ŗ IDATxCA߈m{bg<-|c۹T=ZnL3_ZiL&iUړ/khAsFj֨D:UkݛYitƧqi˺o`jH?Ä&M*i4A3!:KAenlkZƶ;^Kͥ-/ϛyiI;O{ʭʓN߫A6ir5u\n7{y?e,x#0v3efa_ߦ231Chiv8)ex%s?{&Z9¾uV:kh@c-;kp7{jb_+u`\NfuY1s1!&?%8y,g(G(uVG" @Z`@<)߯ %lJxnxpg9}gӲkZcklkA(iKG.K׻ΘёH\>V.oߑ11wHbbr2O|LJ(b,\d>⿛C%[!Jꢗ &$0(Xot+/,Րh,9?!0߆ܖдl@Y q!dH";G0 i_OAC0d-HqsbHSUy60zYgHw\nRc`b٣X("o9 11wHbb w?\h]/ؖRi6pɦk:Z'thkC 9'd{_܃n[TM08jLư5UC8t1㘦VO ! TB`hX^JɑVW)C)B !P8;p@HL\- K◧~ܒc(3)9w%dF / &&$Sc˃?Wp0o{̪3M %(7Vw8?sm4Ǥa'Ms!RPt)s{{3{ X^ca n/+>_P.%ʺ)9ts6o CLLI|xcVx3;SÿιJƒ] -2!1K[11mhƸyAi%!DE@/9zou0o$-,93U)9/kAaVn Ks±-y*l򉷂G- z=#Y,rIy`H*pk6@u}wJsƟTA1IL̀G+\~G7"Ońȹd7Grk>Jj OS%Fca,I2yyY?sLWʹmau1NqxFEV<¿oJu3LûYgffffffh18q yw(d"vWN{@㙆ϤCgpLUUBX?}z7^<o<\$x׀ W#$"=+e[gpV !AhH#=߄LJ c=4,`{ "wdzq]ur&`F m]o4_4|z4&acFq ]E#X:@J=@e"swk3˫{ZXqC9:}<ěf8vR(fߨ2۔yO/M 8z>V-[ږePR0|:@=4 ^T]8;{T! k31,Kka'g4l8AdRMG^dpZvHygM)/ ڶ Ò\5ߜ]޸SO[< m7`ivXbdMkRa ԄI,r4tFS pavF^RJu.B"pU˾@w{ni}ªUiɅG's4>{7s,j%bYfLJlvDCxS^t$<n@qx('ҿ#ܞtw? ɡ7!ћ5@gC' 6LpT-ۊ`mC08j;j(l%š 1!FU(,&-6*D,Lc:s\%;5宠Q4>eB?ڗYRJ.۲PKo7aE&] ːک s<1\Ḽ'a8 QlTza:Ж'Ge|,od~$~xl8=j+"Ilfhѫ:YSpX:`>pl Z< BYw4/0-GrBױ ;jYBn w`$vk9_}DN/8qhŢ8jOt4RJ 9 ' 3Զ7  0ۮZC ޟ%("M5;RIeU/8t{<T8Ţt쐎v"mso@מ L<ē YOh~xٔmA%h:kRݾb:JDbj?2PFEy"cLG] 'њ߉}P~O ke_$ lQ! kLg¹%L]&D8H%-xDY'ڡqI3VAH槠MGm)@AOY\ҏ,$;B:#D;w$ Jْܕ.P+lԅC^#}.WJqefP)YT<sR ܾ;/++%rψ0Ve,GcFɀG 2%mK1 cj Y')%"5ŮTKY"jOjls<\^|H<ē 4nk 0Ubt?G . ϙYê=2SJ~%x $%yp UY/Ud@8TLw",ݿCoU*SXLdiV$Ɲ&cvŶ 8n|pduUUhÂ&daH4蓆@? \6L_;?ia+cCI'cCS:)Wa gIЗ:";onxY]ߚ\` Y$ 0RIKgBK"r8X׏b*8Á݆Vs[b) u:c?lhȒ 3hܸ,"MjO< 6rрg۽x8GKG / @#`PJ&GX6,%801|f/WZ1V~2ƮCcd 2-J5:{nmG3֞GZ#pF1q}N^wt:8&AZ)EKǹ> pAр(@=A_Y\-/!w/5$$砯l~R9搲wxavu]zFY0%erEj& [`4\%NOt = U!69-fG,fixpA(^@Ű>kr0Mk:]0;=Υ -V4xFV[G|ՄzEjxt 7ԟ?9Ο* ? D%5qDwPB|Ġ)V4xa0j܎bKNoŽRHÙ& mbz#)9K^C~wO. K%u5ZEP(5N<d]}N8Ѭϯʦ-M3#>+@Iܤ~TXx@s'x?Sv1HLQцa#GQduF"3K [4rvEx,V SgBrӏ1)4MGy iB 7kYZ۶MmQa PXlEŐ9FWb p>ߴMͣFX34A iu+Hl \"9ri$ߩnFr`x0Z@3{:P p)[i@t窢5 /CoC+ x2ހ~) ]BǹWO⏽|_{ +KBLN,Vut0I]25cEuPDW[Z*JI"0'Gěi&Wvh8.-cSV&5'<': E 0BX'zӓ#E;~'MH3 \:4tX֖ $C#lxA2 nޓ##nBt7 ,Wduf8kY<O!OA0<V 9_E&J<2i+1bXY~sHS=Es|."S#yg.wer2 5z4!-̭pV:[auNRUi3x3BΘ̬d w c^>;~MP\( G6-z"ĚgVZʳ.pV ^$%#ʜo+ؙu"W7q$=Ѣn望B*,B옌0,MȬxy$hEiir#nEjQFdQ&0qkBt5oNz!>^%OdYZϺ![!/r -›D>}|7F-{B[J'CHjXC"ו5/:ɳXrސy3#D,Pcn&">xH^;7,mВ@k5G'e)mGYyq1a"Jɨc#Ț麬d xgHfmt"9>$L(|c %  jtii7o>x"]+ d]Dkj.c7cN#А=Y&H`Q%09EL(j ,.m|GoY zI AV< N fpTMJ6m-z!zCרByV&\|~(x<ao|ﰡ  " 9H0Aep)(o^8Nf;*\٥eԕ̊xg <Ǡض`oM-N`^(0O0,XS X/241t; YյQnSZS91{@pb(oBA +#Uwҡ>0< i f\z .L/Pe/c嵳SP<֗p [|U1=yVH/@ѲO0>T<1ѯ9wbDP+"˲4%,6lfiz&>%5*GM1bH*|6$xz4HȩJla߫g+y^R?kh%9CH$i Vy0|6SZ d,vvgf[-UM/DKƌe'Gr_4 !3TXOf-y܄dqa>SנV<&^VJ{i ZwWG4PNxKI`/Za:gKQS_8xj U] V,@LBOp=e3[62wq~8l-q3g>IyXo4YT +s~Aj@.~ZYOA6aL\6Dمf6v=(ֆL8kF[qddM;1{땓r(i}!opSUjÛ] pF9aȊ$m[3T#?@B [eŵҨyaq[6,foT87\94xޠrymlހv9f(ё>~ҳ.K;+Vx C*w0! d9&lɦEusfY*h.Q$4=g^S'  L3De[o`­筏-j48+B(  bjiKV`d.+צY;IqAJ'҉~>"ۉYYLI i9CB۴`*l$@Dko||cf!4\JzY2I{sKbaJuν&J pNv7n3;ZϱOQL&B/7/K)¾_<r /A/ר| 'ON { )nk@@%?bB[~``[ׅ`^F})bJ(T3,.lf4< dZo~'~ ;.aq} 0kT]+qiܿSU,$.@ Ȑ=)rHP6#L`uAw{wr 2Г== |9ĵy&!,&lz08؏" elGL6W-.²!ɍ$W+rgfE* y|㝛x+V¬Ol^m k> xx,BS)}Ϛdxu7+ {`ˢNZTjgt!T2L4Lo4u1pmH7vB k*ֹ:>@rpBN>ȓԛj> !>a-CCy)EN֑B>%}YHh6WHtEndՌZ5Սܜ^ߡn~EJT5 T]}{)t6$lwgS_gR E h!m~wtI 9O@ρ"?Ƕ}m|W3Q2T8m~S+w6'Gb%377/ !d8o6'7|P$J [NVM0u*غaJЄ`$bk%oczF,l"1ŕؒ 9RA~髻:0#qxń4ZP>|)2lR(dh4Zrz= kч5w $Q)Ғ%=31~\ϭB=ʃry.ͻKrmW>S _zBRFO`ׂZ]2g\He tۿPfC2+ ?ƢVa:- ڳDB [ o25h ! 0$<4Sh@D>h+.y"5 &VV"(C^ROd@ GpvUZ;lCxLw'񵽃)0t3Dkzi 28%BpCc3܆pi;[hC3GݔS%<(efL8fy0|:O~%X,ך'~?QN} ekdli4l-R_b޲Q\X)HOH+F&m5X"+wvŖB0 $T;Afhe)zf<(PMj7憐n,7)f<loBn#u.o_a}s; dœH 7l2Jɏiᅸb?7ԫCٸmDX\X8+֜%Phez.j=}77'SIa-H"[Es28Ps^ a2؟oЕ53" x$eOd)4u ,(Dz6+wzL_ކ C!-3g'ǜ~c* # @ʀS@`0 Hsɣ,t{x/~oyMٖ~"gPKx\YOͤb}Ż3mEi4U0b-oaCV e&fGv1 J=}ӹ8X en]XTY^EulgGYgfK5rfV5l0RC?gӋHpHFΜ5>Dх|,,B6ŀVϻŜPlx;T>71p?4ܲ?7Gdq|7cȰ3ce,f\Ax@7^b-t]Q2\;+m޵0}6\Z *\,r0.:,4+Ob;sINH>a4 $9 pz#(crg샘'LGɓ1WasG(oΨux;^>io2"!$ GG0A( +[Ηqq굳X#>3 "s  ~39\8U⠩eY"俋iLJ$ IM #>v!9"jc\ [;3f,6DF*tb(Gc ? p(oS@%/f.aߎD}Te R~f67zC޸(yWeApms5*"hpR$ zdR"8 <9`$ װ`&?43Jso!-ܽJix&S/oxTfnrf<{=|2v$aJf۝q<(Pg26/L p%pګ2L !Ɩ@S ;90 ـǽ7$7.1'6\K39tZNG0c<7 @wsC^w8Dқ'z7d|Z PVH"lKz RA[P Ƴ\Rw_Ve!Hr&LSbU Ë[W@.Adyy.G'~Wim@T܉AE9Foy( !>FNқU x-+{@=6 m*z B&-SnGaS~ᕱv*D9-LТ&M}$GMFȏEZ+T2)'( >Z0Y% 8ؓKƫ7 gႤC7+@~L@X쬠g7h0$NZ]-sV %SJ @ho ֫ -kLޘe\15;Bk {ݟ]v^'Iޢ\9#/} r]?h?e`*5 G1 L%Pq_X^g|d p!J!ߦ\2ޡfzK?2Gg{h^\'9P„3YVgA oB1pP6HƠ_a8bɱ>V8 -HDO@0̵+Ѣ h-AyfQb L:L0ug^0K>/\L߮ ,{a51̴23;a bo!%NӓӋ\ò$p `tU{jZ]%t"h]P]JpaS,l|qS{SZ!>x[!_DrPl=_`N@cC#n r gZNRC阅VO<~vjJ6bahLV|]oߝ+W2 | [C1EztN/V̮s[+"ԓ'=d-dғO1tlf_)4&z2/wefcyYVkkTYyZRg7>?rYE@3?) +eRLF,,#<8W1Ak*;o8D-"?;7MD<]f?|ñ\.fx&1>աv0nZkծZ;oDdt+!lۣ;gzR|cmbFz3jj-yx. ɹc}YNF2#mJ}O4%>xZVEڱsh@"(??onB>6|)frԐL 9^ v: 6++k%x=kwlqk`Yŵ4޹)u(p3iɢluV#=e#Q< @/ ,+:6/Aٙ0Db cѢYF0P4p&LDuI~ ˜+}bkHk%aYYs 0}Xc7m,I0^3sURY{SV"h ER5ZRtc]8s:wm9KNhamLGN(^d3@HM2!e`Ѳ ~[dZD1L93Ph0(fZ[]+W<8kA[V],՛f0ǃTxFls饓<[*YTœzK?Ss?s/&ſɱ72ӒʪL2چ,Ve~cA*ac!O)4 Q֎yx"6 Zd)3Vˬ&v>n4"YP');1/6Ʒ@ƍiEwp i*hL[FWPI|˯DYTk$koLR%H峬v> %J3?C,67'u\#7.k|0DlnVJDi!wkAׂ6#eQTY3wO/S+B ?xilm[I.WA ۰&^lRlEQ6_-E FxoFQ$H28{-k%ݺO< ?F!9 s7 03Y[Us0|Ô&_\ZeT| mU"+%]v=@#@n1P[B향x(1 dTp.}Ǝ|~^wl]7\Bc1rMaΏ]EՃ||HoDK7 [HΘ5"n_"dn 3Ń1-(}D}]wjb.z^HȲ)۶]70M!oܚ!.SpaJ_? |n yթ–5;ݙL1dĴ9evWL&R2 5%t2#GTʽ l~Gh|l&$g@ۻ Px >$7EԼI|9^[_zS %L6$Z Ǣ*%R$l4'HU! d4r(%Q6*( !Ӿ{O+rGb"1`uKr {vF".@&>QV1]Ao2 ePT:+k xH:@:aiv uVemgHɫ/ҹ u>*AKKryQZϱa ״f(߲2~7CyHXzX[>)Ϭ1t^ xI> ߶/9(̛Ⱦ"rڧ 2tp1 •~x]';]< Bq0Ćl22RoTFصE,TA-r;gOɫ$t?'}>_ZM/-M֚2E\}mcC'qw3)j6}9ŢB %1zϛkK+ΘLFU̘b,@jW^lȲ!܃TO< yLF9S0W!1nW `ۈe16Im`RO J;ȍJ@Ia/A4]3!Eq[]uODD,lpK`䍋cқOwB0MN %HH#ө{gnɧf:[{ \ |z ;!uއq| Aɴz(Nꁰ%gzh]+?2R6Y rKr'@eh\16JI=iZ'qs#3@A$"^cb!Ia@L0ub?N{5&1\]X@HDj>ȍ(ÿŔ> `qF>\GZpWx]ޛCiZVW+lt-BKgf}C/L !A'rauRw)_,A-ZC$en"5x r(0 p r,w7.Mc8w|* ''\-9=,K$&P?5;_qi? 2җuvfn4;"1+q{N $Hx[EIdn\RJEaFWe7V& P75u,Ɯ򱓥 k [G9[:> iwg9v7)@ط |nlSGVrM !Amaچ\rO.;Ճ+SU|s$qL" P=?>߹}oɇ$``()<{ʂ/]wڨURwBGL K|:ɠT)s\wh {fRG_Onܞu^bN*$K+2&:h! u=Bc"_8#o]\(ݸK E^ߙY[tSkR5ږR,SBC,V:Wv1;SqZkyk j>` & &RBC2Q=ķ'%M%OM~aѤ9@ ^'CKk8.~Sϋg'$ t mJA&BNIG* 3ǜmn#vK9l u5$ h }J{6&?Λ@%Z񼇤wf968C$Lɛea8k5ŅMGw`r=Ƶw'|߫'q$  "F@Ј :K<b:s8 T3S3z;O< F)uR 4WLZ0Fzr[[ʉc#yU+C+\f"卲i 2e !!4"@o_R2@tyM g#$ W.R{IdgߗNJǗpUz&}u5$P^aSQS} ֹ_Gg|1=LUƘ'sgw1ɃktzH޺4.Hhu_2*B[Is_*_ѰdF:-3_F)?Fg!ELHYVdd'u-K2; 6N﨔ֹcnf H4u#°`,ɤ0C ގDX$/.}^^:w1( !]Rx#.ZYEH<$W0 .8vvb*PȲ}$!gf1n߱'1iDIr*!xhI(TPZex o?M3"P#1;kIz>r %Bwڈ#a1{BPNHF፤[JR.U/00q]P"8.L&Arzs4I⥕5ppUQ0 /@>qP] `pMJ zl !9YHӖ(M/q?#+קy3OnObbhccy5X ggQTQ`sLpiεa kU2im"GX5 ej|n@[|?~gdeQ<!GB~1|!+Oo:iB%'/7 B8D;FrF[,6oFOt&<&+&<Ϙ @"de ^AMY`h2!Ġ  2 Wfk,|r e;)qgyf$h'9@"-H8 =&x/A8-1n>'()􋈵B*P`] /Z'\Z\RKY3 "eqaΊSU'IZ8q""a)x@ð4NOKZ1 y۶ͭR^X] 7a K8fR%,B!}T=& Nw?qh*^)rdSq>^_߀ZeZW6Y6Kbxg?+8n%38ͤ4r՛H"!z;$-%<#F3+ r:ov.yN^qC5N&pCx8rI C^xD.~ V`;HGSV?-eFDLQb1GHܴ{=9.v~JWy'LpsQ X0k+`{4~?q|D/VJ_'rT=p^YЉi{b:Oju7}?=e{3躥)MUQMdj +]kҩOߝ h㿅k75bR9C,? { I{SPQe&wZz(Hrߟ,G-s@(9\p-:䵜ͦx>9&N|q^"`9B"v<0q 35-B/4'z@ga8Ns=Dov!4ygȧ`USެJ^}0Mַ@IQf4$mlNAl{$Hzɣ\5ߚyt(Plja$c#NhpLJkh$a,XN(7NN!Wd~Vݖ"C =fb6lҰeHcC"J 3qi=s{Sϟ$`3A7ixP U)wSyd0NI󆓽=vHv{E~9͊SH| @hÐoFpB`F7)bl㒤ØM÷iOAMT@ a l#^#pB ,8^ɹ|m޽ȚQbv]o\E !B3(!8s>+ <'Q>\^^;-g*KmK9ǃOQik?xI"ϙMW +C8yK`LɱapT=N::}=9gϞ~Ǜצ鉃-م^TV|!|ֹ^ ,Nz՜K j?Lr"h/Z x2ÃbXqعfBADBytƖ'GNj-kM2USD1@p?n†>3$jhaAx2ɓw|JBKp0nKx>n6 `~ճ90;X/G~rsY+l7"ZA/ yx#܅Yz{QcCLbb_ɑG6NV,! &6P8GqUHR ς` BpP[{9f!Iܖ*\gjD_x΂[9&`y;@;s %z";Ox@2TJ b[pi4w` BeAWps$7 #CHC fᎧf ^vBo oYM o=W_x#>޺.}?z-W 0Q#hS۔,upB z|j`P >b1[etםc6h걩ѓl 3{KMrrYzG9$"]~'^hkڑ>۲ZX#97ԓݴz*Ɛ3N̈́:7mIvp.翎$& +8L \+Ql<lMq 9JN5_ʧ7D[19Wos] 6R}Bc0 Tذ ԡ0@|bC&Azp8 /!Bm KaVFkVpIMDmk?=czf!Dk\ZKggP*Q:ꢍwY?޼/`Z0$]عsr9Fߜ6`:"2 '[$!hpd6;z6kKˆ)z)ɇk}J-]XTg ּD!z!;`gqZEA!2\׾pt7oD,O2^YZZ[rri1tRs f!UqMD>'GSf|W9{R(ȅ.ɉ$ TUη7rdDO_۬2e<Y3yܸu_myy5%Sro0/ [X Ҕ2XXɂnˁJ)}[H[,-JԸ;{F8 9W147^q{߾K˫NGǶ)!g7ާd-'W+%9z*_}TmBL(_? pɕI<3;W5@r ]Ɩ'GVYx$;6.1aMLMbUț(t-(<,C.,{.k D*תJ+gnCϩ{zd"]ƻuYYGV 4 +:}N 0*oKa_n/*toAżXں,ܙ2Ĉ@ʕW[&wݒxG? C,L$ E)1U"k]nE5hQ $NC F*Ͳ F$x-3Kو[H;'jU33bQs %;lROE!-jY*K/8I ZM>,UuWTH3`p^2=N턪X9'.F|!|߃R|VT R3Ws )$:žj%Y<ҜEBoēgJl9<@h(x5#lR5,Dvk8\&&~E r#v "5ӓ w֯gkLCDR8u dmmCVm4m% ڻ_~'CŴeBŹhzkke)-1CPޡrT1 ^< ]W"zӏq3jOH_/,Vϯ~`F>ٮϖ'GRWJÏ>[Bc])Wt?WRȣhV?>LIVa d4 ̭J/3Lirm(ɇrEv(*ȕs1xDRE ZM4r,_kQ@ѕ[VB2fms_+Em<6m)%JeYy8"8.kU&2?4&2AIjE榯)vdj} m'G `]~g[Dy)Q+1 V3^|yP~\gs;m Qd'+swe1ćݭRf_,_liOeL^+I9>' zZ挌uţa$bR{Lj!P^Eolwj}\RB8QpP!< @SH{L/Vg$8b=_+nLzS0z'G6ՐmElն:"e>Iw]X(c{7)uH c㪔:>i*^;<<‹$<:fQ]16gv0^>7%df'@*җsCu k :^Ͱ/(J<0vM1+`9 ><ÀvriAmgW*{,Ɏ#z}5#(A;ݓUx9\m1|Өw?Oo㨄U +Neg1%y^,&'vΨmS//-4ٱZtu'_œī#nYT82$3/us@= X ƄiټX^SiQǍ;XMe7wίDꭶ\ &ل`#u{s5Qb@ dP ?*,ÃjkG SrK/IuDOҨHuuFܽqڪPVRG-@<<,t  j-A{|{*C7nJ__AfT: ivX!G4Tn}a3G5Ybm:]U!>X!$ _|#.7z =aΟ;s}R2sIFȇܞ[Zh(9~gld!d!O˰2WǛ|E$^X=PV>+nIiH^jyպug6 # ۖڏXpyIx%uT[MzV i]v[Jp@,Z\\p%ԙY|M(JhԆ[P{ 11ARj#vUfRXkBkaw5\_Pͅ0:Q W]:~t]cN*r@dv p1{]r-o-]:[Xaϱ+aݞ^p2x#21uXcO`/gRJ"PCs$ Lߛ)r&n40gzQ9Pvb ֿ5x$2QPDqcxS4<H/"?jInݛgBcl?󨦒<Nd]3mձ[oZd4跬fb cJI>6d|H`ϔ^j$1< u@}HJ6k(/cn4=?bpIp ?j&e2b6Z;'j[fWn3UҹIG窣n7vT:3,iXE vtk)EIPVJY]\qar aMxCTBP`1=KAz}`hF.yݶ66TG7z{nq-H8œ-Hme&h8h~\\f}RuhaYF,^h?<% >Y(+#˭~z+(.zv}mY8x6Džx,pH٘QQ0e ߢňgU=- Zg.KH8|rY) K>c"azmGYN& "ϫKgڭٍ{3-_u$VO)-K%Ց3Jpr.J)NӿxH{e<En]#鐴ԇKgCʡz&/ǏM0<'|mʖ}^P+ogmo(f,"j[BC.̶;vVb(651ڌC'$l5H x'^a)g IB\W (xGM/pO0^Tlb6WPZPl#/EBG.$c|^N9'oJ'&趙E.{[//|{gZB6ža]哖_Wb(ч0YMi7" :>Bqs묈< XjGވ Z m _XGMW3$! d{!.=uaH_||pEk#/HhCf\4;T:^\|kXUnn9sCD n4W"#`_39S a6R̗?<JO![fuZ.r$ Feu1mmRb}hahK4E!K]tGu1*8xIkίuWZ-6oc[+h4МRI X) T͇B%WސLX7ϾL;2'j[΄ROPds aD._Chvqޒ/ S'O.$! ^INf!7x#Rn'G҄V $JMHrgzL#ƥTQz&sjj,#n <:kO!û`l&?It*Ѿ34M3kQ"7ȋW1>1!47>y*^'ə&N[6=Q"C {~VgB@_?^u }Ocf.N!ڤ_o3e ]Hu31hX)I9@LKa|ᄠVW67,|+<GHC=;7yҁ-ȋP"=9.3HP-0fkgR np6ɓ'P,d ܾ?.vc~igW }3?Ux!ٲw?EBK5)AĿt4ȃo+C/Xd;<ǶG"IHHƝ#x A=b[^lťX4fA'qtyvF-VNQ5ρD(d5,27H(k1)Hs-J|y:s^~mr3r7އװN_+f @)zqUwݑLRDU:BgpW0Acǎy` @#G 36~$[ԅ-K-'+ձ'Zwf~.?3v+p?7"FB}+0ub)baϤ9Ӭ-"|Nw' JPs_wx&R%\@Q{8y,j& rQ2ͩ7zPs&2qB\U"0? Ɉɟ}}׮ڝedԒ;33md5kcZO4e%KH?<^{Y,˖s h3,ҏ &qѰ1 z3"<ROx@3 nm?8T\΂h6r6 LP) e!2/<>N-^_yd{|0+~vń,[ȓX^t+PMu$3fJY@_Z_1?@#J@$7xF(p iI*Zw{ rla0Cx$H44T϶ _y=,*:[g?hD(<_5I9 W7z- יZZNVV6HƄA[>⿧L-۶X#c=蓟9's p/^$IF`Ӝg!^X?9Fm%%N<11 }v7I"tHH߶H$ l`OLy#M;r}W9haa$:fĻ!fڮ78Ԕ G';X@I,ds̩cLjM^rV&é'F< ٖ7R|6tG!3Q0Q|6*f + M;=t:!3K|~j)+tGU,M.q'-ϣ*tuفYkWorbc4cAO}O ֐ :0+ww9-*zX88Xt<Clmكb18'\Ohp*xPI*DM|&-HQ&GS]X?rG@K-:œ#QPe$<$V]'TǀK\{y,Ԍ0aK,Dže*e#NW1xMh9gw'/nFGrGB[O* a!$ZC(_4gxě]0%x2ef~5q*JCgd`U幤ҩCL&\J񒊘ժ3')g@O~dP0ň-`{@WJt:48JcO\CP2{qL1[U j(LN{o qJ*>)x7ϣï:5lZкHkŴX7R//KǤjx3 Jo1'_|4$%\Ol,@k ͑Rv&I=6΄2]QC](zp,L]d1<fp9^n̍`/DkM]|B zdR+1$ 1\{lbP-'WZ z}o=HڶmaJM<#'u:C]B{r}yd)w)$Ts戥[D¬~!m16pmڡJvfJAi3_w Yg:Ivw, ja&\:;-r?02XA5xʶae (SА|Xb ~΢*܉m+@bC#)f@#hə5ޝ3q#U"g@y>_86Mջ‰iaڭ[7H;1&t1"2#mR%JғIFHˁ ͵r?Bh&3L/Fm9aM iEB~G 4ݎ6E҇&Ka.AǶ$ٞLDD^-$E>)h*ҙ듡|˪j>'oC^$0Q1Ock@E0/TYe3Ika<\)8nnڢz4lP6j |fq -I@Nw"J f <nteHS} mgG2 /.΂~0BZ$9cCV/ҁ_>)_<3ܸ=1 z_xj BZ&Y` Geء =6Ew^*t7{Ҷ9!r;ų]4cHo$`O1XEۛfT<U0:]cI(n؈7Ŷ\=+z3{7+Hq8b<fԐ,u^XqJ4/THÆ*i;̭n#k/ܻ߈'׹?޼;W ]'7D h'("C1!)W’g¹q^ z!&\cJ 3f:3`kޘ9>$wgoȕL* ޚ]b'‘G>Z)żet%@,"fHZS~MYqT'Fd ^~29ʥ_~OD<$y΁ٖh1G3έR- -0҅\p3ocҜţMAfʍrނj\2rSѰ!Bloiz*7ڊq/|Tl^H 0wFNH5$Lx|8 }|G>y_|ﶈ||HwO} )Ee* ~+Zˢ#}YE|m=2Л!͉XXR,H/ *u9ϣlVZ%Y`"Q9TawprDoP{t}_"p$eV" |~gӇBa}핝m[v\uvyw6-R"J= 9zy ۡ>G0 'x|/9k7{~J2:x@R*>+ڡM0lÙL\Q 1q2#}WSH @]fJUb>fD7yI Q"T"pBT-e™w ]9|ޕ'fj;`B> Ǝr~BN0 y>@KxR|ȏdq!a+Z+O܃Ǝtn$JY'UndyJ.Y7;,weo:mQEO"'rc/"N@zcЯB7@ KПa^2-DXJظr E`'xU!-yɃhGn;~Sx``{xla8TrjjP{2Lƪ 7h]#O % r%Ƣ՟L&Dc~s=nx 'j׹S3ZWO1=z>>d|1 !}3Hjqftw~a 0T.iHn63!:öe &>SNё>Kޣg◉Q'$'{_E?? ġ7ʀƔ.М(!df2hO!ega.-||ٹFgB^Fxj@ vAX$( kYb[X B/Yo#(fC,Bϊ $["l6c65rӨ1Ϛτ"Ekx']y&~?Dȏ`uF+ !q۸h*bu%F-Mv;,_9NWA m˄uyDOCzTRe\~#3 6iy$= vt78W">Fc#kı ڜ&Z Ʊٹ;8ʰ3˫{Ɪ/>= @æQ9+i(=*K>ziAE#QPT(xmo\ܿ@TU:K6SU(Q,fDCg%P$i9qhL⑸hyy-eT;6OL<9åDY> Ip5^Ź$fP;oV5s.vH^=_}atZCB@^,r}q`H Ο,l1?H꺬oX87F$b~#Nzwdeܪ V -6JODFSEb8 EKT+:Js݂+`a٠]y7<lCƇZoLWDY\AxIA6&=aB["B#xE=dK$+tsȏI43>H+LJIWxL QD0br>WNWNLH>T=G/ #Qpx\H+S?/ɏdp!t%r"~W-?C_{*GR*& (},e1P]`S0[SEq z"&(綹]%Ȁ HLoܨY |DC)!(,|KqxՓ{`B Ś> *s˲T0*XA|?!V8/H7{BK6+2;"˫^i7?> R̥wm\zYA-;CM1Tf.3>fffffX"'N 3J2Ho5mɉM^ۍ=)Nʉh~ >E+[5!TuUۯԱ@}w7lEq?oB(_Kkۚy "eGy胾I|^=[ԖIu)I].m'z(*) q:," d"szMI!<8z 1dppxD㧨.Րg3tعu`eR$qћtrc4& C)=]8 PqԒQhm`GuӆU?i"Bʶ*Vx=O#VJ @2lR($ ?J.< w .eGιgC~nk!V uS<26d;(%d45֔s仃&P93XָxZ|Ҿ*,h(O"?q$5E|oaVSSڞ(rb VSvC>CPybiY^BasX_Mo@?[ml_}{|DO"&;6ml嶺.eU#Ҽ[4mem41'h߁Ίx<>-GjKIJkB>L? 3A`\lF*SZ $ŲYNy u^:kQ- э[7~钄>AsZt;Ơ͆`~V59S< QZcsʞ3Y0,9qo>Gmk-l[v\K^}9/: MMR7OLpCP'1#(#kŚ`rզ7X[JD7/X! !wr͕k?youu)MD&MRϦx ;__m,_`H[/ |T_Uz[ Lmξ@FiHo-W=[DD.,fOAĎU@HA]nɑ,rY,GJF kʸ{hiLq"g,}Ev"l$y?F$C7>'˛ON^^}yg1XN{}] r,b41GiQ0kΐ"Ub%z~y\GDGjJAF/~x'rݷv'7Xp( YOt@w0jZ\csD #"b |)=U\,jᛱzX*e~g@&L*88g(""$~mcӏnp|9#?x4BT`TGu oC]55(L6pخݝ,]q&rpqs)_j&,߾7-t)JF^eKLrQEX4NM8gr &|o6^~m kZ ȔE*ym)ތ񸤡Ypg֞GP ̥RkJRv) _XEKUJIEԀ[ MnZ~w};* D ]/S9I; 1JQ3n-HK=ߺ(˞/eSH^iRD":/̰'^H9 0)"Gdcۚ*(OFY1S5Qd`Y;h?)/uV/D4qc?|Vq+:+"qm:̘Eъ4yͷ"%:)}j\ }Wjn`Ó<ySBd %Int֎wvhÇs>/ QQue0kW[Y 1UWpk/&r,$ɮ{\<^[s=Wਚz"1RM9KKNrK*`D"N`!H>Fm ܊N$[tKҴ`'My$y=DqTfL)n@)5X;N~G?XeX}1Dfo\xfX kd#H)rö7]moikY딛5Mܞ0=5U='gwRh* z{O|gf0[ Jyp./D`&eӬ R8G"1kSL>RUea:Oњj%l}2C\܆co= i?d7_K0A8&;p =WS|]l}7 ٚ[UM)'6Ҏ믣ǿ-aW-2X9L:⹮5!\DD&gSְDv8&x-MLAT$k+Z%Y:Ru.VUONN?%iI;WKilǗH$w뎖~]wo MEWmޠPR=v?shpxI$9ʢ)%ryw Hʥ1)~6U $H6!VuU9 ‹"M/ 8S31(Vy Ɛҁ#}Ts-M1{-x8G=aWOԈW?Q< hغv-.)_;7ĠN1/0,0L*dzcQNަ' H-Ũє9p3e m9 9G%> 4)ٙ~ ` X)aTTҘmK}6YW7֔b"`+cvWeݺT) `ϮCۮX[_[Z׬zn\1aO}StR ua \ M %2RFT"^ITvWDJ.j1B+Gזڅ'|i8_D-̦^%) tk%ƀ0Y ]c@9? n1TPԷ+%k2i#(MchVyhA C-+v IHMR?0QD7ۅWx;Xeib/hS;_zchVE "*'}|Pw/H"~|$g@Wɯ}øֳ"|y$@2+~E قP)-Q\X3ȑ G[4#!-J! Mk)7bhH9~ ۮs) T*eDF }9e%qotqc.F ͦho=>eGMi['#q_ Yef!9p ~h@ePWۗj4_d\' "4b&!oNu ~lq"VY+/ #f -%/[Z)QSY6h+(!׭m"Hӎ^^} W-p utuMNRKA}]_޺JC9o~YׄX/[J+sYWVH;($z:y(37Rb._MPQ XD\ӈS[#9B>VưdR_ZXoge\Ez|ih +Fwl籓ZFDʤn-f.|S_Oox[LE> sD+OQ!uM<5s) OlOȺB-,DXZ΀jc<1߃d+_nuIS(zx xw/+fX>v^EPp U֬f]@vKGgMB+^nJ~.W' H2I۝V:~gôPy[H3)6RV,ȱJȔ4"ZT>] S|jMħ)e@ArYTv]\4\ױb0#L,c͟;\cYJa^)urY*;u4-'m[Eؘ}Co_jS_y"'uJHe eFt"UѰzMׇkܾC/drITԁLECW:Z]bZ ))bD9P_7DZ0@8AU (dؓH(Y,g;b"ރ}PkkbN )>?t IPfIȫ@e2-+ w1O*J(@ 4Bb R(++C!R]LFzq9! s e Mj'=izI;1/׏eJ[1&QezH`u+?zZE"|bdK=[78dBNYo6ܐ{t<틏XӚ /x|"z X5$8u7܌zbL"͉95;Q\Hqݞ_~S຀SK\-MmmоW\Q}xa/57_.Y묡Hq6oXKf)^كIY%/91C,4;~M[qj @Ik(.Lfh[@dR~۽O.~DQ4goz{z_hCfw!ay%8S[xi-fE A>^T9GRUXV}}}.[XU^j{L^;$ cQ> tr6c>g)bWeHz5oUG\ [*V7FKdBqкa:)|I}*F5R6,y>G%>kHdg fre%9l Um|;:>w]q]k9"6Dkg{{z"&Rʡh^bӺ5 џ?Y̷^xkϾOD=G#" JNaΩ.MyW{Bkwܕmk# "RsDl>_LQϦtrdUUc5>;a>(7%xV:B-5`ٷ[Zqnr^:4Z1Dp,nknʦZ^i>/0raS(Rѻ6v4R\ "Zhݾ}+g=H>*̳ s JmyE_J$Je̎o!`VS冒 yR7 (F])g1rf[ii]N'4%\5@BH50fGzݮcXs.Z)HE!ʂ.@kO&ß}21˟7hݼ]@`瓗)i-ĽZ׬hӆ?+zu^Ԭbc'xC't!BFo T9gcA @\0 Y>G;/ߴ~{G *)Š2dF9}[]]yFn@]{"z>Ȥx2Ė+V*P\ŇqI{HkxعmM]t/E1"ƒIc]<+Bi- _0/)-AU3S<, ,-'O $6-bjLYݥJ4b+9g#=' ,suvR pS?M Te]_5P t<@i 5P"3>?49HLv3vV`fEVӡ||SyGO~YT﹵; }]x6%/d-,3Xߟqa(|t@&iѺ5>oIҀ fm dd.,7/Ngժjfd8*89ڡ!tˢ&;'hrOlڥX-ѣՙi$ 2O#sT22T4X¼58<}b,H[_g5*mg\5Kސ1s8e Ә@?܀ ›W¹)){r<ҼǽȹS㯽}w˺=RbGo>~~6\"W;EO,7 cZkU *fIJHRD2VұJ39$ՕtMe)~595#)|ΤSxX [q ̲3aDNLzhZecʺWݕ]ڦ؝cVNvSn ,oo:Ǧ$EڍCfҜ/قˑDvR49ZR [1'"9_s ҧɳa*Hӗ奂\]Uo+7WW`  Xxd^ r*i/ZA4E^&(/ :*s+#E" ϮId_+X~e}-,xǀ0ѱ8Vұ8S܁Y0;'M ^- {nS ra}:M[xRy;X[S)YS~p*(aӮ}S;~e{] h0"1řK8䒡L.u>b\r#\xk"d3ݧF)5\jǫpgW~,YJHq}?"TK\ 4/''& )9]& L9kfc]`{lBYc}Z[ۨ05`Ƒĵ?lRg*C.[@Bs^e?5**}ʫZ!eG/\2n`linUtWQhGjjl3{kkP<7wevq] 2*+7o^"P/ X{W~돿}GHR'%I_H Q"LVZM| "0\蚘LT8I"`j!8'fʲlAkXs|˧\ufUU]ゝ>)LNS:j=!fv]{y%GBJم} U)\\̩KJq?IfƦQ-$4LiM 'o`wڹLbmaP킟 DQPu XԿ2 uoޟG1!Q' (t!(.j#i\XHQ 7|%xcHstԂtDwdOjmș_t*;n^]y” j%܈銯q5c4ze;By%hXQqm޴V5s[[8y#۠ή}ܣ Tߴ@2Fcӳ<I(*)Uګ6#MnL^N^}~R(2.4T T>\36f1MOᛌ؋*Ig] mzݺ 931H'-b;Vyton$;=0rjP-6lnL2KC\|O*9K:il-U nUKCOb )*Rc[{o]cpWDMc{go H W"G-IPcp5TDc꿹2wzSS/gڎfHMiMJڰqc5ڂ88s-{N'%\H"є Q+ixS*&t`=L\+^c XdJD%o\u׫Ҋ&)YTGgf:egVAw;m\@vKĤW~K?a~py*5ܟ;fG)>Ў^`_^, ' Wuáb }bS88Fo}ǂ5y.|@)95JQgYIKk3ȨJx餀D(0~B%[c֢FQ]UIJQLrWzOɡ1D5)l$O~IXi姞ESV>Ib8{rL>Eh]cƁ1-3.g;ׂil7;O?8Ew&S6m;|э̑cߐ\X1f )\igE`œܔgʑBE )Vk '~by}4ƀ 8` ֚?~c\Ū)2k"j"O<"ÿ׆Jg$2oi8iFtᅮkJ2iI!.@-WE(ÔuWvʝfhhv=P@mjfVɝMRWi^Z[]Ƒ^tRd1}\2L+%w);E6jdH$Vřa8Ks}}db_[)d\*V,RݤILm&bMKK"˝Y=_/$~v"\@2'ݐe(K&lr}`m(<ߦ1NKҀݭY/nuuU׮yr)LRנȔBLJ:<)} $2ME:sK[5RNƛUC`.dy;}ـ[]S)uu'^VZj-<[gERtq6 /|ԩzteOm%&#FCSI< eJ0Ĕ,7Z{Q {>LN6>[Q+N:[)SdʔEEwXf\$4(2bNwhh]i%&GR]id#ř:d4u. Lj^Н^nf_kF7TYQ WlnVupIbzm\H_ L$W{w9~{m.n I$^i\qjrb0h0TpnANѮҹ.+ a:p0rdt;y`T5k)ӡS.di_H[қE&\d9v*֭Aݸ}7|{׹瞻nιGni]v KirSޭ_޷Ww?e"zl˟JÏJ} Pi>rk;'XËf)NiG/؞:R & x$ P@ϿnQr3.w}#55KC 0}0vdo6`sV5TfX]zdBNv %ˉq.*^*ވ€)_3xP)[Yzͷ|ag@ i ۠fF}^D w9A5# +;M$kW*wV Пu0`dV@%}4<>r)ֹ\tlʨ|)v܋6ee ^=0/ٯϧ>iȳD8nV )xpOJC3-(eE`hNdeqR܂Or>44pZ ϒR+$ +?}%9t5~~2w133-|Lt <)f̒~YZI6%;q<mG-H s켇ZٓϞd2WHdAzVDQ#+_|WL4>2#G"gBAD2 ]q &֧9q}ee9h8ƩrD<d)U @,D6;F>bp*-υIծb9sQ6Æڗ0W0I[u FlAbkiR3&\!eڧG',Ft7<<ůyH"M< @kW5l8׽ҏnL&Q ZSu]͈ޭUA_9/zoG"g!"a8t] D#z4Iӥf)\!~NODI2[DfۥDaIΩ-6hTb`pP %} )y@OVG\$!Imr93 ǹt_}Kbi)sTvHJeKNp2Ԯ ߾rXl pOdA:b O ǭFB#փ2-F -;)G$\;;;m}ݝ(O-s !` 291NL lBRFHx: ud.RT(Х, l&ν>T:6ނ7);{Dw!)JnW(Q/|d;{qvb JRUr\MmYD|絥 MU|یE^@kk?&[Pu\|K?gۺFϓȆ8"BIv|LRD:ƝԹd i .ASFd -)ja3*\n.U*]H $j3B~mI# R>EM{82B̗rC$Ifb³%m]\WmfBc;L$ݼ3BWnr7o'hm/9<5=zJ)mt_X`rDbnVߙ+9SB @QEw1'ޡ.c GK!!Jt ZxzQ Gi@h=GfV񳨇\3 |(({MwT0& % ;yM]Ng5O$MF} zB^[;6M~{tNHMk}\tWu|uR(6*jdXZӪ7TOϑp̂928s._9qw;{!yqӄyШUģt6O ItڰzmͰ#A͌4ڜ*UT7r8l`o;{hle3|$Q%}߫{y2?K;GD)I"=/1Yxv 6>.]Ak>gN0}B"5]W2|ԈHRv)+[BqP\lj ! ̷.7|'i+aIdw6gݙ&?4.B ${Xz$|zawlӀFů3"!Ģ~- ΁j:?'m9E1ȑtl GWQ%ZirAµ1dlr(3%;x6cn~A&.ˑ&E%ݸ;KOmEanD?[5H ?ϏCh IKH +`Vd:&թE/#ܦ ّ y}UYHum,(ډ6*iNph G}mbQ_\+K 4Smun痽t>HY~Vщ%"`:wu]jD"N,!K1w.$(ͯ^"bovx(5\co|6\8̂!;҈J^9Ep=).ޯd8ʼn EW4el{~ڿ몹 )ZX=̗[wݺ3zHJy@==y?, kt1y%(pEZ6)ɐn>ss\ >3;_aK pP$VJQ`+ ,q'tE*VQ|*B+4t7>] A$. e͂ 32N:A'NOĵɳ;FD`{p LvH#,| UWNjn6C::1W5߯q4@vAG%O\bew:ge-P^FEV G4gk=D0D:-R =C{xϸ%x  B6+J:ݽs~I6TֵӮ.DNJ&fT M!9 6=u-6uc$%>`09"zڴ^KHqxTuPPb^(BF DGʕŢڨpD҅K戅S`8\N.wNIw@@m"2fJ,t?|RsIE NJjAbPētDHшL gtq);:q:t&?^X\>мKB/Y(luȁq0K>o۷efC,N̅T Q8/DBp Q *?fdu:b#sёEu;!`rKdiye |mT07B$cR8(6?T/gSc56S)pŠ~].S>4QSa'S$9'C*4E+ UDB/ H$:?Zx$7̜gC Ade D}Y9G֮mEs9cOTBL 0>grAH4^LͭlfVYh\ēb7d_2UչqdMW džBԘnН+gCTrÞVMdbj?h5ϸC@n0q*ӂ}|aXja'ӮvYxR{ٓX,eL̳9*$m}-e΍(\d<0{y= ڋ:돠62 vh룗WLN+/Β,^?@Sܑ6ʟZv0!Rs,00 @X5&i"fOGN5 lg\{.rVd^ 8"@ Hnb[i@/5H-͍At-.&@04!whO[I14fH,UvPNylaмcy0RqW G@MD)~[ZuOͯN-ǖ+flGN^t 3>!Ⲷ惀#"dҩ4" ?߱pa/AC/39X_|΀yPJBȁs):|\Ɨ&S`3G!&&#0cJ](΋(S[%";%.iii27`wة5AHb8:l6!7[*5;-vg4\_s$SA4)>dX";2"U2񉑤 g{qnuqe-!" km -U˫k5aG\|B(G(k^ }.-'giDFKdDk< ߥjGI}c<߇5tgC`ÖwxZXBRenMue.Ge")C[%Gُ#7k(6L6.?񵵕aydrMfڣ|n- 77jU)S: X+v'&J*VV` Iyslf`.I4/A kx.uGA(-o;cvD"/芮sst&#Y`7;)dK;&')nqJZD @ un~=(xB &Ĉ܄R`F!8j;w*x^D"5LY(X/kF]ߏ뾈ZXKYH>(+AQT?aQiaϧN~iD"!ݷW:ׂ$ދJO>x wuSMe9۴4SɄ _jL[2 -7us NK-,{"As8h6VA~]:AٯUr0z#Vr {b|3:d{8}52 aAPcwT7!zQ(@J^u"{-Vl+T_WwA'^ncF+S{v PfkN\`v_2p(Bgȫp3D0v^d +rCQ!==|Sm{!?^zy_/O1!oc@+8X ːz]uDNW!?{S%_$kcH^z2/.]CkT,6 ?;1/zc8Gb sf$c|bIFq"CHVmySST,; tY0g W¼$uTT7_謮P%&67h*p 'IAȑ߯CXH= X[ Bi@{YZEWD傡"jGP R!"Jc=B¨?(u?X_:ƵԄ+Gw>Ay(znx8Q~t"wӓujY2g!;uyVù!N6>d ?c Dj.]J|i?_"eO!}u:i6^*Ej EC`'8١hnDSZw^J}HJGmm@}]m[z[[ܑha̫h5'C CDɯP;G+w'o1:s[WLtBXN {H"D4(JTBg\H ~? lven蘄xm7úf>iY \-psxk^"_Gߣe mo~G^֣g[peu=4t0Xw?|*VQҮ^+s8yN"H_E4>1Q^-my 6-o 0]ft>9Dk&ǤG s($ɜ\8mlEbv&&Kmm[(2'G#V杈ݘ+ Cz,lb@g!&lKd؄E6ס[_v0jbttlrYץL_-`ؼӳ<M΀cฮۨGAJ:cj(A(6e|w+++,l=:O$$RM&8KH9ab>|gJaPGJv0m5_xp#Rx9ti1AO[!:S'}9؟Ul< 65e5I BކX0g9G h6rmuIf2 OΧww4sS z45/k M!NNMp̹~M$rj>o#"8 K< 2%͈e4W5ME,,E*pID4 T$KwlK3. tM(|K4R X(>_:]ʁļv6/qA"QKgu<i̾#}DB0}?Iv=twr h -~~E  G#55~K`ApBآKg0 Ыqwߙf%˪E[[+O ‘};gØU3 qKLgQ)[SH!%Ka|UUe 8ͪ]e OD#2:(ʰk%$E(峄+{$R;6^iw~W~}`7YxlX"b vC+rCoDJ.}54xgK׶~7֎퀆 0y`5(Bк WK.QQ^FݝCC??N,*`g3sEJ:;ǯzQ?Y92G"|@OqX |IyؿaQI(e'OK=DRr4׹,w SYŴ`{?(`$a>1Z92!GrQX`mu+vؗ6*lB<ۧdqTU$I]'!Xn}^7|V12Y` 3;9]v,^vwFpkwH(4,8Nr ˌ\*S6>EfyXOW',S* $N DKKk.V}šg` ɳ ̙g8NL]X<㨭zCvͶCdӱTv)Nma1u Uށ. '%Jk+RHåR ?0(L, ߆#tvWT%y]5:E5M' ɳi8`<~\K`:o ZY R,2h6LYϺVUYF/cNj{KͥivvNkbռJREV*ij`N>X<`qx L|r72EɮPP556 fC~ȂKH$ A9ۏbѹ(Θ'&] F{w'^{UJE,jPovJHuLat >!]wBPͦ"YlH ,Z>CyF= ={޽k3Hy٤yINZ3uq]35Q +!6,.ʏ5k,,N1*V8^AD:kwKRK,X"'҄҃:*֪V7%*eȄ2쩬(([ *sF;Rnm8ya&FۄPլbP;1Vy#E I)5S2.!137WqDkf]%"uJ܅@9d,o#A, тKHh(z m];Ɲ]|ʭc݄ߗ;7WP,VPbɌ BZ$3%^ *k5NÂ`ȥ$n"Fwp,7!QŽ43A:(HIR7˥i"$qvcӧie+]KX9JZ4 .a]L*IbK\׆ozĂVz5M agE.s Jqz}Aerj]J ߨ2>ZxXJ/l3RG PD$az`7uY\#9IL@4"&鏢 =069=>%Is!S,X(x">&$hQXD%}G i.]+av<8«1^SYZ`hD$"qHIt?!m!rRd,2(yB6`MŒ*> `| )ܷ74Uw=Q+qzVR<hŽ |#qD(>0ѺnJg22ݸ=BHÐ.%g*P<7!L$>"AXd)28w LRJr)U*o8jqsS D0! MqE,('A ~²o)"f`aG %(Y:I򂪪[# QNťUy{o:)[t:B9^2 B:| wrߎb Aa fdd/2&R$b ;vleM`a ̀90o KJ)gnYDKͮ8Q}O&UNaD87! ̧27 ݷ)65#2VCAHDēi.rR/{[o ni-߲H`A+/mED7(,^>'Vփq& vXfR 8T*g`QWWDp$BF29=ˢq*xOvlvPQá(aNDF8kHT~sO"X+fF)&IRJg]Gf4T,HQ:w'A+Ă%$5 5LVanAa5PT̃O:Y)^XKQP %$VqD1+ych]BGG{bQ꺏gZ*YޏH?RS ]mZZYץ NB]v)ذ/`: }+Xb!޵w'glhΨ6C,^ߜ_JL%  r"ر ^PyYX.xxn]mSyKHڒ(lʛ{QU(?߸]B4LήdĜrтǁ%$ ~^+Bbp i9rAEaz^b.cs GitzbwJq[f螣@8&u]Օ;m2\'k,v߬2٬ VjJ0od{ =.-MqދW8C| 4ʵtwtHWH\Q-XT*+WJ/u6ˇ5":d&H(WypMRZ f9'HHL_Z^yX]%Z[V" ۡb\A4R_W  ‰8ͦYYnY鿑Hfg-3%`l@K$aqNS?9IZiF% hԘLZ;<\8Wh5U^nˎV׺&{ӚZ:g:zUϯt6a=w]c@Q-,g#`VfB|4AXH LP>zvR;)Z躑gpOEl X4{ks8*욭'譩jm̼b C )t+cP'F Xt$N! $B&hFh.PFAiz+[=g;Sܤf74*9^T(Wk4kV7}T\ӓ\0f=y]k X Wc40Y&N2E:@qz.4uPamw=uu4b4Ue|*٧ަ@:r٬b$8oe'Jk]hRŀlgd}"̌y2F:FgAt&Ut=&UhgpVV1x54{=}di^F+4 FA-hMq-LS*fxVH`N%H4I yꣃ4@g<-ZZL;o}Z2Y~媬MS3)Ass3֮[L؋DPX[3++q9+i :ibX E7aѽ@75 %QʨnfH4M'xWeOr_]O}wmnF$Zۻ˅νYx xW4NFmO\KYBe$eI F꡵oa={XR[8} >ymVK8_@РkUgH˦fCP XQ[vv(7,D12> ]Go-A2ApЕt;]OwC5(Uظa/~~_Ƈ=dxJ 0 -]H{͜xңa;?<4JkFqtjm9 w8 $PIk6P>s{ۥ/j|训Տ}CՊiS|wsoCqOI_<)o`Z >@c]Nu96GhpA,'ֶ—7߳?=6>4rE^iw>47 :| ^ cS "p]'3 DLαH=z~ޟz4/e1P+p,ADFa G0Y DLgW?G?n8fui"-K=*vV"L3#JoSή"?;`0=g"~~a $t/HSF\igݶsML8ap̿x5Zqӽs|Tܥe?@?`qz'ڇV*EvAC|i><}|~~!蛝x^Ըz9ϝXXP3sxShu-R,T$r0H)jmcYk/wvYCVʖy>>O`G\}QdDb4JWQ e]p$]{cêeL!ttPU pf<Bt20T?18֦_3B8ÿ` H MQf!1BNlknZX.Hbjf c݀uIkrv.s9e77,CV$YY𩪚OZnB;2~>bxȪ%ɝ`xy}5ADAQ/}>}]װzu#c_,ʗ> Wb9a;}7XC]6>+F&y3م(ح D$n@ڍ۷l׽%ؼqlSxOz1VO-UH>z!Wa%?=61lAAm3p=bMp8\`my:Nޘ "H!FӜ3d$7X*JS`j?rͱ(ǣ+iIzA@" =L{{ 8r XK ۷n .wKkz~0m'hpÐ@":m{Eڎ L UX eeeVQ߅w[mdW#ˣg`4BA$7<D"6<29XGGsT8mttyf% s$h}8}:} +ׯm]ӨO}j)mV{p]]=XqM|۶~gh4Z E[i Il3AH0Mӂ&[P19=뚞 f"a4w]-GNcldf6ӓtpSd$এv`m۵c5x'a&yݪ P( B4M~zF AnDt`' ۶lgCE4 (C:~K= "ʲ׳w )1;w@GV PT#];>#b|4cfr܌O?Η͛72 %B$Ije<,2ƊL&[S`ڎd)e8}1{|?N蠁Ү '!ݵ#鏽W?{^312e"ͷ~{Y/n %BNBŘ ƣ/[Y_9Y\kPt]#^0D>>>~}0в+!q߀]Ά1Ŧb$Er,Z_;t3==Iby-mY, WnސN_z'wr% .]sO)wq6Ɠ:X $e7Wps!63sO\pH x%I$eof]Q,86ӂBpsa)3LZlYrb~+r Lp k7bg._y+&58y>t-MX,\X, R@=,_Z( ><<ߏ 7!*tPySJƂ` T4:b=Ţ`XΉt: 8,\2-8g2bl,XD:& ;Q.{,Ų:|w}֞dha鶻GЯ"l /EտxU 7S]<oLOӇhb =I :~~Jz7>t_p[ x1zN'M<|~"veB+4?asEcE X,j|S*nԝ֎0 7qZ|p&&;88و"0֫Z"cb+88(8Naٔ O8HObAb; LD5 Qǰ)nM~2::$s} ~~> mNuEl(KRh423 <PkJepa{Ira# T* oM9 3u_W_}uzƍ)~} %I`$ %- CC09c9~OkTN7sIl/ X ӧ.~:H$ʯ _|1"عs''k׮U##n,azv0Rq(#`ed)Lx絙dFu=^\<͊% O4ƼoaL oV;*KeVGF0WcѣxZEƭ=,0IA,L9oDDPqf:<=h,6H, D zS\.tM[l߾yxsaAR" 0J4@ tgퟷy -JӘ33q~` XzCD !2k׮W|W`۶m T( x0Z AM:BtN̲6"K=L<@fS>5;9/iX, 0z+[X,c4 %*,-g10w]Xa`5<%B0i,P{JaW.?:pX^;H,$6Ac#CCC`,D+ytf^V['V_йG{S[I<CG||lv@^FGa$1a@/E__״]%Nj'>~⳿s=~|2]#ҹJ̒#mڛ˭9N XzhD3H2l#aPn^?F4 6:ӟ}_8x>|Y>W]0]lk&Q_g=|2&-6H,R $;VeY/5GO0 Z="c-6O6?37_:~mNz^6p^2>k`936H,(s\u2Cl f J#js%zrߏy_yó| ?01t4pUm^O"t̃w rl>[׹㔙g47#~駛Z- $D>N4H=~HCMC$jVlWJ(;Tbbth̲9;7?ٳj7l37#0Z\f[*Y:Z'1yؔN+J.*;nt^eAbX0_R'$u>V$ŗ^ŸٟPA4ӌ#}UmNFT#bJ%yxɱc/x88z"hz_+˓OCb$Jk],DDK\Sa8A#t }Kq$B?0Q #2PFU~S KK< x /"qah _k^g[Y|?hݗz ՉIמAf2#_ZXxECI|slK>pGS}Jg<7=ʃ_ߏ~;Q㘵t'aeIb|a;}JFh`m$ Y$}R09)pjfD0H^sYSZmf]۾]sM Md:d2tA?d,dA]Ƥ4`yUlX,&=Ő8%"s10Byv]"=DG{3ժyYh_].LtgHKyVRb>օaz!,6H, g%HӗD0OcZl"H3nE$!AS\.<44r霉H/<{pqL,,=cb_xGQ.}}] HO)BC/w a1"cVGk ̓jJeU]Xb__-O<| W?AzCMLܐnywJd؝ag. U bmdo {|H&tEBJ Pf`9wlX,/ӫTjAtH "UZ\ZeEAbX<$@]K2Zs?H6b:#F| +Js)y9VYlX,݁֗,6qIky\ZtyRJ) 4ˊ`b Ы|vl/rIkt}6+ECc|2xJ$"t 2DT"{o_Zw.itz霓} 7H,6H,1z?~(6^߿vw=㮽䬗K+rY.(>Z 6H,8}ސq5O^U.g(0=E:o"m3 V}Ik0LCX$eb45tZY9c<;ݧ2`"u}?bYO!̓A0{h><99vQ.mDzXZ1:x")ޱAbXk[XBnܖ+OmIr?@$wޘNB}tdلwlX,6W&Sp8%lЎ1==s$do=hs zްAbX.%։Tj|o߽Xzg"^aI"`EpF:KwlX,HZ?V}2{Fu6mv,!tLFX}tS7<X\`59ǹr$ 7]7JXQ y -QTb:纲!JYe H,C'Z䦜^{/db1Ąapk9b{WCuNV'D"`{6[ &ݖhIAbX2t73-Rps.gp:°[9s1etؕQjQ`vrcIAbX4]Gmw<~|$6rP"0rs!KGu*ΣPtz EqX?bQ" iz _Sꑬ^fR*{5x Cd06[11GV]1D7f5Uoo r9 876H,azjWлQ׺߼Q}lf?\GofxŽB{Zl/.u'F"݂eetE 5@^O/ ť;{Wѷ*qg0zwrn3^?o@H+u9car^y4;Dz.?lP+.tބRW( =<22ֺN`-Eҷ iX,lɓщ'Jg0+gٟzɳJ%S !"ˎuA,%˞n?>c)j`bYOZfp Yx-~g/T&gә6? Uop/x8vHk}63uؾ<{Cm< CUfaAb- }tzd```[o~˷|Ko}GydMߴMozu8MTYW.㶡!\^,"uOv1WY] :CqIX}-@NQB""nUJ֭[;B>0`PL_vmxg*Lnںd0ZcHp<Â@!v NLo; ^5a3ِq]˅ #dHk?v~߿馛 6! !  ~{p=`Ms*>vuþQ J„v ǜvGVy#b'N(Q`S6 =Jd4YҰ X` qt]̠8[l?8H8Xx{}ؾcr"j&_Dphczm.UU|ǜ=CAKwNRDdIbbVgeBKt Qs1e/QHи^E/Kf:-A!Rb!,-i9N۶m͛4u@fUSUx2Ia!U"C; ΣjW{FqG(mA=U{/WJ`@$ Xt\ٝ{6m@CE,8_Ȇ)JfR }' &:-*8(㺎. ϩsy>2&+a;v`xx8nI+IBw>عsg<; 06XV+n)0Pf*ZR=$=HV_9# `֘uZuV;ՈXlX,uL2Avrri\ B#9 1ZΫ|ZelKٔnl:ȧS~?'Ka6@is!jť(0HM4^.Wqg%7\.#JōF@5J$l(_ǐEal&'5 9a;Y8RpEH|}>FuxLչ'XlX,5Zh1 G(Dz,wQs&zDAXw\%xw[ه:7O]1F9Ϩ2a{Cl%q8H<\/dh_jŖD&jwGFSx} b+\c eشiS,#cc>o!eZBM?g( wO.6.Ѷ1cI$ͦ0HZ+>I?ˊcĢh%^Io{HL'}IuvC#CbW1}Ws L)x~mcƘA.!V/)@60\Wa`l}o_7KJ0Ɩ0vL1hr0V"[ ΁8Dr 8=L*1_bzz'OyI$r3Q'&RQGuq{.በɆr^|PE(C.ҹk-cٹOx~ IEt'%Z-Z)V")uR !6l[>)9@ܤ7{-ɶl ʲR[8\m۶m~µqmmeݻdiWeef\3xAFT;wYCI4:QRlphT,pa juRmX&dƜY]YDD)n!/-EX0a|؄:eu4VD>X\*A;&`BŐN GRq9+d<%XDR OD$m d, LW mx-"0ds1u%gDiq{,&K{V ,N.@7bcˌp:w&7W*ퟭG.nϽzMjr: ["r|Gn~VGnÐ7ŕ#)J\9%hL L'3*x&J+pp(u<GR* y xZdeD C5zBBGɦdviMbA&( wcVK°J7|B0 !cuFCF3  ۸OM̕%^vj6@V 0>V@lm *^(! ezr´yq  8jd >gܲ:_GQ;Be++oܹ0#"+?ĕ' $1Q4*=JDCY?X2'3 B98ԟSfllwb x월X7/fhoRK#;U%YXH:vW I@x !<#D___'Hܽ{W}=_} FϱW IV#(9jx`̟P1Vԧ5; zaYPJuqDA%1NCPy3 ׹䶈O&oXZ l2%L\y=H0-L&ԅ,8o uG_8HOKP*CSE25ĂgHhtͯC>8TmL\:3D qٌ̭l2=Xija$3$_W}gBʶJa&z*A^O dōrv?mxDMϔ L:=ԗ=>$/?<4\@\c&$P7eeR#jFc$[Uq%l^k[C0|~M)PK$ݳ,L|4q;]uuu$U|;!nl0 &Z.1n!\y ]C_6-L^c?{b>wV)TrHPgP XQ5CN-R5@58-TjdPµbUjEHUؑMM+t*^VI0*+N¿3AGWa,A0 -Doo?o2$ʋ$R*0ܟ#tu©OI+'d?ވUdkUeqhE㖜wNLD i- skMđ+z;H%x#?b_>454wZH>u (oc'O~d)璓~;߹o}K_WЖ+1ӶK&#K/G ԣF{ʕ!*s9" ۟Uv" [IPѦq$PnKk˸JJ&+PMP)D 80`˾o%  yJܼyC0sTz{{_2ۼ#..l{WW6R$B1 Nswu]k^\y<cЬi_eAOuɓ_ykOLtvجPJTVfih5a*n)WM L!4.L4EZkr Y-T^hbFlzM`B/=eNG Ya5WVX,HXٹ9 Ǻ{$JoHxP'X-(<8>.W' q2^KB~. B ȱW/L^~ұcҋL,_R(hgfWih P4kmC?b$ycrdZTT Mq~du98C^19S4 Ӌ<6xW q%=eX|gsgcW=NJ+U j$E ۊՎgdy읒Ia-n r"uZM <H|#!ߡ~|A CT|O W0|o5==P VR㥸c (ԮX%-*g#q$tЀ9/2|&RyAq% / ;?[?KuC38e]> kEYWKFldzҀV7نMPњ "!qM)E1lࢅB1ao2!CAyojl|6@1 2]0VIL& Y; ^[G2$ai8s׵H,m H u,B\UԔ7@u' 6aScccr Ίyq%ZXX`+FȆű bbW8҂C&';ԯ} Ō5 8Ȩ(p3,nKOB۾)Lu"6"B=x&P8ڨ!葆YZ\9d <e!/r`@ &&x"s] –x#]9-W|Є)*"H:wrw⫂&9 QPE{oK妜|~K^0$g&{m`lQ/fh@ ˱BV &ŒB7%Tz3]z-L5iμGxoiytaiy͎0ÂN˥SQZٮ(> lJp ͞aL#6 [uժ.r5OJ^ `aSgDq,&K)k" TIЖ+HzWbG!p_>._~$aC5^_@`X9LG*yvrXl&,Y$#v GW;3veӀѣߗ.p(X$F  1ݕ@V Ӎy #rq;>wFXPJR%6h=G D\ qgLB̀tc"ar78>']<&PgQ!(z]TL%W\*?+UjE21h8 kvabYf6%)V7rDۻien%MDCp(%m)mTDR1\ɤ %wbbžBN%`Czw<"4iN↶YEe%&~l '}όETr~\0g84;*K+ӝ*@qi #)rbF*Sskr@$9PNʀ *DC*@ދ8a_VP`YʜY_mkFᒴ7+v yC6hj9#~[W\C>$ ,*8Z+bJ z3*-NHFEfRQxa&>e-2C pRrZ (ʣxc˗BT (lץV"XxxeRf֗𹁠]tF\p,=ln .XΕlG-'Q.u}ăĕH 9'Ű)pj@Ac TkV 7#) ilOFb^N5#\;yez̑)Q|4d_/|gvO xL8(>jJ :+۶ i,0/GLN$&AdH`c !QxsT !q]3U!4 $k)7 .@r轳F/G::qb??9#9z!#0z1k9%/v*@e"xԅ5 /6N$+҉nKY'!OCYle^pa,q"=s D~ -]lʲ?H0\,8BEN )83Ax,Vң\t0<-\s1)6O[v2&d~w -uǩ>56SǸ{}bgk;?Jm8h76K\93a(b@0c<x}I5ۦNEZ/us0 >Jqxv!laÿ(t!` j+jrEX-mcےzfCӠx=жW#&6K9{,0,f`x=؈2BQ\?gwGW\ 9ڐ1М g @CFC7_>)OK8> Y9ƒF 4ByygVAYZ;q@AE)|0x_7.0'Q i-e{2:ZsLaqn9YG~7SiO ӃΥdd&6mFL®<} <_zM/7ߤՒ*QZ듭IkA .)ι|g&% 0=dcx=P;[B4jH Q{.#1]kڿH@ϙ*u `n_;-'.e+o`-tABiZ[7d2ȍ\1-RafI $9Q p' l%4@Ŋ ^r J/BKB>ɕ>  z$Zx;s-ngM#$;A㑐ۨ/{EVG7ww88 sTc-gﲶKA>&<5o토e<ޢ'uMHYh'畯&@* j//C0 nsKR2 o+Aw C` +8w 5Ғ͹ PE<4^~ ^ fT,X&x«!Hd*T` A~8>>}Q-QZoߞBwA\D*joq$k  M+@ara& p/C6!b<j0ן\0F][L,I a%KG䤽 O$144]jՊ1HƢAlCͮ""/mdMi]ˢsM5ϓse3"J2c?% 0\q_V Kg Ɯ]X[mzxkY3 URCY 8=,#ⷳ xN CBiZ ٰxpz-z5.֗Ah4,]locR[-SvA&;\z/+JcU\=#ɈO2]QxyHv-s *" 4 Ҹ(EȬ'lņba84܃`kK;#CoDOIC"r?6ѫ|-oc ^[:ksjP xm~.>КL|io~ DzAV)x5(%H =*9~3Լ I3=vH;gc 퓘,-3` # <1Ex,fH - &6I3JuG@cc}w GJڕ AҚ,,ȫ qkH`W7J}Uh$gBp+c~lmz"4fe-wE ˜i>"d F8an%;)3 V(i:@.MT{t۬"BP7NJjXdVM~6j1!M"'ckKכ#4nU j!k[):fEnO/6wިI9D}> (Wpō _xMvF:JPN66དྷ˻k۽ΜѾaZ&%!|vQnRl;=+em2T*E0f,g (-ZL.j-m,*{Џ5<#/B?#vu}S7?pyBZ 4C=M댰HdxN.@RCZ"D{ݒ:V㯝!9F a+3+Fָ],P ^1ډ(c4 @j f&d5T_'%VOk l@sj{7ɗ⚚]NH [9 Gհ+S<贘1sQ:3l}^c&sy^qs~NDŽ؇Cm+ G]n$pV+oU `8Y;m@bů; :!֑`Ц 9 Jԁ݈a:ol"(v.3@KNFxJWT睊2gV<KB-5C}{ BЮd\Fql"Ѐ~iu㩿Em RY4؏L} q=Aԃ&Bi>_A|1WYAuSp_o !x IPvKpG2 u1Ffr hV i^C9̑7>eе[-YX. w`uK vp%^2SFcgV?]G/kH[b6COA2#K)KDLdHgl7&hq&8q*co~aBfoB{A\]>4ߞ=@!!>b%Ǡ@&Rw577Đ>5&Nv+r LgXG_`\HSL!UL1Eܝ7ȗFL$\hNz$gF@!;>C]v yR`?YYOOdA~̋Q<( d9{3mߕH]1^Kmc}`ڟ )eCQdDsFpy MV+R(lwpvSl9Ϧdc\,Ť C 'ٻѹ fmq0<9x$pĶyNJ";n)Xf׾O0H"NwF ԐEx,tW8^;J=,cm9$woys[r33/@r MKEo:ׇΟc,X]<B z&GB0248" wQLڽ ekdl?xb ؚ3GyV,#4E H\mC-Jrً#؎Zt L5ccAߤ3gZ;$<aC=:sFOL SIZ\9q$?7O҉*%" c&T޷aqrYyn/aU0^#s-)Aeq2EA1fE="7 y>ߞ | ]]P,K*\BF[V;"SۀL&Xijk?ݣ)( 즇~:{'Fr{ܱ # @0BZά1SÙŇMr`SFLpA󗪛Π/ײV_0y;~l*E,ޙl.W)x_H\dA0+,2cpkmgkx}Pz{2L#wI&؞]f(ic$(0mzn*c5{>t)hpf6O'YR鉐r <ǐ{yE$bpӐ>e6㍜9kl%a`7`ƶ2 }8d|9'~/*`(f\9qUѵY.B c0=/ygwQo^ = ~23ӡ聵) Ūm8"n=]?sm*nݴd$KI"7 ګZcˢKTOC[yo<ɱnɦb$)F yk+\Fa7.gF^۞!U%'c*ROmSɘda 2LߖoXE9l1gk ӊOT4tI"yx"?& "J@r{f)Zx 1ԛU 'cٹU_\#Xh**HMMϮ2SK|[C\Mz%PHM!>t!9"jc|_ ~RO8sk +pϭ:3MnEȋ8DNa썄A&QA/?%j&oM=kq,Q6\N/.7M(jÐJ$ SN> $!|UL +pO:}5ȽEp"3a\vAe*W3favÀIAsq<LdtOR8SQ}^ ^ Ӿ;@Hn`:.@i:[S ;90 ـǽ'ŊUFMǓj .!J|uT);ap1mg"&˼^cD[ҝL]qd2 z?(\p<U2F RA_P ݛUy|i-L6Y^ 0ׂ /n\ ^ ] 9-ӹᘊ;>>`('`2#5!'Y i@z16jz|uPh"TLzSGEE)W v(D9-LBO.JK3ۣ[ w9m1ZH ‧Ss"".Sа 63qQp.Hz 00Ўwli˼w@~4a"b]}N>AT F 7"0-.6\);3AݧƔ’=#zBk%E q+sQ1ބmYB4Cr Wcg&6vgu g!2ЛHv@bbQyC:?zpFo7 쓸@ƍWԢ#>\ýi*Ш#tDNUoP_\Z74VRڐfm MT<@cizk%Y} =knޚ7-ǟޖwXsJQj{(3^eXwcc0m1BgjڲM;:Bg̝d/oo>CHqI [NH@ ؓ:?ُv[O`٣Kb -/qNFwjhWWBL'q:!M֬ Y,k%ܽ`H7S^ܣXjx~v7}X- MZא xtrtgrO,~R3ϴ1N^kfuHa$om5+y,D.^ۋ0!Ӭw[ԍmVV wek7K^7Uӂ%G֍wn d&`d6 .~OeíB\ C/ (2ڟdOf<ƢE `hLb;ug*\_os{^ug , m_f^ xeoԾ}PGxx,:›BE3B\gh|G*+@<|R M(q.CJxPRKZVV]ؿc,9Mz2-KVQ΄ٛ~*@{"&e$ϷPi,D3P|H },=Czx( /$g&~񻮸.~hdw*@g鹊"LE7 .ZDԶl:*d FlreSiİBR'"39y,0v b1)pfH4HpRHw(Po|S?o;.y;zܔ+7'XeGOq/uOS296 O޸'Lmkүɗ,קWO;ݕ+ק>" =¢ԛu':`^jtq͏1ڭ@1'qT&҉p:l~0WqKމYhxGVU-f0σTxFlsIG*(⼜z _ |XzB-SiMge"+梔Jt:>Rn#seID<(pj>܎VX-L1,ƃ);1'##6Ʒn@֭isw"bV`h$mUИ l%li΃>%Lr.?7z=6W$cvQN'#mȃ=PJh,\*W>NcO.kV& Z Eks Wڵkg&ӝ Ӊ6$Wɭi:;ЎV)MdQDR~HV XX N0/8"h@5xγ9; >srC&ĥҚ_\!P0ִT1]~wB!1v#,엠 ڦipy4e`Y qKC@ޕ3M Gʢf4^A#,ՆGPxZ=%\ [&lx)jAKKMW+꿎E37r>db)Ief-kdS] ?FA s' 03Y[aTlm`) SMԩ@7ڪpZ"گlqw9C=l,sD![,3.A̷WY,6v呍|4 &;,ӳ s~|:2y)͑0,[N7j x̿N!Mf[P&b{"Օ $=x!S7k1M!oݙ!e{`; o";3d|ly}:Xkk!Z>ѻ3]baĴ9evW HĥP2| 5%DXG Z{ XNpeBBҩ>^#}3Rk[ "Gfdv}^F#W(1}}VM\O 22ؠh6IRh[@\ i#dTB1ga!z',D{b9w&f=[HH \>]I~-"oBe%x'm(~\D$PXl灤 +H=H7v,{<)\s9F[sqgj|KcQP$0kZ?27CyH, =-t`3kZLe{i7`W\ ¡IRȞGk {p9 CI( \*wh!k{3( fb̩v56]z6L:2զ} v=6/sX*%ݴJW._ L^1"YBf4*٬4 ǸkK _tfb`uEX7s -Wfu̧Nf0Fs޽rg%5)Ҵ 硁HmH|aD;9IMne <çwfjM8o$C!/?eym[]1H2ٯk?6]_#)lT%NH ! S|@}"NυZ <.poF>>̵zy?3PLpJ=~zwVW>7NA*+;#9̍J@v )i~Ec\=Uo-7HG?&g |d~Cbݝ4co2<˾\Hv:(+|ウ-<`dim`Kf]g2+R7哫S<$Pv4H&ED,"PΟգC&@k@1l;g|HLV(UD ^9`' 4pZd$d3bGo}o~ {mF? Ҩ)xQU+a_,8CXlMMl!ϝ7/Mp7bY.!v7ɧ_ vG.!ç&n-W\$z##*>,SB: 2 Gu(じ-^w?>&KO8އd8fЭk9O%ЄM*k̤n9Qu祳 P:RP#zSҟiCL&1׵L2 zyv8H wD9SHuoNgIyճ,L#{?6).lZMN¶qM'w~II.U6DG}?W<t\6ǂLgrjrOcFF;!P $C:N/U>`snNQp0~PMz>*ej&JR,J>W=e]>@07 ~PEkwt ? 1!P]^.==g\{/+ -Hx6;I~_B^ C ԛ&8 :1K_R/xv/OǷ1bI ksnO3+nzvъ-)LT/qN9S?Ab=)RmkvuqB&BZ\\d%(SaNibGW\ FY⥑D4ĐA+ #0JV_PG(Es| [>󘠂n/O/𳛳_|_`'a@4Lc k(*gO X[Jg'F0GQsü3 rZFc\n7{,Ej7kg]R2)D&j"n~J҄H\qdHi5 ۡ-B@!H@v& * tEAfCȄj / 80b&%epƱQ?W{?ºxFDiz}Y\9dpN4fHݒcbhcc)|@- <^ugf+wb?rl .;4!X.)fZ"8-r4Ue(kPc{"bʢgx9g%L 0aϻ{-ch]KyV$vԑ,A W nV:pVӎvh(9xib%dy*& p#^ WLyBW双PX?9?@NڌM5 -,4h) {O>+鍻2>D(<>x3 zuh9-9@"-H8 Ɯ&x/}A8-1!{M'}xtVrX jsEvPc@@j)e"əSc2 YbĜ3> Kkv k?y VMuq kM#KfG 8?zHBC@9.nnb0u d~L1Z(Wߗc,n?b2JFcr#0i\v\ wfDZ$@PQJNM H:Mx#6UjepDwҬW16pc?4@*%O @ 0b% dMp4%(kX̊ȼ)Jl?7JZkbWCr8n:?bUA|6X0k+`|B)$O&ы69qU !p`eW*2tbڞj-ZI㶏 ,2h;{qSeaJHxfcmxy,l; M;1e`00,8Гe/kܛB@nj:P ,O?=x $g69 q%56\y-n铣r(7 ۬Hh#$hSHX?Sђtmc<.@ف+p0YUΡCK0$]{i B=l| V\44EZb(<X[p(JvBaI(,8p>כwi;&PD _9+W>K3 ~hukf]z{G|L(2T 0=VqP1#@ڤb |X,p9!pbO8'>͒fk Yrҏ9Ӽ&Mװ?&;# %{Ri^FESV?_bSDL\qd:6g1BGbaAݛgƐckg}iiTRJd66 i @Fbp}DFՉhݐ)`A*X`ז{}RGʵ '/^ p=E fWI㸽B 2#a,ta#(/)qX&uP~{YW2*dD_tGvE'!e cdEmv?>l 9+.š ( +ʡD8:s| b@@4Gd 5:O{x7| T>>-զ@2KAPľX83Yg#k%/=п:/2|$Tysjг֌@m `um4+?W{qU 09p#ߛEZ/i彽PA*[P۲RGՐ_?rl3x /8/![1B c9w?(5?']?X\y@rBGQV= /\}$MmؘB0C!șgӢ#- YImۤfs1[r =Lo`l8RmZnb&J]+_2ڗ Y[1SDʁ Ņ9Tqݛ]H(zz'YۋK+b䎸↶naxv-Or8'i9‹#cd+(6fܽM4ۦTF94e¾NkKe4}l^DŽS!o'cOb3E~C*&?λ/oÄu#HCaFѾyЮ@XمUfw!tꗾY,h|ܸuW~{~ ں ]W\ PCR*A&`>Ŭ#d a(2HB"M0pÂFb!Ad="^spG ^;'{i sy\J8&\C̃]f{ipRu;S,/g] BNaa3vQ-lz*K45\C^GVf|uߗ񻨱'?e9UIv?oT.#TXXZY4}V]q=Yel Ax5Ȃ :y SlavM &@ TAӎmz(& :cUuMEH ,:ubBgܾsWW {~)$3)? {MF Nİb=Li}SfdB7%GO.7Go"Ͷ l-'RrYd~s՛rTqvnlt}-W\ i kL,rQŰ&bOY\^cQ 6RlAMඇ;|PE+9#AhP%Ze}̠ۮy5uW+I*ܖWՍ|tg@> TB_;zB̲OTU]=9g?Ƃِ{#)S\[ooZ݇WݑXW;~PI8&P>JdmmarƭȒ-=D\q$G/Huzl" +vK,2TPrB7zhP9GlK-6)&rהB(e+I +7ei"Z(u_TH5`pQHGS"Sk48ac poM\Zjm5C.NH|An$?^[0 0;\ضeqaN6qOs W)9~KЯDv\JVg1 Gn,;EH`8@r'.dXww?@PmW!,xX* {Mzd}},JeGh+N=>fu}i%8LV8 \/T{Zq[ayq‚X\wWѕpo[}HC^vr#TYXԍ|U5+GWJP-}n{ƺT,33i&WRȣhVFL:)k0RKT)z&ʴJ9Pi%QE9v(IWg7U6%KK^; V6dt[rEن0w#DB8J(,:vѭ6o3P@h,=΃kx㲲^H<%}LH23OP^i{=)@CG"̒5x _{gH ]%f*8u7T PiU>JQ &‹]mrR|vGrj[Dc`ҹI="5;XkK%YI;K^0tj{5C_'JB{H;sQ(2"20(A'+G~ ct9}Ov.{4\][~~f<*IygG[}v]7Acl49ӡZ4SRgx>(J]wdqQLF84Y|{6Z=;sgN 搙֕U61DjY'{5%(t">kSV(K M$lӨgYheYL开EI^H[6gTĶWk+ SzoݩwWW܂ĭfR!–a-lyA血֘ef_x pa>|Ou.&3R 0X4|4tVh a)Eؒm?nc'tBNL5m]LSHSnғX_F 8R{yr>NLa *?xPMC=Ȥ\tQ7жլJ*Y={FnhCYM#wqHIhR*?WW ܺ3'9} Xeca!^6Vu+qx"4Tn6nQM`,s:w[<͐ C ?njK'0{+;׆|un-pe ^J&sN[-\ɎC3kL9W2S8({a88BYLjϯv!vSF u\@ J{zs]wq 曏YpIx%UT[MzV i]qW p@,Zyߛ3ۜO E)d}ir ]IT L0hUBV3:'&Lqs*'ހ|uW`x+Lx 8W/ w>WWךQx ;i 2҄Zxok(ߥm2=9ɋ#Ql~q=Ǯ"uwzLG < xc=>>9*}=B- +؇FHo+I! $[ 񇑁WB2R#agzjLł\?Z yN3mE4ͦSދm^#) ߇stL rcxe&!Շf%IqAV;S L8}lTU,R ۅ͖Yg]3=!eE"7ϲƣ*Dv*+ &R3ehxT&ɉŌԊ+m{'6@7kH͞ #%\W\ ٢I(4 @ACpL\BhJwH 1p4ei+Ҩ; |fTnܙۜ]iwxʣ*e_*nAVZwppwܝ'x޺=CFMLӮ /Es`z&jWg­'EL'^tW[?"ݼkjqz%<E3cYXopE @Or餜>uR~7G.^~U 8L )HBCh*Bv "E $kM[H庲][B̑^@Z*[c-<PӐrE7 ܶXլ=v^N9G}v MfQm.n/>\C6ֲ<7m7H4} UzuZ :gyuETW\ y}P ڀσO 4+dѴ5CR,P@₁)6T7P^+Py=Bi [քiGfʰFL/-EHπQ#"G)0{ )Rcwu9@^`փz&G𢚨_*< .Gv&X)J0e֋$H<44R-da(zݢ 2-limh0$Io;Qj#LmM0lv ES,pp&m =\zuyH"''YA,h3 $ }մ$п)Qc˘F;\`h"%}}}8 F#rBWҨDX0 gP? $uhٸA@bv( [WJ^4!9Zʽ" aP1)jPSQǘ=մC[5>;rl&DqeE3!Y[6EqF^D<*DžFg]SL F}qSc櫧 =D*vN`/޿J9[rR+XLzzdr|)*B\pFMIޢ'=(n=.@C4 )%IHb)z#4/f_`E#}{i-%mNMz>1 w~[ -;[!Ez3v] g(<[`z(͒H`k'ɓ'%ʲr}z}Uj7Px|; {iX P A甌JLLL3Ymy'uLH~Q:$ETW\ yD 4f ɉ{홟[kt"`'n/JR{9'jTs  !e}HD $x5 ^N7)S$ fԑq9u漼[r5z|'W5,xqShIO^ɣhlIJO?@0$}7(ǎñ\c@;d,B6Gͦ6gűɭoH5R,3 70cWnZF;m=%*?3v+p??"FBQ3^8 p.y:aK@bxgRi֖X";ahxdDr;Q޿!<_'7flJ>Sr $yN3c׋R,Wm3e x6>z*ϟbIY۬J$cE:ۺO%ḫ3cQG~ԏ\5 :+F\ Ж=w$ufDWWۨ/@55@ҕ۱v'S H0CXyY81O. [9z^O86㢈&2C\elC$ɰ|{?Fߵ~oE^Y-4'8zMryz3.ǀq/mUdfnAⱨGKYEW7ɰ~l6]p(War@/O(8.2|\AK 8z wHh#.pbB_F8 ">\S@4r>XGSʾbx+gWL <ɱamE?nJ8@1M- x!!,>$hI fy5Jku "F{\ >G1]qDCSv5{{>h| TswCFDYFE|!te7r-W Q^Ә0`f=.dV- 'm 49<QܵI5ނ0Ekm< 5n"U'k9Nlbl1IsqC[+2Dj cu|!:.!]@;@)hAO^#;c!^[.U*d3-9|@L.oH |@~جɑ<-mk B@/x* $_߻&JIXk|SDZ;·QvRm ab [w#r Qv!4]P !ӈzM%桖2os 3P3!.ta?fm&6lm%i COsvbRh\APhŕڦ(L.–|7҉70𥍍"3C{夿Ul7lZc*0AR0EnR3UP;xl[|6ibrY1/iJa:[Fk͙1vWZًkm`*zXOWA@m VyPLY"̸y.Eӆ#4$)LNl,2ӕ#!`,na;nϓ&&I]6΄2]Q0C](zp,L]d1<fp$N̍`/ĉZ4uWX -JOĈb$p5!^ocM%tR#"Sʋ $E)ZYqZiR@kuXisLyݤ`EϭWYN]gB=c!Npv µ/ ιFx@ҟ|*D\}`_bP ~̢(m -Ć&49*PEkMΤP0yÑ7X%r6 ' qy (/v/Vl{-c:h|GΝQ|]W_>5ylr$9Y,A*t :k+{s-[qHsy/G% zkDh}p9V@T̵% 5rM>']d `0cHo$`w>XEӓmT,Q0:z}m}/\e)VMw\6b~Mg qwN~5?܊ 虙pTx ̨!Y vԕhn_v k5z#am3̭n!+/{ߊ'׹{W_GZ/'b!@Ya8!HbK!fSrah){ A!۽yLXcey-zsΤbr+‘G>Z) 8R%@,(}ZS~MpSϞexXK]E}ɔHI$V4mB~r B- -0qɦcx,*|LX_Hô\5#wePBYV Ix$:LOfC[Qn r:[H ig7F t=t?y^:j߿+"=)Z0#VJ3Idb 'ClN|D_FZB:<8OHg#jwZ$~̯?ֺ!L2"ٮ8a:A+h팏3|gO@M3yxЪ:-NkGDQhl;]ee ?[J\yH(%ǵ.qGɪDacbB D(]ɨ \&q(C5xU~o/LH'b6۳rgfIshi J<>JG`Yۛn598/bX{:3ޞoFrĈ޲_Ƃ "WVL:  F* #IH vrHUk3$e⮎z=M GÇʪ~Cμ/G)|p7ֱVg3EQl{31Bb3Ƣaz&x$No-WiJ9HȉTiYkLdcN'e\c݆wg"(K?gMRcbGfE%4é6FB`y; W۝ƪ:kGl})eHc1!A-hoרpI79˥`] Sw J$Du8C+ }iho*E::M0#&0@ۋ=Ƈ{% ;$qEss+4aGL5t :R$XVp$7UKDpf7$]OH7ќ1S֌(*Qjq##Dқ1{Ț tHb뛥|H,׾MgG#I*F6@^T>2,23KFGzxfHZ[G9 0 ܆u 3酄B~nݙcKp%쪌Y4vdL GR]1r` vK+qK} pC>Wa&Y+1#Hm*)r )i,$˱u#<u ؗ7P^wE;N>h?4--e+~L$0%}p.vYbNÇ3y8'Ppo*y9 l aw%Prjr@uud|> !y3HjhHƏ&-OkFIXٍ6tBuamz'L 6){p/98Qnɝ#vH8yя?̄qb'q`e@cz̤Cg|>>"'t2(~Bh/^5յՏ?037X{M=D$jZЀ$ QT0voqeo!CZ\#('5ZBxMQѳbI|u> a \>74j̳&p3aHIlG:#X5 e3 p}[ !<'[n3'\,.o#YՇܔ.0'{ƑOfRuœ=ŋ~ie萭My,H7:< ozYNͩsZ_tn~)>VXY[wNM!=]]:< IAC^ICw5dѝ]4<He8s; Hx|}xŕsK:I:Ue(Q,fD\%'ONA: D o)HHbAA.%B8pبw)KavJ^FJϿsJ=LҾ@ o?zO Aв<4 41d{{cxOwNF ϟ=#յ شpn0Iţѷ?_|^_{//ԕ.㑔hX3j۽}xLiAO^a:̖`T0bfI5c[% p |HږbOo,UY |㡔m>24g;Eb.HOU!*s@Dќx9_\PV]D ̏L,(ª{٢;1]XǞԖvg*Io@ ٹnv{~kո Q}uYj_-0IUUw!w4uRckaM~;ں@b?tr{oD+24n,|_!@֒NtuKLD9x^ɕa.'9N'{,gCn >}3(tuVk̓7(zf$v žQBt]_dQyY|?z 0G4#|T뮙y$`0Y;2РNW,L|9yS *3 G{tL{qŽkem Kuwb C'qƧ-EkE;DQ~]4:D~ا``elw's-6}߷?tٹM"QER ϤI'xm_l;w_QZ[A^-um;jBW_we+dDxkR&L1t~ffy5XHL}k"*I$ju-MF6BX "ڑ QL& $5q9=.ioVA-7װZ=Nбay# njrn;7hu4x?C~>$co/#U8=¡J)vt?t9.J4#"8Z2Suu-"R%zQ7#{kc'm@C31&k@LGṞ}Scӷ͡x&L"F}. 5 :[s7!F(׀Xd^9E R6 (PӃG b^ͼ< 2δ0cf^.aD+ɗnYw3346ͤ{yX,x% OL5w*ZS{?ܐڤk"CVWn=gvV5sO~쾻obouwsg=zwA=RV{-cMz_W% "92@4H&,QaXFl:[7'K>ݘ,b <<>-C Ls9jUݵZx("Drqq4-e C _[^pE%Ԍфa7a[:__K%1ԉ'3?p's豳Hex5&KKLbJ6 no>,"*]cR}e)$7oG>pUe9)]R ztq]aRIQJB:Ff>jBxmnj ߇Pֆ.ֶ˙/廾eKGe^ n<drnR\:gD)"\ul^>! pailW Op0yvL|4?o˝k4UxNo@:4f=A_aO(=w\>-}Č}4!aٷ#ݽmSnl46 \U;rl+WP+s}*.JY`NN"ץud{-.JRHll7,E*7lzW#Gԍ(Ԇ%u 4xL{ y+7qztlq55985+٫:?ΧfqQZbrYȿ75/b$drG,g $K׊y__ժo`ssNjm{),9qK&&xAs(zKnbrQGnxxaZ` p% ׯlPv^GBJ{ᆪ'z:A B yTyλ{_H`:/=_ql6 sJA-Uo[J0&  ~I2"MH|W]Ys-b-f ~_TVeaKҎ+d 9„!?yŲ!cV(%/Ix.+{|m筩`_WWyhn0 ;xbu䣏Bn,Tw"y`v2D]0};)=?#.x2ߎF~$[9uv Y v~ nߺinnسS5uȧg2l TH~Wh@;H8n|8aXTI5,%I SC!jC}`e)J%HUP Z쬫+@…=s{>e&WFblEkSR)y;?E.*JQ$>/\ .JȤ#"&\arL[yt`!xgŘYOCգ'v0n{4H $e|^ &P6E +dMne%ߩn 2 yu=]F}VNtQΝlI歰:&/1 K{"!X>_y;Z١:e 7s뼺t{#'.uyf粸NcF]P,DdEV"gĥi "G`+Cq]EhMᠪc!rCRyZ5Qۈ6@d#M Wȯ0Az4a$Yeqx%k-'J-P̹ȫCOh\^=z67 ${ 4M囥M|4SY ޲xuO8h0) 10&_~ <%h6Ũ*䙳n]H ֞'b!W=\`K~buV'Е4iojo&Bn̅,`fwu}5,`Ay4? #?ۿ83UU-h{]B$09vrdwc϶pDZ*9P!a[yUG\ Z^iR< I …܋* Imnm /7 UH:-NfT@w7vwŅ~.Cջ~$}$j0g]H j;5`adq9'gN`:7Vr+pSb xV \5:gbWr+%כ0-/C,`ܩPV]}/9J%e|Aҫ_~'*l<5(Nb.g "Ί[cRKΉwiN3 r|o?-|y@+m% l!- J<:9~ҥ=! F/Ɔ)[3p$b6wU&IeUN@mf*kp k_v50vz ׾ -eG,ZiP`TH1j0i*0R }7CSmcwv,ς/sVrʉQ"fEx3ԕ_8d%WĄ׳y%Ha06>lfP޳b훚fkb br]jYy]:D|Ƨ G8=1S+ȇvW&Tu 5>  ]L sl36-ɸQp=ӨGd_?zNmZ-Tc[Qu2ThHG콼 Q-/Jd"2eY]^ Q6@OEx"L1%oy3#**ҍ{֘NhN Lb}O&HQ7>0~+m$͢|G.T8s15^;DtޢL"8}NQIк1 B=e6ZG2fJzaSw=x$&n}>0fheъˍ 0?! ny}:x.kB^'w{p^SΝ,Kw꼋' Y3?u]Wۣ={jv5*z8=W1LO+|KhMZ"l.@łayx1@nRȳCB'wKDf&2'I$JAY]{ ȲU{m&KgKwD^9ݳr?} 13:|Hl2x;l :.FjŝQR;#1EՊ^?&LC 62\v-M?P0>2";-$PkwJ /*;L)q5p kFKTYM5} 7`(2 $< Y3 )jt I2!yaANTAAgsA*%33-s 3uͬ@*Y T ГA簋v x4WJk:&Gbg MLe,|&t?W~7̏}gߵ6_r|9Z%$Y[^aZׇsrN^鹅Ru)hcH#>#V FFʙ#1hXh#*bha8UPH'#9B&IlZ k cJ\׾QNmMC΢xqN_3͎j uIsn.P<|k@qL> 6Lstx4q* CҀxsRXCC!TV B]Ã^\&H@)d טC7zgj ̧df礷o/d oʰ⋯anۦG/CڔrK$Jz§^}`DȕM>o:gsܹeof<YlC~~֯۷s[GQ׶#њBOw,%h[6EV` Md$ >М,㣇_<pHDaasTm%zlCx<]˕v9-rnx Li2|=L>tf*wg=HLXJ&s V?>΢DPߖ?>iZ9z9~dy$Zɸti 1-OxpF !2kVQ!787PR1wsO յDw.cOLRW&Z嶡>-sov~u v1>CT*/O;;Z7^qT壟GiaY2Jؐ:.k||\:;vg.k ᨄq"G#.̎#+ ? ѡ-t8,yj,=ew&+fҖ,T-rO)51>$p&OJnl6 - ڽ՝8yȨ\hmbOkK?%<ĜE~Yᵟ]-  oT2eM\rx(-h׎[Ƒc 5+H>wZ=YvTm nCx,[FN:Ca9~I9 ՈEx=8{/Cy x qYWVSSeVpԭO yV"-[j|2 $Lv֑tMgKׅP7@a*P[i+֍aݷa#1,<ɕq0bK3y>(Sy,Xĵ4m(#jL@%)&SK713,?1ވɭ΍,CW^?~VcSAЏuSn/| E"⃇](Wч]܌VD\OZhy?02aRZ'["vy@]Y[$Qn&U5k*/1}<`R1L] %L%ȡ*y˱4& \iĴsHjD7jf$j'ɪG'WDM221Q52::$BTBjLo/(4ڬ p뙸^\O+,޹y*y\2=8 R wsyB/"{-\|~W}m۶C U3a| -qr6H`\jIu9qcnCg IPhL-WJ CIJbJGeTsiޠ ٔu &}6X3Ødo)ZP%"vfgVĒ&$oP4pVsilʃةUKu@Von2YG"a%[8`#k & !j?·괸~?u)ONfBdw(^>m?`Ai:O#쩩{1Hz?_=>>YG^@2f]_ I] o=P/KI_rz>o:MӔൃ [%OL".BJv\Ɩ̡!2_yg;C XdGL/H]D]KG֫M^d,= } $Fj0r:鑬kF8&VBBzƥ#8zb_$}B$WVy?}xָ{PhK]]ͮsO](P̊@;e׼APO[[Z멡#ĢNmZy[A];eCa\>|Ee<4 inXOBtoi6#|oY$P/XȩFV W@"EI$&\F=5BNsM,")h?`*YӰɶzkz)D=:'>8tژ{O !`n#n]O 39#}%ÍR14V+K9)Ĵp5x\ F x%  @dJS$LQ t/^W?qJbǂ/B0CAX%x]EiPoN.u9~?baV& haCme=9[K+ixE ZE(+p.0S}0E7z `CI럳XEvΝ!fQ\e[&g>tBC} mBݠC1}>x/lM\쎊D+e/Oķ4T ;z9뢭[t+l$j" ALZq&hr'Ղ0r M08$awKJJX /џyc}_*Z};;f}7@P99W锟x:U #D; .qG'Rn3IPVAU7Rh}S By⧢@|@T_SΧnbc1´1Ť];<[2yi'P7,ܽWo۶!$~ZGV~/1?+S 4nf!2fSfŠ;s~UVh(&zo|_*ܾ,1ijW~r <HD M]㴃U+5 ubڝN;[Y`sʼgP\. .?נt犌+C Dhvӂ5J7p'֢SIRftQq$¢aD(f!]?fx*+~E(B^#I 淿/{_|¦G>lS):}2w<{BӘV@ T@1ٍZj4MjhDhC$Ow޽%팜R@sa :Lk Hn9]ue IW?h fM$[DJn^8A)M:K3[wh|JWK1ic$)|;^ys_̹KsssB!( 1ɴZ@z#>Xb3A/τi 7?k@RmM%:[h nP}hS(s%b, ]7zbQMZSiCm9wr3iDMԴuPN^>WI&$[q"5ZK5O~{_~KfK_mN-NB&D2SY!geӦmJAQ ;yĞ];/>Åt,h[Xώ.@X:fzge0i6.'YL ?us:Xʓ ⺄ q(M<ҭ" Btoma6<:6AK3 /'yV f.+ &9UXn.k{dfz;wlckӖ-зm۷ ^zh_} aw#[)}F"Q:qiDt|僆k+60TSi j$vV\'DË4{K6u!X@L1qVXo^"< ŴLeyt GDcnpO4e+= 2LfIV\GV} p-)EՕv yr]tEyԅ;x<Æ !ã1&(ړzDOVrIk$ّY#uR[|t.9+y)v &gi׶flZj$}=#ʜgNzv[XF~lw\ ݌A+mTv;-N7W]mfMJm,/ :urx'(g(+Lg@ThR-rջT8i15dD<9j)ϐgm[mʮ͔vP' ZMI;}q+t=KmTU]ע 7ʻ_!"8u($ HJEӴO5)2hDb h,//D3_ۤ."̖)5; ܜ'H&ׅp U> ߌrY-h &A\j {^sk?'%F$2j W!Q@!AHҼyPk&FޚOӳ#t*W4j\jV08"?(̓W<P/lǧ湰N'’Tfgg؉-fhJB{'ϥ-Z|+G"h右`D<5 w&0>x+3A>0 XI T(F6*`b7Y_InՌr+ ?f< @0a^':я GvIx-ccvgY=yp*b{nj=H A#10^j'MZ#:E;b`XLjE&laa=-w?~1P>(Bk%SɵWӢ7DS ߘKZ}#WI$4iCdy& x8}$-l HvR)J ""k DT7)%¦4'"&JGkhSr$j!U4,B+$U𵑸RZKDq_ p GА6ՅucRv#k5r{/n+ uUeDJa1:śzw9iR>xZS, B7Xx d^ Ƌ;p@ pܶ#CӤ@4bIR8!\EyߴqcBu sIƩ-6hTi*ѺqhrOR mݸx\LLLʞBr:)/7'Izwk{_g1Iؑ#NsiC˂|COp  @+zL!HBGt15uL YS]F6шڲ⢩bxP/r\?2u[LN75|D")n,(Ą7rNB H(1O2XR7p'Ф d"ν>T6^}s:.:(r9Jn XL["*HͨES*ltnhDOl=rܬA#RxOS&v*0cBCf7b"ԤaHIr֐bF"i!ع߈DZF $?$[yNQt-"t$Ղ6WMi.= 5Peyԣ>:ړ-\+ы#gNp^W56lE{n]\hEKB%"SG::R{gw믷_O`胕ٿ948Z i1Ep(Y,WYgps=$C5 K޽>[~ώVDNNC搥 nDU9kns^ش~@ i(ӠjE#@Q}br9z <>{7'&Ҫ(əKHL$ .dn'My"i6jkP(L yNo)Nz(`s6P[!MojݖѦ:8.x֎wϮmTTDM ܧe-E"5 @6{\%"cStYpzso~0fCTp C p0O!=g[<}#AՌ4r´ ( 훛&|{/2ڍܚ=1k(.fgT":z6** :S]Gi }: )tnϞ#N'S0exDf`+##eeE_=f0>^s$, .QSS#elOڭj^û u'Ɔ)[QQYEM\a I#<6WZX cqz's>W9ܫUNXBHBL2P7&{T' # i\Nib4+aۥ\:*/jf}XčSF'O\6F%n;\poP" 5E8x"}-%$&i0)%hB,35g{{{jK*o3S]_sj롄QNQ5 TRT@]C;zKg|NouE_:]cYeϝ[@W7F'ҵx/+N,z}gw_~FӴKz$޸%$pl[x2 F~{^0C482K3tj?ճG%LwpQn'$+ОÛGDg ER‚|RÉMǙ{K2`:m-.hctܕk7>R'"E$B7%$)&A'Ns1D&R[Iҕ+7hnv= \GklI3|}(PAI P\Eq#n&ghxbM4JXDZb3KivmDy9nvY=#qߊ<>- .79@s@=~bp߁)c>,!Q>$1v 9|2 c?S4$̖]Xz &y?ge[BI~J $KDSC] Uū]Y_OfJ66rhv/"᠆¤JRfڱ}+}_ͭuTьVEP,$)0ޯs RCa:~<0=y.q#N瀔 ]cZ" VRPMBSZ))vUϗpQNtqTrw n4"a7Ăڶu+lhrptFs빽;3%N8Le͐ iMA'/\P~--j2s/fJ,"rC~;F'&_'_85,!.p?18*&P(~S:#Jpkl TQQQĦ0?iYwab7._HaZ0'khs[?gp ZX RJ 9M482E](Ld7mhHdSn".5ݿZTNghۜhzf;lDMV  2#_ \xonO,ⲓ[@^PW}L7G&![y8KHLh`?xHq(OvbNnU|p],*NUqĒ^褴ql|F;XH`@./]kAD ucy*iC%1"]..fY*).Pfre >҈I c7H>WVWwaI?PKp[=fh # ^3a}ۇ.1oL͍ ĄOFIC}%G*Յ8]O> VS,7 AnRcf`lB%gَ=dN'T!6#l***zy\>PnH,N EKG,}:iaeJ?;0Dp!bjn!;vmmRhNK[ہyCF'3:$I&aS"0^x {S\kn('"ôHqT+=;RUrdZp$ʝo<42FMuF,gwK8X"?8Ńt5.&E}DԓꖐHnU,<;.t VbΗÛtB"6h.&1B>smŅP("JK rHk}_ U4ֶf]a$ qSy֦/fgfSȦز5g 4001@@4zՕ%tBcӢ7"_ L$S<)4?}%k"DԒ@JÁ\m5NF) | f@qi1ڶy#7 u>sJY*> 5aD\۵D>n/&#]d-".OTZfwoBvUX,n;*PE+Hcݞ˩-*d~ ՗ӏ9nHD;-ϑ ;we'8E .`J/OUtՔ"Va?̖1S||? U "hReH" 1FHe]EDtgg}ב.[//uNM >ɵ(7p ʬgF&^C "I #σMgtr,cݾz{OUtg]C`za{txt4D}":'VDb" 7PޡRtrqkOmZ5>+' !.eS=ihjj qo* -2:dbi555A}tpGK1#pg߄;ʙizN½_cH{lFӐr̼'$B)_ O~Rz ȅ?G!Wi6oFJ t*(Т:84T\\ E#/˥$J!~9TZs(7+&)%G!yTQ^JꩮJTxS+-8g1J$,.ndYZ1_gϾvj[ATENxY4瘚^ՔS%Bb}f~GA1l.\]C ^.&IsE>uCZ͆M7}D|3L|~[uG72Xh 2_tЁɣ}05-Pk\0׺2.jH2J q)3Xb7 *GG?4לEEeB&AT#ƀ ;EwvNӕhq6fS[6RMU% T e?'WD/Y%,~3O.jJzDA6>=A* wݥ,!aԀ Kw+BAX[nG5fyt"oocXP泠*_腅v|Vy3 {V~ҙGqu/<3&.P N?TƖFdG? mE /@HmySTVjkDTlJX$2ӄeI˭ͿYD"P]Y/q_ 'DARn7!d𰠐$~!:bAp{,4u!3;>9X{IZOy}ŏ]JIDZB(Wʒ/> m~<@ xQ"}'q!:փ񹊒mven蘄.gL/>+fjgIn_wN.b]#ZW}YU0 䙙Ol{WhȲ*z/і9Em0YP/]E<щCk髀7&KY-Oݠ  ,;вV^~%Dgx !8:AĢ ٚ(AT:mɏl9 )We2]u BH1ΡEx?ɢ?B_7.1a`i>8vb_/)) 8i-D4s." HCCڹSGe8!7[[tc`z&];2R1*1yCSx,!cbP _ `UQaAKcC][Qa#@zX& [1JNue;پtQ:-`w& rѱ Dϼ^r"S3HC@a+fFk`,<\B0wooElDg/k߅x;1`4́Q[=;4IU%WW;r@U?΁n7h&R;/W8>܈>@Dݚ~wbaUȒ[BrO]Doc[*~{KN mE",)iPZZm\F$r[VkO=.ڈRWUFOLW:YHt\7Bxi|]BdC獁ݟko QhI-O0`X6k`vCCKH6&"OEꥧlXW[WUV/lFZBw)M J]Bb{f=ta9;;- /<-P+hĤ[hWf sYkivKHw tqhT#8;iKO|K9*ɲIwv}O!#^eR̂%$ cц\D->ũ33PnsWpQv zI㲀})z\=X59Q?׾I7o#wE%<~eSCx|| iv=0OQRLt̠0?˓e/1U#Mː+m[ߣ'vW,Xd]"N@ti3te8ȥo}:qtkNiڈ{'K_6!5[ uwߠQ^p?%ijHqըqgįԡc-,XLm$9pTcX0SϻO0wb4# BFBDRlm=XPEsns ;"%X84M+Oesl0ZY/c)Y`E$32 =o?mb̥??8-,,e"Dj>tZ&\Ƞ6r JG=];6c%o%/Wj9ۆiʤbdSMj&?ɐ(Rx ?`q)DtRT\y,Jv ZQQ^FD[0-?ɂKH'$gOߒR:/vn'Q?x[:s:#!pqڊ=$?Gd[j[SvaSG]XNt_; >9B@$JFiŚ,,,8Plf, 'ņ4TI>- *`M-[JAu]=} ?(8| 醁@(8vIw8w;[}6w~k`|1[7QMe)1CDB <7{SPe ]ac?,R˰X(#n ~t*BP)^#3%*A'HR6 R[ɂKH5Lx|yo ?[G~xt&sog'stӗzہ|!띝 8jH!*H$HD9݅q?',S11 j:Ţ?ǏH!D,&)>Ǩx,/W*BaX~)T7o&GaJyY ,!Yc`/CU=FF1g=~i?̂=+*).)" _ {{B=ޫak&[5hYɓK '^$M>7uN;)|̒B\WQZ&Q + gClX\ ?֨8,J%9)ip|!!cG,X][4CP&2$%GSS3uߎ6`4OcXP8]vlŠ-ivZkCD ֽ&d $عEpJh,1\0Hplv',|"vhQ``2 ]3s#Х]TOP`~ W ,!yp")CP5MŽ(|Ҹaٰq-?d PHS1$*w`τ&S݆z ~vAT+\\) ćBGH낂%mE<(CͰxPZ4GfH (HkR3%$~I0VS7l*_%$`!%⁠](njIΫ׺nbv7BEup !Kd7zl̙D2MHCD]P[?diR(ܑ%UH"u!8ŏR UUXPjΏԤ*IҧҬ6RVxGhX2:(i^H,5CT 2. Y|ε^i/1=!斗;3 (Vk6bb* Nq_o$ 5vR\D ,C bA0"1L@T"'?PJ\= 15wДïQ# Xb6% z>)0vHG g~12:aC#c<>95sWb KPOΪ>WHD"h#svuE1!m!RN)H[m<( aDx _0aGCY74T{B=^+qbi׌4JZ %(0 ipXմT:ߧThlbMWvE1*D+)YXw)~OF#EEWa~;MV Id$*qȹ)@_LaQ0ՄX@HmOC,'AcS αƌ%*Y `3`iLTU\iY[WO? k7h#P(S_Xs¨|| wrRVvN;(t >)YhYh= bOHv ;vlS NFhL XU2"A*k`0.4rQ6Dt|8-,'YTHC׃RD& T+9r3@ZlXT-@vN.LWN3L%e@ytJ)M1 TxGg{vs?hYBbs@ {g!("'~\C+))vA. @3?8̢ q* ld e$tp@P~cO-ښkEyI`،( I2aFbb~eՈ RnOdWD愠)`6v[( {kkvVWnEj щb9okn@Uic UU%%YX!㙧WڧCuff.^)?^}' !4\e =$YZr >Ԗu3`Mnn6ج| 7T*UxJ԰7 UٵJC E}xcz`P&qcK/[S_&r!:NN*udoM"9% >Ă4^t:5 9McD *l Y.Ɩ&*).pLbz< qII7"fUfN D#Jt)w\-XX9Ă[6"RB|SaaaJ5V HJGee}g瞤JD'TXyWo"N\EqhJ~S/QֳO`xL\1pTJ*\ Xa>X WBkq#]ng%s&F.0@/wVMe@XD[[ruAq9)%ݓw_|fȂՃ%$SXn+m:F7W=n3K,ze̓юmNHO x _ї}䶧쥅9ƺ `F$wdiͅM&_w&׎т7q`a `H,h`R)NR]>r2hlx~[Y⢇A(jz&ك㍢kjF! G 8{vPӤ`8&`26`?`aa %(I0RUU#,ߊALWOOeFƦ4RTP< aAEG_VVV"*ETbAVfYstaYh_yoPLIt`t|>ɂ{KH,Q񂳆+ή4:bC^1:>aTM!\?c8Op< b6!b]99"MDIxriD@|af_h,XZ5ep 'R419]?.]z ,.(F99nR8D*rkmjMm-asł^h9pń sIb5Vno@mf9HoQ/(ɂKH9p Q&2D 8q :oŮwzB>SMm$CcW]mƜDFy}‚W_ (ERu"&Gbp.łk:T7DˍNiiySu˱}k̮`Qx')gϝK8~ UŴk[3KK ZE>:աsܕץODdB . n`1X BheGvlK/|+/6U`"!N#ꡏh s on`J][t\'x#r|>Ne[z˂+eA"8(= ivbaJU"t"~@Ѱ^!MSX4 $zhn1¼ p̋dtr&f<L!l^k%$x1p`v/Dw9pox^{kdz6V% ^Gs\T_[Z;FH/%!5}AVuD81YĂ\k~ FahoOpÙ3o`0 b` DiY9LJi%( Sv^DҠ [#^D;,얐XXj$-Ljp  ցj\cC~WXV[g4`e2a >s^JȚ/*/-voʒ;JBGs3tr䍏O~rDeb  5F|0}ѧ>_憗ָ07MBDܤ6l?G4|VD!04!}t^}tHD:d&$,a0>aD-`{v~}ӓlimnP_DiD#@5%J{v.,@o[ 9sw4MAܲ`H,X4(|XJY:33+>>qwU/&܂$R{T:.xD(PEf*0f0DtF&#g04=>ywP44n}ښڤ^10Ic "nj Dzc V>|Ɛh,Xj$,UnT ֔0n['CRJjJ0u621DtZ˂+"`!"pCGEu8ADҽ &ZdnKI}A(XPz&ktsf 탈NNà!`ᖰj$,S`%ڼM~k/VguGUeJCP^zib}$y@G$0yl"o L  ˬG=Bt]pQ`ᖰ" $8~v󠫿oN~kiZvq#*Zt*%gBbpo5*~;<|RGeoUIuF ܰamzpP`^ydɡҵ(=/ ،?<@W$ \pBcnrSDEE&8܈">ɳo1X{=7d'NgM =C?9 cq92 F"1e` ,ɑ={`˦ 2]$n/E⼿+iQٹiyڬiBGyfKqk5EP LAtl^]B_`cz1E  kӢ,=Qޕhu$H\dz~+l,P68 {a` 1ƾb.(SD$,KN $9CDng%9]qރ㽯@C0cߠ\0ZXUJtme rQ0or.tj8#Ͽ>J1\l17jd8diib21=0L&,ZH73o]<ǐgu-c}㌠xK0iݼIoXQh9L}_r9 v MyǷ(1ƾ, mVVJ*Ѱ^M3ڵN&QZIuOr _)^NWkq8(1ƾ t꫹ra9jh_9q`~D3?>Oʹc;խ߇1|2ap'g2c}gC *oJRէ6^ ]V6˵&!ɮؾAٔ E\옶|,* -8C"c0]PA#X\K/ݶeL&%=C6/GnL۱ieĮ+.>¹3؜ 00γ0]˞-OeR\}:Ɍ.sdqŊw\ʓ'ssAvN(cxcLMLJ/w+Wti{'|J8-'i X :a1aWz)62^lksK}Y{?[fCm0c?pzAxdffi؀''9k]Fk.]͡:jcîy9ߞ-+{(c~k.\tV۟wc0X^@Ã۰r|~ySҢnӇzX/eXdo\%)ʝ뉡Β}p5?sblܹnl c"YA-,,,NTR #/$ܭy~9sd;d_BK̛"#hԠN8n:́1/ e{{*?ѦU7xUTީH cXk  @cb=Lp^ `9l֏p;cB} a5,B{`,Lv+c̠ ԁPl LD, AWc!]b1xco!a̼IENDB`PrismLauncher-11.0.3/launcher/resources/backgrounds/backgrounds.qrc0000644000175100017510000000307515224505336025116 0ustar runnerrunner kitteh.png kitteh-xmas.png kitteh-bday.png kitteh-spooky.png rory.png rory-xmas.png rory-bday.png rory-spooky.png rory-flat.png rory-flat-xmas.png rory-flat-bday.png rory-flat-spooky.png teawie.png teawie-xmas.png teawie-bday.png teawie-spooky.png PrismLauncher-11.0.3/launcher/resources/backgrounds/teawie.png0000644000175100017510000046772115224505336024105 0ustar runnerrunnerPNG  IHDR|)oIDATxt+[m1bt…p/x$,3ݬpkSCQmtK3t"xprޓRS꽁v:K3t<1t00FlRv ~ŠU|;P܂?)Xt(h-lg{4BbK)Ře48ej 7 3L':%3-tM -+-!4XEI3& ,s$pF+ʣ08j6hRaV?c#t6f Wf_iZpRG;zC6?*jo:lGEԚ_}SV.O/Q cWq? *"VOW\tsK|6j%ej[Jmfg6۶m[_%7鳝=gνAsbw%9\NQU$8Ԧ;k*Ԯtn"j[OrrNq0$XAkeLI 8xέ5?{ d=;M}d{ǫy$lrv]|#98pp\&Ia' IA!|$: A ,^q"d]XfIjCѽG Q>DNZ;$Փ}`^G\ a7тץ¬ln&F{I-8C9*e_Yg5=y#J'E@I8xuڝ_[SZȥFsz>φ8s[}=tue4ɎϨ(d;5^lFOj3"MwFZAQ]\7Zx xT xV;KVvw'(Mhl'_wʤnIK $XJHU;#FJG:fSR)zB1nV!V VTg =}X?Uh7쇴s{k_(sMħWr?}$7-;D 嵘<zrbYkr@[r;;`~A#QY w/sB}SY*EJ"aPH[.WF72l;#=N=u޹ r|jDB6N`0$qIxor)Vֽ ./dSkmUt]:g)/\{o p('ZhD6"p[b&?hI4YR3vmxiJ+$T^d0'ɏ/ z("C5I \X{-{quf|E Ц/C& Ny{ݿ>)%i6S >J80{!*6ͻP}?aAnۇkȼ⑧0$3kESz\1m:„6UL*=ow֣phyLdet ݃A[LI8Y׬#̱I_bLℵ-^hINw< 5 6\G.ҙH6L"NQ(I*&k,)O¹pM ;dƚYf g_RrdK=;<3.#o9SgZhC+ՂnĈw6c304+GBcĆ6 2nT1NdziO•0lG9H(D|]a'v}9e]z{TGJҡw-q}L &8iyzv^מX縺n0v"suVxCt鉞$ց6ī5Ҷ@H1B%ed(':E$zIBQ8gĂR3ͺXV[Q溕E7Sen9q {aA@*/vynR5+U6a9-\EJ3$dQlf1\Ѭx k WK($ꔊ z}U@Wq6߬[@CЦ Npܩ 5+P3+5<2ۗ}3sS_uG mEiŕ|B@pSd X`:0)PyXfDEL&xfѵX؟&dGRs?uuAO?tsqRa行6sǴtD] L=h~vڿw ĂW>FeiJIE[Sh0qhtURYb Ga"øΥ4{ Y ^Kj}X=11}UiG) oM}zޅܐ۱EzIgOv}mgiMzz5*Zx;bzC"[\箻!bJXHŠr0}#Aۓr*iؕa!jbpI5:H~X9NSA3PŌM~bjddTgКo6xԘlt90ګoE#4tiDUF'ʃ?BW?Թ83zqZ ]T";^Q6#wl~4!'!27ʪ̵8g+O0D8P$n96՘k_v8 ("V@8.*ྐྵ^UC,(kM`ͥD$8gNX8ϕMdT>rt D ZD' ԯz% zkX @w4Ou=]3NG &x}9ڋٳ)BkISfpl`Yf]R&|b{`j12l dn!D8;vs?@5% >~ےҡNN\h݅fF4&ӧ `#!b3Ɍ~v S4,%&syD-0[y[ki6xW~C}'QX, g{X 3u,R; ;UO94gޠG/BCPC5ϊO@D:аn5|AvPժRRi:ΙS\Yq<\#75n*('qǖ}O`RϞF֟p|͐;Rc1I>+J~D"&6l{mvd yZy %@`}(͛7kCt.ъYllc^7ECA"`>x>R֎)LٿJ|CqT/.>Nvs)Lé'py|ZymdӜu`kdfIDt7S7,=;PZ44@j͐bf  ,mc ?0 jlL`?}XSeĪsHk.~6> cZiJdzC4v$ö<3ePѨ*LRSt0QigM;}Fzfj^RD4(m %fF_f?"l=^Iz֤:k\s2e>>ATKdp5>zv5@almֿ}g&ťѥQBϠ=9<~K\|H 7m4j0^}=cܿsAO G 3e8"2U5QIEftt9 ?3 u#!$DdQ!D(R l9+`Ǭ㮠N܍& zݽAB[sļ2L\0F@?7F\ ͰU{!fShrtzsS{i@- * |Kn`*fHs3:Y,ΠH~!G{Tw."j-_ 毷h͌&M 1@2k>_gYPo8f+#%㥃@a}Uh^了8bv,p$&юDu)>>;ɷnx=8T{ۍQKعTub3TtM(tcckFQc ﳈgwM{g1r<]M@m5k=G8|B)l& ~gFr_"Vk_C3aB4q5RXk)D(&Z:U6SqteyQ(ueS@/Cy|mY;aBFQ9zo@ i4t.%،QMQpD5($5fW 3f@Ȉ}Gڔ>ŔԹ9yQt4P~&3v,646Zk=E5q.d j0yv%}pxPFh`-]@(BI~G?h$C8""yv`GTm*R#hɞXO:W-3U;jJFNSn&ZH5~zpn[+rͤC{um&m71g E(ՏR=x]OT_@AaF4?fcFS96eʎw3W8x3 6{H:*&А7&MB?2X_5 y;f܇7G@uN! hm/@{Pb?p~=Tv#T5OymL8~_o8˔TfkQ=?A6x Oh֌yDG؇04q.kDB~R%M!8vJ:Np:ץFDqQL3 yEG*@ufFԑ`uVh5BHh`L^GRo(mvj@fE*66cI=wLɾq\-U RjAdwq_T4:gTh8 4g:F3MH}@X0 z Ud+(+1 *PCM앧|a:4]s*I5WQ4pTm￙ֈL=*@ fֺ X36r3ilx]J+C(Jk4ڭe3S}D;6JDw|Je-5N$KryE4^3%KbWؒMi",DD#Z$c&hH-5K.mt`Ua2КQi,jk0j6&HXuc {fe͇ITQ񇲻{巘rjPeRL|Ť7fE[v.&dzP#٫n|Ny,՟_c\NŃ)#7I~ -ER7_JJgHs'װUr"[*tĕ+)~>Kߢ<ɏ2AHouhM?F&frlS! i&S*(GY<0-@a:O}bzܘY :mbbj}mh̨~R10R͸>SMcI?t&ALA{;I*.ؓ}1j7JsqF5UH=G -ult@ѣiFF$c4{sq^9|" XҒʨt[ pm[" >_ @:us̃x oT,=ao3ռqEP0>4u^q`--&7Hd"XH Rg% H")"D *IMER'FK-4 'S4'*:N(|Ϲn.p9+3R]}) 2@ CLfYg߰ fF{#iy{e}b-۩`]:E) fjXj7E@1"6cR:mJzEWӊko7D7|}+5'Tt",ь\wjpX\h !]SO  5["+˘DE3X` pb[;ad?zq%L7E)pjr"p1k @Sivs=׎q,΄Y; %ʵkOO kaTV+bۛ 2y̫J`4b&X0:Zsf0`tCQ_*YA=3qCui(k,T_{IX他9Yo^+=6LtJjsqy4gxZ8.w5Mq;ˠbtѣ%NZ k6se0A!Nԧnd'h6qӷ[W֣ هT-&6+ ""t[_QѮ+"h31thDDs|.(1Hc/@Hqch[d$7=4`i(}W4QP67M-@=pmNo-ܶČxmIY,8:.He|'\K10xoEF:0*5c4Q-Fb'MelzeyRBG̙=e@B) pEci;=9Q$Ŧ Z'Z0Ͱ3o@J+;3l 8}1Tl};`7&-)Q.)}H^7(  3T&/MqEΪnX6v@ @@sT+;`ƾyC֪Lq )26g6 %zТ?v+!6LGhǠ07Y DsQ^<=lW/Hŋ)3@=n@D FɩT}f55 Alifq9}'S$sHs5ŝh]4eQ Lݶt.|E xx(:{"ܳH+@G.:kIyImE)Y3}~NA%1`/|<{Sn: iMXkE4ZϱAN;=)!$*߇j0 @9ɵ߁B)6r #)SYSϟk3vXhX9e,/Yfխo4ʌ XҞ!ubݮ#O^i 9XҢs)J$"Ԕakpѝq!h k6l# qLr(Ç՝#wQi]qww'C$A͂(]AۿjSwWn@|n>]묙s=}q~.qģSZ }c#FΛzj`8\5Na|Ije):w?S~Ӿ7;[5amO1CB(.S&X% ^}Y;^wx}c+hI9MIˆ3aO?89! " p7/.Ӗ-V>;}5-6c &'_}&aa(+a }3dfL^ ɨ;ZKJˈV YYvfީ& R;Z ҡSX\SY{G<Cˁ @=OkԤ$47vHuԎ1:n~e8=hL֌et-t ]xXfD=iџ{7|uAȗ|x I~]}׭uȪO~k(~I50|7 x%4lz]2M-U=#e[@HQ$=XltN@uލy _9Med FU͓]U0SBKk7nj)MJ̮{址aKOX1Mc-a Kiqo;pYJoZJ\g*VyzޝɄ[dR^$CI20%G } `#QTdR$ܠ'Y i|>Γa.̫vǎȺ?|!w_ژ~ 2-< yLıŃ`M=ͦ\9|<뇭~Ʀenq3fob7eΏL}L]z'QX'(E> c\4}];?,':lCހ9Ǝ 2h"JwZ/k2$&5CHkS){b hٍZ}רͨ]bDdkB)Ў%7@G^B|@s65 Z/𭙑O~7z*ӯKzefZt}rxUyΟxV&O&]z AL!}7>,c'}S/yPf^GVºyUXZOnAHY  ӯz@ z DAΨ8purpBzMa0r` M=BiZ솂^^x0 ΚaBz5GLmծ_ wkK 83[K3r;}ҫF=_9*(6b3OSHP^Qz}|?nq5o{|`G 6i=fmaΑMd{g r~&O)Ob7mK]RЮkGɛwwI3"<'ì/]9&>lPMw 0 {"A!({`eu|eTpUk'M<|Hxh\3 Yg. B匫Rv!&JsQ>Hl/{;'!PFfZ)AKjW)BFҤ+#)!{+.a33+[ $9?V{wmҞҿm5e0gd{P "R6s tzc,4;Y٭ӀM03ǼQ2U(PF{FXAG2=FQ{&F s*3()p(p6rE DaѺ%*hg 8#g01$Jc 2"HgHv6!М%yXjYuא pzn#˟ ׮2ϦUĦm6woӼ?f kl >o;3NTߪY[JA @({~J`0C0=wt*% gN!Q1I=;<2f PyVC`dosdQkQ1ɡƭ>i*Nl1kUXS,Ly.+: c@8zN14iscW Q\dgzaO,4^ e:4\œ1pe}zfMiztZ
  • mf=#vJ`3 7?p0Om<qV #Qu߂30\b1sCـ`ߘO{(xx%|C.Ӳ~jÀ@jc40zk 8L`h#  )XƮ~8m }S +V9ƯFAz=8~ -qF^+ˋgYsd匒;&Yb?tlmÁȢ{@;P`c]3? YK/M3ʉW߫zrN9)Qًq6}·;0W!]Cv {r]k"[d Omy~T3x~zæNZ lWC&@^ [?etUMp+Ș̽dB'Ӕ:l8skmF)5ِGGo8&UGe4.vK,R Y'w.U6oq鿉a9󺢁3{,|jJ*G'ȗiSQ̱FSF>_ c3c>uEcZ5hLhJekzU'DH -%alRCBާљ>8?^Vg9?ht?'꠱p~}l9m-g\fr>Zz_tOubdx|d#3o9L %1߉'swK^zv|6o#[vUK~ 8l̆o볝Z7p Zد$Tءkŭ5<ٵ&ER_&<2h0>5 /5ZTV 9)N|M5p67aMhѭ,L6zMi-iB z,S+oڌ'MgG.2f ,F󌁌*}TU֛sJ/va39D$gʀ=(:E ̓RIfKqg;;Z~:K2 j/LZ*/ZRNvl 5oH3C&>7`cJaް|"bk܁%팳+5e0᭮T0/, (8 b~ Ϻ0[uk dseJr <ݿWHNs6Y(gŷ.cmwʂԋ{S5>/S+)yleMWک_,g%Ym߀$O,~MM3tEk&. 7acLdl]vfiRg'GWxKdLÜ<]mڕ;MѰX%gVVH&)~7ЭܿU_VBfXTrHJ(1s54àş`CFki|Ck 4 -)EC|AΒR7DPKc$:[$\GR>&y{[Jqi V8H V3Z>t 2A 4&1`_5*@}/3;tKxyYk"[p64h|r+=ء0t(5@sȲۜY9ӝ+7uFeKx3 &pyI?Si^ߣ7Y e*S6HAd+ nozV3?7k)mI;du 6 r%(PrܖmeC;AXЏ*x1@Ecfggeat[ X 9)wԄDu=m6y1Ltز;xLTp :N)^\$#]ffpKYO4mgdLzK^\;K(A3}>߃`@b #Kh6FA'a?,#*};6q+8>(`:gt@gpA}٨dT9a{Ӱ5wI;G,4d5Y \ &xVlI#l&'oc,ӷl٩(wS̄HȋMd_Sq(Q߲1зM{y 7vakt1ox ']Ϧig:tlR#IFh? O3h&0~ɕX 6yeNbT9Z,c#dfԌ(QPe9 V"GWfGCKlN,s()W{Jv/ʶ*<%ddϦ <9"_Y הּANps@G g2ʷO=',l<7/K,Ÿ:)*?Zk"[l2@{~!Yˣ9Z/>#6R9JpȄ8{r趻%Un.-$լ| s1J`t}z}]X0SJ3V&fM.q]t q:S"lɆz, 4JL1Jܑ2xL0\_],̼EW~Lr|(" {N5,ϰ )kJAgz̉ˋ9G&hpdllj,4:t2wLIȖI}sϴH-U 6{y5yyj&g 073)8Gw|B6L&MZ8h8Lu=;t06-ZQꭵ\v5p6!2t*ӠY]rfOf@sD\S@.Á x#2`,=Q)sϪ*2}`q u*#Xi}[j܂%s@bt,wfDٍ7m,_wq1Ô#`|`3Я e4k!+Ƃ\ n6sB\kN_C&C:{,@m3 2(}` =Geݾ_VxBg*;Q֠L4Lp8٨Aة!oBe"x3o~fDAe0gl}]ssSإei2jtPp="Df`4\ȒAVzѶϤ z@a-?H~8@]<^7Xo{-6s 1Zi:OߺEE%T91ݓf.2-uz)N/ kf/t yq u{5.c.)JsA wQbEPj}{ oy%@zãNVc+`cuɸd=SWD]=Qa_ru~ J'dpj f3PsTPq~ W2Ǖ.Ekeu>gƇ'\21 (tg/5=?izjtx41pKMHٲ+e;e2$V(JrUcb+S/ 3s z532䕭 @[33>g8 =[ llyf}nF&ŧĤЈn7*xl9U`,Ja 1{gs5|"ڟyGd#9S7s Nddþ_Z)@h^|oα3(8K\%'Xix⩌6o 썯Ǜ`Y\9Ts޴-{S2{6Yvv|Pdm6 PK5ה tp]w@ z&IfJО0AL^ 0F`93!<^>yK9BcS>.5edH&XAv%4l0g7_%;>'a[5,9AAi jc6ntR2~du uIW@qf0a߷/iv4@Ϸ3dl&T%y;N24 Bl.WE #2.p0Meg`)Sy?ߗf{8sqO2/2 C]ጇ%jOE]oT Α(./wDZݳ`p@b2>{L_y!3jނ~αWޕ} ehx({|ӰuqUkMdMN|ohN@SBqMK(Ǐ~%`4q.1ڜPķ%|e4h`LT"CXu$sawM6ZKCdFFV, ۾."6$@J7`p4Ü 8U(Pp"A͖aJڬ?seM ^s{o͇eDRر[X1l3I~5òM6 h`a~~` \FcaA̖=O^9e|RKivV% [ud6'5@35A81qzf#drFN>ؕba1Ffp@7ݵWG_v5^L=1Ye>FAgzujȪ8sFkf_*,0CQ&BJ!SUplEA VL5i -R2˜j@016rq_/O㕭 :U9}~-%G$28Qi֚e~Df &ʁ lL&ϴKњ YMN֮O5ȸw@-֬l,͂cu5g8F fL$g\@\I.Ϡ.$ K[ 7gZqo0'lA0,cua`2\ S2֤ZO;e:}iQr.S6vl~:Ϛy㿖-eY1eSdXTѴ:7mW&Efu*k'Cv]yC`fj3x@Hl^)&֭/q5Hs(6m̳%$sYtGhIkZ^0{r?س_=:ev)KJ9pckvo"04:!-ܳ)^Ϫ֚wUA.fAJHfR3z\0Qyl&Fm7$ibHQ+J'0 2g֯35!<)JKeV~c캩33hY\ \2iik-oFJJ d:?X@d S\w)Y3F6MAHڲ0/L``Eyڜ4=xd8JhCzD^ SUD\߱w}ueqelVcT% QM8m:ȅׇ gKjt#5Mo3AO b{ 0tk3BҠyz=T" h5H%@*=Y Y h4K!+h&h)MZ BZw&2&L,?Xۼ䋝_`M%e^Mǫ|ag~a(>Iv۪^Vq&[TZN3V/spz48`ٻnvKWv|FAN-1J݉uB γ0ZҰ^\r#PKkF(09Lgϰbo%n1Y!̖LhL3J&S5'ǡ\pP1Yо %-Z!3e ޶%Dmk~sgА)}z8<2:Aڅ ^rk(lg6mQlh! kACGjϗ gA߂>6X(ωKi^G_}ET׬w`J=,..|>NLjVcuhw) >ZC4Cq6#ӹ,9cy; 8Z \z2ޘ\3 ,x4@{p8C֫)EݢdXll@ GƧgɴly'^{Г=[By6d6-ly/C2[ƦfHUUD>o[r&䂹 G.Vd1Zg'XV]4fD.%Kg'N3 jS$\ͼJU6iSuO4b%#CРko21D?53Q&B5KehYUDP QpލfOˉ TȔ˞I2Go+cdJ h/dCY}p8X013;YjɁ :×*#SCZazUkMdyQ5<ٲ/ d`bˮc~c@xD@rw11ѭAR#<٬̹]ԍ3hEzőC"c13iSB= VVаZf8Ҹ ( cY׍/VY,ӱ:qEf#}־e(?t̽x?> d;U.6-RFy~#liZ˳ȧO3+]7gQ8q  e$ 8aOU 潋^t xΊ}(qɋQԬm3+fձ N>%3m𗱄!s[KKf~ޏ'~a.Լ~Z`phn5%},L&4[. qLk|'e~]zh!U5-B{>7EgJ]O(~=xTi}-+Z]q*97+ jjS"~+Jה1qO{y˞AF$K'l!P4ɒ#OOdpA0S^#*p``66 OWv:%%tIڠɗ1׸4VȌMdlz/O!&BF2 *&G<-A]z@l]޹QͰ8\7{$ag9Љs750R ._]ul$L|d}F0}YҴ7;:C37~K@Ľ.QR^Yieq;5Haf`01!j/@@gb/{{: 闐-0tr5) ˀ=Яɖ^f17ce@,l2|46>ߚ`}Cйa ER7>wa%GhϑJjdu"N=Ɂ`{JXV_ȼwԆ%aS|aA17鬠gӯx~KboQFc߅<-Yi)?+P))CKb9K"{j%i(l ̐'utuq75rkWf[&4 2x 5i\`.,󆁅!zDdQP(A~`Yٓyj`@eIVҷMIHs.[k"[lթvj&ܯ:w_wmFދS(e"srlWN#$} j}eߺH3:q?d,}3Zu1fZne7]x0yf2R 0aLY*grfؓѺH+Pe#_C&zgŃD(38!0kf4['yN;w,5V%Ɉ$('8J=*Lqf8,+ӟX)yc|==Ζ˂M  A@Q;VϢϫ]l"[ _hRo";@فL7)ph58_Zd08WyK$\Nnh'Mq,Jdxc#4^ kΈT1#Cφ 3}71 b Fʆ8=)Kv-')XL$zj\l!?jJc e02iXnȿLA,J3uTz338k08 +.yPpj&fȘU HXOXp f,i6g@9VhT4~p/[{si4UA{s0Ҿ dU 6>!AְbՒ. 7w6i)ȠM9zo?:,&L`:n7/!QuJNYtG6\f > 2xh5Xb13Q" >=_\h^5|Sڵ! cpY||? Pa n}.5D8_[6߱C^{)LG.2Kf7jaz#xْ_=چO}mYV2R4l8b`3+Fsd;)gBv˧6s0xnj;;pΈ,]gInLXU[tr\7wO,b`kсc}1#MyÇ{*YPA{Y](Z`tuύV!@60Y V6/z@'7ʆx.A ~Xt ಡ=wda2r*{Ŕ@$)dh8lߔ_9¯8`~6>i1 abigeZ"`3}0v"E[dt7g b뼶f'gu\0 L~ #t˫мB{ߨ𚑕'ƹJS@/d6GfS$vFs dmRKW }VhaNLS EO\7:Yq N= h`êp9K~R8sA̨HO Ǚ h'~97='o;,&љ02.Q&|3*~~/0ذSK3n|ݓ7_&hĕóy8}F*Nj^)';ɱdp{o,K)LElѝn߯R0pDm4ắFWhɎ҈ m3":Ł Rv~ոH1/+. (ċ|h/224AV4SH8K@g :߀6KT1&gܯaUg`46t!=W@i̜1pKnj# DP:ZI /˒X8Ef; 倯!Qެ3۴#z'|V4Wg䓾Npf沚p%4 ^XX?<ٵ&E2!c 0' .d9s/v(o<W+U.:tL[[Fmǹ:^[ӰWLFgduW͇e_qİo4BTfuPS-23pEY5S{v Uv81lP`)s%)`3x(0P_-sv@=dZcHA A 6|ow.yI2.#[ƃvՌyWg-K6@xDaQCo4os۫E|y%,:៻]Ͽ0Nlz{ga}T(?`}t×Z E Ӯa6f/_<]zl0lKWI_Pы@˄u‘edߴIm\:a)M2? Yh2뵸Y}n*'4sxfƌkLu41 |]z O[gz&5atgGVwr-36Q2;P˫!f ;rιx3JoЂ?kcAߴyKk4ڗSZl"[DEigo,'*C\iԜݼN@`/-эčQ}AM$%yJ0]ra$QH#xEf;dkOꙨ+Fh!e=qK֌}ߛ؈`f*@2Y&&he/ ,υ7[חf^ &VtSoS983bqNao"`q~\Ʈ[rz8oFY@Ó^ YбbO fqhQuY% #ZHRz{ᎨGJhUM` g`)/IpDgѵʐi˚ g Uc2vUc@\r=K@2&uz٣_.ڨ>IѸlF3-ɨiM zZuf8T粚i  @ތ=;fe7Nؑi3 P,X&YL1+h \DC 9j[Q2\e9M!Pxk:S4} Yu#Xb{|}{ <%8Oϼ$@nZWv=QD4ǞSOI_ʂ5qw|8Ob?YķXl/Qs_g?l5%ꃝv# '9npnS/}f2Tai4 `&ϙj p,QX"%2RURCi KS"wQ<מpg5y4#M.)0ٓ#͠$c,%O\q,D*^}f%❋YKXe`[&Sf"Oh-&@# ,S|<G\BlG=ڰi"Un&P[n_[V6dAds㹝d%;\Lm]~^#J.DWp28(d f$4.遹Yek/`fHH՚AhÞz"굹o4!& 4Q q t;Gη:fړar;VUy\X ?WKQgQlYkӱDS.pv_}\eFA,9^VDg_5^/4 i00;29U sl4i&p 5[;vglRR~D)͜Hsano&PHoZcsdu1Eا Yp˒-5" /;Z7 dȬeS:G#-H:BY^P#[O= %})Z7Y]Yn\sbP~@Azr'C>4z232mf>9R2Ύt/)<[>;'پJ;PY%d| ;\U$al2ϡGrrv_gVkMdtEB''6 4v9n?U[Ɍkaa²(Dc7$3Q`$hda~&*FN9:^aiE 7a6C)}3R$ɂ/Dkl=Jgm> 6M>G=_dz7Ƃ4+5kqc\c^{l)4KrRlθT;Ȱ.IiP oqJ'Ӡ?EOfb2)݁ y[MwAT0;?`wך4w ˊ#^;))Y2ؗZ$kl<\x]Uד px\Yᰅش=Z`eXhte?Ֆ{zf$*414RYMFś{DV/ Xi}$}_Ʊj)) > ([,)2S?v )،QF%5 Tl)}^rdwxXHb(k+W{<lLV|ײVT'͎^}mTv<_M h}̢7z$ۼtv4e;K~vdJ),L\ .F˟QƮG2A0HnUb4L' 6nߦԜW];ɸ{e fXwx測,]YfOvo= @cfJlȠ 8SdZ k<B2AKX vr82bZ:ZNyeOJA\LɗJ^A_ }#5Ť|9x yklxiͺsx$Ì *y>[?w^ U$Y-ZB~F m^h"[d|ڙg<S$3T2s sTdy[/Q^uVD[;C,̼ ?Q2+j ͬ6L4& X6gg le~+#ZOFI_<'#_' !MZtxyM?gY6QP s~6ɀ«Sʨ+D,wP?֚<``s4B&+.| Rv9~C;Y.n3 k~i6JfQr$e>Rd!)z:-69+Iүgd- =$yGlQr܎ђ\ɀ~dР2z:Y9~qYѪaO\!O}UVB92>(s$wN;w{h.,C{'NMNJY6syi6|>_?}Tnu5M(U#O?㿼DֽF_gbs2f€Oh)ˮf%i 4@,8< K̋./ƾYϭo]Zd48ITs:e2Y|pY|LV> %="æ&I]x6.R!Ud3wb0te fjqPMg!JjD:`N,}X00۔ v=>L7" ILЫ)I:(wNUѓq|huχ;jU2~w|_g/yS%tOy6Q$'6d?d?>Q0fq6!4 #瘬D{f f%u>:l4?}tUVh( "w{wqݵF] nI(!mf;+_?{֚uBSw""x岑׵]z'rɎz| N[6٘!̔ l,~=1V|S0cEVmVP^hϲF}6c^sVbzZh:J}|v<)6 v(, ÎLiRgȹ+g)a M?bY~ֆ=`Y>i\!ۿFq3߳kK.2pn3\hEq:fjSF7Xf4Elm\!&,2,(l8D;a3RITaNbj?N"k(bi\e8X CӰNḣ] rN 6TjLN[zQƢ(X/Fql%6a/Mi5eۣQ(6*fC"W7#>VL 0Hi 4`ֹ% B2 b\4/q;#L+]ɑޔo)QŸ7s,ЩCGx$&CK c-bL]R$;uD Fc/ dTg؍~ьswΔ-#@;Ʋyyu\)}~;&:v&vmsM :EY{nBݔg^V`JiOsk¼ƸO2L{- } 8P6LY45}٧^t lygcN:\\H:U ~d-r,Ďy. 29X {2NC!DdB*=6](*6m^9 ~'Z q@1V-2mhT M@J#`hDO3>nLm93խ<)1/ZpU R}b.GKTlhL4cMS"QroJD͝D|_fcu} &-p;D A~–b o8vϤlo  L43m/8UQi$x밦40fLpؠ||D{1JvoAeu];tdQ0t~1 s=o7fסXS 6_շٍᑒViYshReE>VCȂ)x̌$xۧG¢h ]0kmjIR 2K}%`MiGh$|^i7@ T*ݶvR͠3.=g MMK5 ˓Pi]mJ+. h]ۺH(A+ky[$xȃ f ^{еEP;3̞S Qi&a4lja0嘍<oLfd_;)9W`Y(LHw+]5=^}ɦC~}^ܘR:C-`0e0Nh\ 4}>Zi R@ڍ:\Cack/3doRo Oԧt?1ʾ>yt u`ƌWkקۿ3Բc pB026[&Ҩ*) `Y_m6[7*X0=#qTl5W Y-^R( !I]I9F<Ԛa=ːw"]-,a_;czx_%H3U5u%в8f'"4" s̉,Бͫd(ۛ5s̚QC24f p6&!`kd5 | 19ޓPu7߯B"c]8`(<)@ hOLrmΏE1P6u]:WMǩ{qG~B{B0 -RUњ</0 ӫrj67n:03jfhRDZb4G[=ޥ'T{7ơjNnkplK@vƭIi&]24Rh"Bi?F@Ck.X1~3RLk* 50ˮhyCq"\bJ\F KLHm"،RbA`3/|غ1iLJ͍2b{x>k; Sg虞ٕuvV7 9kXңßlhWxY^(8[ѐ'@ýb(40D>K`$8E1 tʦT< |1ds78Ǟ&QYt*H>{K ZfxN>kԚjt9*啘~1Q@A֕5)` ]p/ac`LA;u^Ql_[ih9fzc܌?\ =~a5=LUCP3OE;Kke6.D96sz#ixX\o_u;;[ogC)T(!jHDd6rL Q39>Ro Lkj(G4|/>'xWS1/-HM<`tR.32 [3Sr@MqS}'=$øi ̘*Y#tCA&X/SnAHE1 CF5! JJ 1ڍZ!̤QU9X ("FEUl*Εoyr]1# -8Y#߻R5s8 C5F'Wn|ڲ5,SO]d+@Oi7gdZH90 l!QD~+P}ESJV)" C0y\3r}2 pȈ*>c%vg-]ct=S3)s0ajvɧo~-V"BLF&詝:8v.N^C҅F6}‡`At/}F?:@P{p}ҿF;Y^l h͚3]Ǯ4͘N?2oxSJ˒9)[ ۚ!^C &QЧFy!Q.p@%PU^Ι6cTH5I񺀅F\'i4eHx>_@k$ /A+Tx3'Қcjmin D(NH@1 ZSЌ.jXEi4_jٓ<~#av(YLi=1|=0D1ap6>e27p)(yn+Lg'.7O y{nHe$& FF̽Q}|8cu%='͚.7 -jPilK,ؠYSN>_ú ,< EbT=FU#<c 00s/fs1`1ʿ[ZR ب:bT/h7S@0aRN]CvfP-$@rJ/ =̱Me0pDx5,I9k SYO" o &_*mЂ!S选unXrzr>[[RAA+QkQݧ53 f܍[7R35ԃ 0^Qx0*S{G$?5[Hx;bFB=8`e F2@1B?^VaBGJ ֵhUZKjz槕HjR/c͗P}2 _F| )h id]QJL@R@"Ef#kKWڰ9Ǵ442@8j1+tSRSv*!^;[ʭRl*@IY }ڻmd(Sf`v[05?DvS9E/\$l5ϭfT=t`e,hѦg4hUI70A,g:lX20r&~]e#ۍrj RʔQ俹)J~=#xmfZO46%&q<>Aa#03j[hRSB+0v3 Cѧq/Lɟܤ8粠c"u_03l$&e{?sm}Kwq)(_3{_ 9m~^YY-Y8`r<!3h5apT~D}'ʳR\TOs?%W X~9 a>_L3㎸b1WDrBI'Ĝto̽z Na0_~65', F AFh O[zҳ`ݟіv;t?+`јѺ*,q|@S*솉u$g|s6sʱy:|000*sU0ŝ3Pܻf ~8Ƹ˟&\dGF:"Ek{u`|F&x9J12/ X׌|[yd[Da4jc@D-|wit|s  Vj#4\%BvC\9UuLiyS,d9!-醐͹pnE機1n*LuƘSVYReY`rmSi9`^6lҋNT."2V"2Ro`j?[`~/lvڡFD-"6"r="lͰB.5[mAH YU#"j#b:c~,ju7"9sH2cx_;KBkAh-!XaS#μ]m ; fk}^ e0lq1NdL T[Se~GF##`+@A A*$"Qa[pv\| ʥÅ]7|.xv l"{M`|lw<@tO/Fbff|c`+jd oʃ{w>jih9 Y:UiV,$V}+iƼcAa\X*K `+-^m 6Hnfl=.wZ!rB.#TA(#|c+UP*[a;ɀBX =vJ@ܙ +#)v]m|"+6wꆧ iOȵĽz^8v"C nN= 1UVآI8oTdXP Gg EZG)^X,.lF`W D(>-rܪ~w`˽lz9 Ǹ`jw j %^P%d+dB.520~Np=hDXN`9?Nـ{ CY)!rPv8/Gl7V#~D2; f30qƍ[Adș5oPF97 ;&w1^cb 6.^lg1o_k⏸HuֻH՗DU|r`D&: 9nuOzc܌Dν05sV4*2ge١Eľw L?$358-G̮TqQ -8EГ, P0`)(L0 "~toA)E@I@F@L 6L5ì-\ԓ"}/pvn9( \c\ǍpNņoR1&tsiL=|w.%ϟWsؐ҄U8s=oDh,A/ (6#i`9")Yd*Yϫ#07 ނ@C" %t#cόF{m&HMG"d=Y*tI֘1dJ SlBvZ-%57~X,@uf(c[~SW{τ)H I7>hEMJ H sU"mFϔz&O]0jّӀ 2=}whLڔhg&nnߖGZ C&pru~7*]A`*fb7?aRh}{bT,*! 1zsSǺE9bF)8zLdi:W<RcMTiɃ@;LT8Y{Ǡf $Fc>qQ?HGkE8r'WB|h~e]ϩc߽$VMA>%6G5EB C '8i"'\˯g?YlC9wi(UF*߭b+1\TJR6d2 dYoC@!x䢳l;J]&]V`Fgz{,"2;lZ3e)u  mM Q>l\}qc[p^)N=Z-MӔ3T7A[s0i+X.4DQl1;SŚHE1u=QBu+RK16b'͐=~&$ cءYF%1 ɪhPѲ'ZMû:əG6f G x2YiT̆@àgvg[Ťs8MHlwu2=/c6G"  fTY`\+V ,Ef  HB֤Ng$Y?CRnF IEU9bi8T58k1_-ɻ4WK6'ؚRZޒ|kc-dS@46"pيܫBsVu0(3JM%]_t\xT-E8\N<A%S bSxu[_#YLŒuIX 4njr ~2mc;i8U(^tnZb@SV4pZS3o.DW6[y[Kۗ4]. ӓo4nm@(y6꼼2z0@L Xbk%J,njMl澙 w BgSDY$LF" `l^yDﱻ:Uy*]CjnU$JOV-u8,WZ!~rl?ԡnlҲ-H< 9TS Lqu j${jBͰtM6|/3h@͖,2lDz` bIȕ>JD( *syE$Άa+B65-d2L [s!Ky4k6”gN wλB{-$J /'ߠ_W23RiJ]Ұ&"2 > / ѬD;zb"*EZnÂTSlB3oGV$I:i9r/0ّE=7_wpŌ0% +.5mLBGÈQ7/=ā W@Ѿt;F[ץ5Yl?]z%oҫfgXz:s({_jƸ7'I?'זH'OZeg2=e|p6s>S Hvn2* شɾ 49&jxMj9 "_THh riЙ. $6RϠY&L=< ~ljd5+qNԽjߜhq\&y7N~pSKa' {hYhj]quC6[1CP9 [oʠ30}&zKd6@AY eh)dJLJABE;J? Ic##‚MHJ+-< {9V<ZTH́Gjs yH~Hyo:{[Fkg5"TDlFrBj5fD`D$LDd4DRjakhZJ 靏Wtʰ?8GF/c hԥ s&͵A-[gV>v;. 7c:Mޣkc\nLHqvG6pFa)Q'qJ[~K42Tș!jAJP.lI;}ʿgCܰ{*-!=ȯ{3߯ L"Xj Qm@$0 Q @oT`tʼRLADA(v`R-Q@$ [یY}J#%s>Xc<凹r2CqyTsl?N=kh'/ ayM'S>O#OkS#;=e{ Oٿ+OC-rh?,C0DV8އ^@P#]t^a6yK:nHT AU|V=F9APRi*˳-MD<#\x9n[ZOsׯ^lnn-8k+=؛Olppw&{yMkV AhSŗ_`{x/fxd J:k U #DX6r0mg?#̐J0 pK&-DA R^ecNq!2c(؁PEn{)V~})6ˌ=LU@XBWYp(Ȓ%!8;r8 K9 [nF]7<~<Tt Gի[􃏶9_94w)Fs#>ʱ$IdI4途';.CE tB H"ajH*X7筁8- T xXPQyfݝrǹWqmi=u=r=WLgrNTU=N:ABd>38Wqҥ.}=cX=v7>>$ B 3:(i1j0I$@En D hzw#J#&~iȞh'07tPR`$~̘[(Ge-VZ #I8ΰ:/l!*JO۹ O0w*:Y;:5 bmz0gƵ4mKJmqXeMAЋ}ws56y DjX6001PmhUEFALn4$V / d^zU`b?7IJ%c\kFF 8gQwO\`T4̶y)?;(%fRzZKgJ!lxd*̜ rc`'f* xydzO?lb1tvRNxt1=͡kxJ87!~ѻIOq_GW[̽ӂD Zt]8h#g$BÑ\B4 EY,qQ#!(* (J7o}zq_/3)J@4mv`!ը}v݄i#k3͞_-̻H/ŢtoG2=5^ay$Tn!*Rl_tɄ~(ȫi}2#[!KXp 8;f;ٌM{I>r饗U '٭gC9>ዝzz29*OB0 >.'8M3#.d T)Ps+dd$zw#݁uAC/2nk CN"P]DE*8ht6)h<)5Eg`9h Ɯ簞.&=%V&}9u m.j+i.O&AUH @BO%ə4 D Bkt>Da@a Z8>X1 ).\zߎ4R-0Y0=`5aǟ'M3Cmtf@`"hgBVWA񟦒qK5em|1g}}W^{t>]Am[!ɞV_lCG Cz?ȻٔjЇADWN$a$0i1uE5B {JHn3 LA)ٱY !H_(￟b&xSh/ࡤH`H)8n  D3\`ZZ%!5 seB \{Nrv׻Z^dh`sgHpw6_@Ѐ5%2-Y5D*iľ\٩(:yKdK8, Qqo Ѝ}OsU~U Gf͌W/_dIN55=zTuklN$F#d%2wEi.ړ԰q&*˔8@ݬ)>m!f.F 5=ei~K,Sjyp3\`24 G?&Uc}chD4&V}vvlow)5#FQh~-USԶ^U1:dI(E+%#&H@jR xcFA diS'vNw?~:RdH6cfX[g~)IbSwn^3<9"VUI%c> SiC;Bb]lg31r*:lmlPOk1Hm <^D)7_&Չ`|s6s !׋L{^^$TD#Ac0 tPw{R *Za kW^׷n0Kd{snlO|/?Eo^ ^ DR tYyY+yzlCO!ALJ)nr.!p£1vS~lJy>x8]/O!ȳ}kقiSk{ݭ4 _y?|7^dsTgp lpe(B0Q:4 %ziZ ( TY9A= sg8v=$J#j 0JB-'GEIp0πpL%}zͷMj {ۻԳ1Dbir Ώuif39Nc>9!*08HhW1G `HNE/_s 0Ѹ; .A<{zՍ\~E9Ѵ5{]eg7g|bL (7&78'܀Yx$!6f h(LheD \l&,(or&`ʳrNLN&] a٨ۆЫX,^Q7nf||R<T697(lfkw^bfdq\!xYj"ieA'O C0aS+QX @FZ9Xc%>1ٜd}`e} U@\Ras,\]4(G'X=r1kI;D3M2N.,NXLlR +|Ywpw) qe DxLH#Ar"Rb0 +JY ,8F^r I#%EEq'o'1Ep51?c'+ܲ3f>[ж TgP]ؤ9XЯ,Sd*9~[P/aA `F)A|5, (|co2 ܽԛsfc:~YGv88اkebXY=B4MsEׯ_ROOzR]3,&;6ccfHoGݿ ak QA0r!*!K` OR+EC"PTIIO:H=:;NN:Hs\ں&T kepw$!rn8%5p146اe܍mvwP~<\M_w p8r;3̋E|j-XEE@ϑ 4(Hu0cs@Ф"US\`8A Z८eC[lEl #А%Cdf;s{ )6JߵF]%BJ-j鵋$sx*U( U 䄥Wx+Opl _L3jsuCj&lb%e"nXL GSz:m>of69|Sׇ4r6<'&?D1FlF*FFǁP$a ^!mA90Bv|Gt~ғ7wlԶهʯ>'SKn/|) (**Ak#{w {9^`1*;S?@#fƠ JhtL&]"c,%7ue 01׻k.ef̗ f=Lt%UysDj3:~;V 66QH] Spb  8QB)> ൯)Xg'(`SwL4 y4 bL  CWRQŦpB=)ITۗXN @.F-7O4eQ! 1TNGj߬*KF]{W6XrY677dBUNȲمr!D7?4WU5v 4$MҬ͗(4 ͓2Z <^E!R5z:3k?tňF!@ h@g9^OF&+W4@ a%bk}HKM힣=U'$FSUU8_jT)ѐ4Ll8Mx4ؽ4Knqu +DSL1] 7*5[Q5">$Xhjpu&6Dfu.LJB8$ $FOyd]q<"$H-Ry2ss?F9T~D,+ Q%ɋFN2B+ OLyZ$LnH) Hc“5%'kV\RmNh{N*W;!A+рV^2Lm ""Eޚ~m"{X^ys{ǓO޸xhfN}Ƙ[T##XIҠc"T夯\4qF4 ,i!mtC%HKÍ=" (j4DBǀ.ؓ/>Z݂t'PL]Ѣ@m3.,א4W6dLGSx(" ۦkc,ӳ4A%QLP-:TM+$ChO(ZETYPjMY@2QsUPʲƨB@.NuMPR@"AE2VߓZͰ *dJ/V+HN/|.hL<~ ^#d6YEJ?[8cj̀N:"%:* M@x%]dXp`@8&La &c7RL}V_ KMRy-[cLY\ƵF=6CZq2j[DNov]:M4{^}G$f&Ȭ!A4 Hˢ^bßI33X9p)$kڦ LPiMO>@օW)jxD(h[5-ۡt8u n'x۬ zn,L 0[!%ev-!5F$1H*4ZD~\U51>{Lǒd cD؇o'#!k!*EB%Lj0Ey/\HggE'P;M3sYz(@iM!^ECH qjMsO6S{/`[Ok\܇1:4 -,-V<ҙߤ뫞@S-(ڭ^(~ռ0sG46/0IBS_NMSh\$MeAڧNb b_ZUhz̓=1J)}@T& x!r\S ";({9=3cn jjl/oZrcxfbna<ςfgؿ]{b24-:{$E m2IgM B8 >k|߫87RY}bDAҙڂMF*u$AA*@WFJ5*Hk{<>ޗiY|B4eg5{.)Y\L4-i=R3Ĵh]zWPc6A)f[Ú^Ny@ V441d.=x sAU>u] zծ?"ɿ䁢3olv|]4ϿhoYxۗ#q&PO2 X0UOK+mh F!Lbh!܄F 8-Sshmƣ1 FLPiD @_dan)Zoml~gyJkŹݲNYȱ#䭂vEݦյdEN1SN#",HSO(FNx\bjAOY_.k: |j7UT28q>ff'?`h=0%->E֫ĿTOC=Jc>SSjΩ'& ύ&#ЌT2#`PPI9)F5"d9#ꢙ *Dp@ACsi8MĈJ<_ {}Hδq{%G9ṗCw<K[vr p"YO<93olvƃSDk4J*/,y(3zmEGyhuigK1 ^p}eP꠹U:kۭWkbeLO*mKrnZB0^:Ϋ0enЈ4avjNk wU~q围E^vϞdyIRa͘]꘭-° yEMu1c[j m$A2Buq#S4D$U0O$ T,-.أAM9 %-̴ay]TT4b4Vkb&L&`ʥAs}ubzP *؃R΁BC}ҴEHD-j6]qR+rIn5~ !a1A ^1OQRwIͤ?5cRk7C1HԮbxvY"vdXc +wB!  LNJD$ҠC1m=W>QjWۢ(:YQuN:˲^;s"iv{^ɂlI{cF6_[qa_t{wΤ!쩰zP. A\L8f7AmBS_h)D]+qŇ,,ZN$ꨅm8>ha=wk 鼱mdqH9j Fhw͓$C 5HjKR%D6`d|*gscgϰʵJ3okˈQ X- iIdNaW^~q5Ÿ#/g嵛}GxyH>1xk9}4NɈ<ԣ_m:ICfq. & oX_֨-E^*/W 4L`05eRBKm|V7XP"N>oTDq"P=Trj5e:b{9rAN3ݜkבTbnIQ@QљU68M'kw8o҉ fhf D 4ZdEPn3pXQT<l~DtmmҜN<*#-Z 70՘p eU*Z|My;_xoƹ?+}?؛GݚX_hCmvIkO0yԣd÷ǼׅA a* h1q7+gVV`iԃeфm]ꜩ2n½h-n8>0%ʅ <'U;I-Npe\v9p 9pFϼ7lάnV+R5ʒV˞ 5_cRS%:U$*RC"Ԣ$kCF)c457sH-}ѨըAYUql T^F`cTMZ4w \ |&nT3uRD*֫Yx!+z̜x֞k⎀o)1O>o < 0tE`-#"*=15I2nc޷l k7^H^6I;᮪ˊe}s?l2n"+MgKw>C|4qiai0r*iWٺ|_1dN@ԚD{h#ŋep"nT`H9)@LAF2+oY522 (@)8 N@s'Yٺ5ưn?3i^/11 2#vw|{W}$%y/<<7 9݇uA pFDx0gc1y&(e.'equtfMXi4a6Ą׉@0'4o~#cUdteU]#wG\I+/`T9kocrwի*y{n5g>+}5p%@єXK8jрN* ox}?@hTI8zUFrsFnD&@ȋ;Np&5Fajc]"j·>_0_9M=&]gE"$!'l_#!8+X@v%h4"F]s5yjHb'nzfvq]LQWd_aէv{Xk7' &\n+}5ϋSO.^OTU?ukv"K?v{dp7F_# G!fn9~AHz]8$d#wMuQeCrlT4ڝhQѨ< L<4T@ao3*s7qwH WNŏ>W!PCm(˟]/_`4ݳn7}[Ҕ`HN| ~,I:~^_z]aqSظBMO$j> M-IXax/{xbޣnN >$) fp Ijqⱝ݃G! ^{L30GVYWp6{o6;Shɡ$~hk_؍iH}Qʋg^d|U-:w[0m POl; gFD@wNCj, y iMkRߑC?o{ݻ5嘗y?In ;k0h;|Ϯs3ٚ(or$j"/LoUڥ+yuƕug.^Y{ffQӶ$D`׮`Lf!OYkP&\u!XPEZt x,4k,a@HM  mU qE]&piMj`+5]W kCU2Obfrae|u#Vt>edh2϶]͚ ^+0;3F`F$z$4D=·Gʲ4&IlwjՙE1eR_V7V[g3;`3p|mg|rk }+ۊ/m}wflx٫duA Tԫī3eW/Qon )iXyLjc ǃN|`מArǃk7-̰gԧ>k/=~y1C!3yy9Qs5Vw~2044@B}㋟k7VU>&x\ ؠ4%FW{VSY+ Aq61Z¿ņ=ISJunք bSs>xZj>ꏓ{^4Im- fG ͳ0՛a ) CIZ )<".&m~ ncL0Fyuql8G)7oC@Hi6oh|wbtχ43t=A"*WKm[MBOVNݼ؄7‘}ײWiO1\Pb,+%MMR;.'rڥᠿk~zlv:e9~&iQRLa 4S xHlq7#04( uwz+gtN_]e#ӳmXTƵ5koaS^*uBF ^?ҁɳ6v1~x8f1-ʀ^y/< ^y)7} mbC w~Ց,8r^r=w"ٳ'Y[Y_I'e= xS{hOK-(R _8S)'%*E[֞#8$1:gDp_< SKrh6 I^#-^'Y}4t`툠B#:׎ @=;H:~6/fw=Jv^}.4ʊBffhx<bx+ּUof7şkWw,[Cmaj}u"BޝvjËmѽs/ % @-Ԏ+Q:&v{q`q0V:M_ȕ9}g^:N/q8\r7/_c3Bt~`a# W+yFt.ӳ_nη% H@A؏ ^y$] ) }]x{`@4STclKQb*.@.q!VیZh1h51kt4- 5 VG#7 U՟_{dYfX`zo7g;Ҽ*ʻ1}v h N&USOF6VY~+x3 .fr ;#>íP@nIo[bb=K)9|;w=]SE>F+ˬ\YƵܼ57G (X b-' ٞkqXԙ/KH27KrfFw0/AD4Aҝ%oPp_{<'uuq{o㵗Ot_]Ln?]Ofsamk"gt*C2QEWLrs HDIH4&=⚶m1ބcj_k/>mVw_4ݨhhm?p//5 Uf=QM"P,ݓZKLk sdV6u"4$ 1LG7I o8`# 1-TN|*}_Ʊ-?q+ɲwf~<ܱ!!=|QFMeu:˧_veτHD Xv6WW~$6?e|t/J=m eH#qSL x^BXܷpk.rlr,,v+Are}5a$ ӽi< Lnw +\/ϟq8ʪ o4M}4OD9;*FEڻ7?ξr UzoGM!m*J.^xbm",m\:+_|%>7uˤ"`+婪M(A RKӨ?80ŽVצ]Mg'OQRQ4<D*|EZ!U79ƳCZq#ҙELW/4ͨ'Gi0m",4X``tfvF|uhk2モ/\ I@ '«HC_8|i,҂ll=v1&hd-TcZXps VVG~||+>t"DZJ@dy+4HSjo~6] s\~-[,BjAL gI7EOmWe8 tbqFSi™HH~r]ql7&Kg$M/aROU˛}uƗȃa4hf`S|C_U$q#4G4SQKs&D[JG9c-"#*ר!ex ybРo1VW;~prh8=wG Ɉyթⳬ> [m.mN{E`lfor"60Z6R 5r^祟z?sV?8IS|ŷ-뾋cJΤTzcJ VhB,xQZ>`ی=\(P9uUmy7'UV޹MRVi1_ܧ~k9QVӦÍ_fT\=p5n֓$Wd#TjM\׾rcB=v \̅[y!,iL9N(loJwsc!fC<>VYc+5HD o99~G~͍`=SS} <?7e싔PQbZ+ծtu}0CF[ߐZA}wsZ%foỘ8 7WԢd}.\y^yss^^cg}e1kxpʐ/3n`B-敟y 9 F mfwg8SxKW|/{/LjG#M ≝$#2&x]J+W~k E3Z_0 Go} POTS|*W^xvERdCAQ Y'z=UFajdC9p xhez:4Ͼ,Ss,-贸{{ 7v+74+={@@*xe@)ݰp=<ʭApq]5FX1xuPxH8Xdgc UAy"K{\+cl.d"hSg GMFTh#z `d[+AS9A~:(qzNV3Jۦmh ._ýilJ񃭭t]W/6ܱR5mkyDAhbys#{ s*ۯoj=:=;;$u&CcxL4CO_ύ/|_!FDf",! BT:A$6$6 觺=}5k׮Jx` i5nݠ˥j/ ƎG>=6zy|1~iK/k1"4U m,C&O-<J @,x]"MbQNe:;oԛϼK5t*|}73ƐQ"d-W>ڧ_gsy- $Efwqa>>L=2+GH* 2c}֩8Hn(yb:I)]ÐMnp͒`X 2Z=ҠxZU|F_O}sk/b}<_tclvl&1f:اjW_>AshvZxY':.km 0$GM2wxVu&e9>􇾋Y{x'_| gLٴmw-e{aߡ8]Ȋfx|N}蘐4۪4L^- >_ |`0+>kB5]$Oi])hJ.'Z1>akc-(JQcUpZS-kZ3j# SO|14 *%H.f_m=eU)#9o% 6/yGkv&,) hт-ߦ֠.<8Y٦ζbʹ[;jD[szkϪO^LƓr~ب>ld_,go`IT_];=veʿ#qAk6ڋ{sbAhS bzS9n= 0i FM6\5϶5_F7Of+o N*'?_Z?L&ĩzw >]?rs;`N;x':~I^v/杷z_ˋDis*p~'LO_e/jT&!O?ˍ8=q?~s3h}ˊjRkF%VhVSLBMmhz,>M)yf s"Q r43].=z) -~QnhicD! O F\pK7/N{/PՕLCZJVMZ7r!=ЃblFsy"h$'[Js>O1꣰ Yk1}i}}%R`#<0wgMnҏ&~nF{G3w.X@E²ۀILp/6EW69ݶZJ3:߳wPrhf)9`;L?yxD0q@RNl L9:v3SU%lBI@$\l_+=w#C$, / )CQE3!?\vqjM+Jjb1iGIl!\_:e*39a/ʇ? v" ޲0Lckb\ks_|<2b ֆ My/F3o# s @!LB0hַQ06i,fz~{鯠2SB$̦`m̹* 0g6H R/eYAXFX}R>F |&i{fm9;]%T!>tj 5ig<@_pb|x\K6זМ@~Hdzq"%vQe[4@D[{6eӨ >M'\5Ox9LY….qiYrwЂҳ曻g. chˤA4B:Wzm]+_~6fIox%2 X1%O47=P>́ߥ̹xbXegų"ŵE09|VϾcڴgnCgOdWS0yreu'Ҽ+8 9'0=1ZXCXdLB^O@&<˳H:ߝVeQǃlԭ] OR Eil}+Fi)@g!:ҸkgA 1;1H$BVm:ιL<( K҇uasކ¼% rg>@ aj\M+d5#M2 kƾ?Ff`YƊWڏKJq`5zSۿaѸ$ähmz_FO$Z`ʆ*.qvqB8ȭYs钇>W:}lnˁCW/{!dا͸LF\ Usq~)!:'< 8G4~ >5{˞Xa`֝ɷZR'``fM_3`elD 8<0 2.EHL2NPv%?-F4t!=4,)( lbǛPm`B; KDLI(1{0m}Z†pG=N2:u lg[{ elbDsKİ^ w (ї%+}٦q ɼͬgÝ6'CrƆ+{Bٟ5X1*JrwDM7O !ZCX-m~R2Y9!m;6UC'eOxy꫑[>P4lqb ʓ(BMK(6ߗ;q`z AJex!~75  vz 7@u|[9% !\EMq 3X >` `IjX}P,&gĔ~6/_G8˛F 5mPly9&L!]lXc9 tlzOCطS*{13>E,Q,@sUt,)A%6"AID4HAÙtsh=( = =+ $:jw/-r{:QaIx Q\=4A\pQXx &4η< DdF;qK>9+0csL)'f?!HĆ7%2&+Z4 n ՞)$leœ?!JY 'wqb9O.]`% !-I56qPkaϠa?!bv,-ϙq%[`~6r=׼+׬mET *p*|](V>I$ CSb3,Df^⠥zK#arlRںSS89H*#Ѝ-^B<{z?饀lW|Q퀲.K,B. Oke#)I $nQأ2Y0{_F)$G37h\mʻER5i !,M6l̓Cl?8hؘ3Z T w'1ԆX@a/uZoև_|ڢM;eSNApG>"Zw6><f`9pOm]D\|x|h$S`a>hzU_?ư]sţy(f”Y*l%pm9+fS0jϤi`y 0m21l =9#n3ؾq+6߹cc:1zFTg!,#Y@ зEY\oEB"ᑿ@W>"a%k ! ʓDcʃela# x%h+MtRDnaF$,F纫yΙK>5;?rHT:H8 H6cq]1>3 mK'&(S&c1L|Na`/EG?-B#?lˣo_pm;B/f* C7smՑ=GV?o{:.y8 r͕( /wT䉽 P.H4QH ݟذd۳@%i~dC`:QOەC%Ja#$C6B2F"MCD@k),vw` c* / /1%)+7޹3 &}Ur-6ót qVRaM"4F'z>SdKHʢ-;~}(ukЊidW+\:x2\o ?g.l .Q 撅`SP6݂͟}3#Gߍe -9o\l.ɵbxa#h,f[1UDy?67-h@ACs1pݳw%2T*֨qR#lF6YYlς9[% e%ׂ"e\3+]OO*C X5޵n[#@7cw܈{T:#sΕ8i Y&>&^fgK ^e=p4| @zK4>4 XY;wӣQcw|`x[&o@+%Pq97*m1V#M+pW8޹a8 o?N\?)f @I/ESyQQ#v')2JIq\II=#~Y*i_ N:R_`;q^pzAFXm*.((e3\W;q#5l8 J+C9 %q5 NPAnSt{*<„ah<(i2EOqܲ,*oKA{byw#:Y\v5VFo`$S瘷L<l/Syr! 7Z4^;(r tV]`LJ]Qhˣn._kV4mwSۍt.K KLcQJArl~ 6-W@K"F{G `b\@Цcճތ~M/";hRfԪO8jiʦ2h`މ5烳 3mA^gL̀91f-q\ؒ;bɼFGɘ=Ө ľ_~ ˆI:`G!۲~h4s̆Vg6-fZd:Sj)||DK *_Go6u6-\8O]w&^W`hoW an̛>>^ .&NY "N ?7;R0댹\(͔059E^8Ã4˦9I D2ts{$ɯzJn)W>C-џ8BKTFO{ L"JXRqLg*ef`k DMBl(wc?|0(F=#@eτ! Ù KůelZqnݶ%/]rlG6?58gm[qx 'UNW @!e Zw:ݜc֊L1aŘY/tk/@(lrW 2y0TO1O*}!\WVH&hT86CsLvlَ}Mu|P,D<"x{wpI})Dm/'\FM2qkf^2>8aq59/-xl4BT̎=7'rn^jiyewEg_ɿSh?ش'SRunArB4%OxV\X/ɘ:;MZ h'#{p_Dxp P!,߳ V1EYR9@ph^0>;R:1.c23=Ͷٳjov|=Koǿ2R|N(`w>EQ%8ECixk.I&2 N"kKdfV<LJͳbW2%`'s$*NbDg #C2$4R>(uGϟv6B+*py Tlz8⒫&@ZV`He~WH8t ۿ! [Dd*vdz^,8n)@ ڈB͎PEnWq`"N. ]?'FS*)-$F D Ìc%ͷ? \%HXHdrXwa bBPA`:Q,kp?[5*LS q&} )3aۚs1pXSʘrz ŁèmrqTK%+"K$E?x:ܣ32~!Z68oNhLNJr;4uRZ1Z]{)!Lh[gPSдSn=oCסu'?O~S^-kshB8@+ fC2xYzq-dD ,) f4%|7߉20$zM\{iށ=_~;j, Ep ,L e6\N{> UȐ PӘm2O 1e9ykNqGc_ƱrrhNZh)La Pr,gnodxm;<5V MD&B[am۞"z=VQn*`%3 &6SM\S(&20ki ^BEL]1lIE ݜI,4i#$HG%g;8)8>ʕc!fނ6**KN&2s!%9lT7]Ƴi9{5Ͻ&6S9,M@]SLfrl:Me>EYMn1_e/^ c/3f>/%*#0=pwN$iLFDP,E@jV@m`rt-xsm2L^ 8)D6t> O]T!"Kfөbrsqt7՘p z?Ru/CgW'C_nb2C|EI Gщ9~~Ye (ę^>.Tj DЩe}f _a{De8 i 'b^Ll+ƌ̊Nn,f&I5 H JRBv ՇdL/T~64k ȔMxpꇶ(( MKe3QWaeQaղ me8W?z<@OoxMyi68%qxf2 4Q?@ N8idgN<$ˆ0#o no|zzf+>+hlk1o TƒY^qdzc|0W0Pr/r h4O xtH"WP< 1ue4+tzH [mNGA3D}IR; + Dï`zb"e HP~(* z&'\ hشbCce^}H<"Wwvao4M,Xn=wѾt~<\;`j`0*Nda[ږvh(j9᥯{) 2 !Aq@bQIt ^0{7:^9 Dt`qǏNŎ5w^oѝi3wApup_b? Y|<}Kp&hvb/C'd9-Xlk=Mq%I%3&INЕO`R<DžĚ±2hoY+ !25 ئNl< ȴ6ZBE+-~܅ Rn,˲ॲ;`ޅs/Cw>,/ \T:k2JEe jg;~y#f1=1Ng2.>Ipf87%ql|s_CXr)ܧbᒅ4 3#Ӝ b:iT) ?sc4Q(óil{3a`QPIMP&}ѵ8[4E]g5>fnwZ{Ǒ.^\iSllЇK:&WҙލX,ƑKbfۿ?S)ڡoEg@m DCh}0ò:R.L[A0"pRoMMJV7@?-iN"m+)/'Me3N$C ҈tݧh%:-Ao BQ@U̔Y ~fo%}$<.otO{S[L7, T3YͭH)}?KI뮸yCn+vxYnve,1bnqC b'v\*ïבH@= AE0$to}Z b ߋ޵LqHb|,bbze_cu'ূ BlzBx*Rm;o L H3NKS9NJI@7&ʿzJaU?p$ɍw'h;Z`iRWZzsLwruA J3YTr\' skj^]@AoCmlFUԀ!kPa\!߀h9 RfD x>\㬿x=ڸV78(u5=5廸놛ˆ /@{g#ެVqQ8؜|'Tpr R IXu *jߜO(LзnLMgd9ù/y? O û CTEkpj\{lf[ ;фN M;Y pIq6ބɛ=^428n:y;?+58<@G"ʶuwN G-:cwu?"/ٯ%IG)}'Ak`inP`Y+ش>8ı/р0@i$h' i;.HaX? W@!CX+brfQ@uÐ,m&E+Wbŝ[b]Rd Y\zո2"X h>=:u!P)/W$/ pC.s_17{ĵs"vǮ--9I)nb|y/6޽ /id2a[@B?/Ԫi%:1fBYAzmU005 11>L𯿊e:z;牛eַuYZ JXZ㹉Ei=گh^d9GDh3:XAt:;^n u4Z`2`KJ)+xImp\wyIc[!Gخ#t|P-S=4z+%d奧9~bBidLa6rx2<་!Gb|f_W8J ^g<l3A&b^D 71<8o0vhJEccԇsO_<سi PiJK"cc7ޏ~0(N:O`(/ͤ L{6oSrZ-iY^hm-hp9W-HxTLFJDv\)5iYqj4jCe ž>46kγ|-=:1Jy}/x:."wuڃf&j"߽Y̸'`Y5S`: <1>m[Ə A8ǃ纘FDž<=IXz͌-BJ#!ivkDf|Rd*x`_ȴ\+n0f3#-Ӥ̀zmMS?|2g?][ލVD lZqN&ɤs_k~5JBFФnoJTƇB.׾ZW[os-Xm&S޾>,]u νl=Z]49'V`W&'1xeMZ\A[ݘc">cvLV'c#c]6sJS3\.t:]$ɷaźsqubu & -6:|Ƶ=Dq㟡Kȶwq,J1}~IP?K%^[f_q g{z 6՛Њn8Е*ڮu/훷 ^ŢSOeB'uak\1#.I{ވ-wmҽT@N,Aq>$ο\3 30IIM{7 o7z)H` ލ}:LuaS_jz3\9^76L}9.m-~pM7c?GS{m-AhHWS ]WZu6KVUgO'֜wj,^$p 2l1YiVupcć4I(6=5cp_n_ Q twĻ6u&.a=-\I̠8=;wّ09>VGcه*{P<!x =Dv<&cs3JiJ<.d@<~[n}VRr#߷5شvMy }==( L$u3VaYbi?tpg $8p֟g=s=3z8qvm݉\v|^$iFSf",L:Kn}8c%l>{:q`aK%H3P HFlNsn&XWhXvw>7?Y/CG-~ht83yr(' dgG{595d.[v%V,]%p7`隆&N28q8mچqش%L4"%[pI_v AqmtץRe+, ]qDZI/]B8ulKĚ7M51rrV=C~r5;!413[۳ul ߈ش/ړ^rmY̵S);eiY$I\˱ _0lnzkGܫFW3n-تP|=a\1']/}}4 hq J I%D^4l5HѣM3Z ~)9PKg=p2m,}OQx ds7f;r5QLۊ=Еrϖ-"3Z`ӊV\x@ 87쥒W) 0zpg@(z.tl~Pϸt \F=AYˮm;Kg2GO4YDJҀ3|'hP3s.E+vC-f&JHtEWo hߏD&t@eѣG0m3كÂG{d]\^ -i66h_ʄgIC̶6wbjeɈhm +3شAܡ\&we".?A'M:XXy}h5q@5qal6*/s)|0:<ؿk/iYuC!Fm%%S4فa deҩ4ʵ2  ;]@32ٶNVm_\O َ(!ep`&! е`Egac^¼fm=g|fќ 撩9wSZm;l7NA=zSG lZъkaGC2KXM6H2nRJ3m6aYv5yv(PJYL%R;80 ^(e_P E*x-9cʦѿx>t#˂L\@u]}JDR(Y q@ih~߱GHv@jy|fY”Ex@pLq\:*i kR Y vBaW i, +q G3t\܌$4 Twm(N2y}5V>U*)31l;"Fȳ,\Sl;HBKNn[D>rRݽe Q>^hſL8hE+ 6ϳu88 :F'r }}s0 #)]דBeIKX"j(O\AaSa@.߆Jѡ1Rw_~ue4 `EET(љ~b83ӌGj)((Ȁ @m#/qFd=",d4P1RXs/a \) 'hׂ8c+Jm)Hd`YIAIa1p/b(zXgaR8F?."kJRI)Rxhe6hAsmgk%Y{HWƷm۶m۶m[z1}BXQ'|K&*Nfr\ `X{3v@Aic WW*KZ0PV?#R7lg*D*Bf2A@֫K$CZ㨙 b{0HH`,545CM $u5l<Y عyWֲ݌"ln6cG+|-,-~qK_x1X_#!mcoRS/jE_=G_r$18o^|#\ZCi6kBi~h &aiP~7zH~' ѽ?uLؿ}c>w x7\8Y^Ez ђMFkU)*veYNwO}7Jmww9wǎI u-y Dq嚜 ׼b`Ah'VU{>O"N+7Y׾Hk8'[Y̗8г) >4J)ZC!DpUy0ͫQpu$㲰0oC/.K}8|/ylbl5< *:WIKUVAH! jQjy7EY"c#0Z2PYk{h%-^*;nz^g4?~"HU7{ܺuyH1,,ܷWd{UG'1Gg(ϯb3;:Bj :~/ ɧRpHByb74A.rdg'J /dDlm8b't gӋEds+RgTgTZC{DV5{  sIhxhɦEl߽|6d:gQ B2&Dh4$ih(x>|tcAg 5IJ=ޯyzσEO%4 qiZ^!ִ2e I$JT^񯐜~F.֏ïz9xf&_~8Ԫ+qG OhͣqU<+RM$rnQMLzQ& C$NPV/'DZ(tLGTTy6~!פgm1w\ '!~c65xQURIEY8_а:ENsY8^!@7!G$F?D]+\FqI4nvBVſ d%HLdlSJ΅ |> i9n%->g"Mmċ9WV5y/Q<Cl,޽z"e g-WR5K Nu 4`&Ut:d9E1"$} ?(yȤsNң7X6VV֘yX?䄜G 7Oʪ)&hQEqvvox-! .ό"9jD'x<~v$&ςzU[%-ٚVɳxs14qqp$)^d#Oms6Zi{)TP$zԩp9p zتs2#+dI"s~$"TSbHx=A'!q0ݏE['"4&8{SaOC1HX_'y <$B_>S0,ƬEK6-Zi" :N~M4̋49 |ĐrVL3N;K!*mrFk=(O2A_ %A6,Ƅ(A)x<wP=L$F J6pf 9`LE ΞΗ{j=P  P +t1s"HZSB6~ F"]4d"€Rx>sBu!H㘵@UU,Xc -ٴhQnE#wp1|P4B0Cij[BV1v5@+&X*1$9./ޔk "!4pz* 9-O3FG)`HYȈ!< @87S$Y+doH p=K̝S pIOs LW+87\nXAQ5* pdI ?~K;,L`W$dӢs*WQpյu(7Sk"L@k{l S VL^>J{#zdoV;@II"(d2aڍD* c @<QC :1b̯?/}ߎGB#"#|=Ҭ6 t`˲Zib8\:e0q#JĬO!x*-8HϠv0ꀿxΣQEE<ۢ@sp igHhIP$Ok9n'䠵 h(4*8'Zs} }xMO 1; 1ԋ"lxdJHٖܓ/8 cY5Zzwgqwj+=sVO{ ~H\Ap~qE"M aRlpƦlZ66Q4/sī-lZ0:(04l:E*GC`I\K~`CQ6=X^Uu.4^NxL^R1 3I>?~bc 4$DAexScsPXtfnӫ8'M9p$HiE& HNBstOgNS@& =S<_,--c}mW)Ý׽UK/$h$%DOhpͮ> ]fw7A19Z-ٴh9`i )gd$=x_ 1TKݸub_G0dd 䦲-,$(IJrH:EQZ~x iaYp$JUJSb1y9uCHbإEcg NyeeMmSc5 g)ɶe7M&KEQ"5e\ٯ_$׀2ɅQ"l"˕4ư߃1* ђMLʇӃ}WНn^(Q_RמxnAe`>3aot2K+縷^|Ֆ|y $~`%}Ox":Q[pHLCU_%0EvBHݟ~ nWS8R[Pi}xx'Nj}'_|d-3h81.|4tX@dG^P4\B&N&ʲ4KBВM7F, E@r6%Mp[mh8& |:ᵸو+ ǻIICrO- =cjF4F G}E!䦔Ț):@4~ihr}y?'xq 72mUn(kcdTd72׉:r?q?7^gBzy 6!Y⪪DM8YM~pζj%-1OkQ~\INd r@5.&D$M((K!qDmm[Y.o:Zs H(/h1p󤛁U2g! GK E*4([aA҆?!^ûҢ7ӈwnN*+{~WVyyn'IM:!NnWpG>so. ۶p-h6O":6. )Z-ٴhQd6 0"W&)Xx91Ÿ?CiX2;/_I//ޫ:ƒ҇Kr.YvUl/ΘKq|ҖD)צhYO$}BН=Ԟ ݺQ&;k5.iy6`3 {vMpO66~>~ 7n<ݱctn܇?i+(f7.SbA J[U(tu0Ziтk~oFK˜$I; 0SH\4ב ;aı~{y x:.n\R^*_xw|:^_{~;pu/EI'ߥr.(RqZ-ٴh1gA0ecХHE!懂ZHs+̛ԞnC#2`~pO@$<)HC{!]?RrW:Hj=#^D.? &m~V@P\PiXhm!"!S/'BWp)OG-m5<7_W42kgYy{nrmͳ4sUUa }^ݟ뾟޳c¿,A <;r]BEM0c9,zYISCCR=yg._^H)n^G,9IN>SåV8zaA]߇84˲2McOWUfr<}~ۧ_w& <˜sV:@ %-31$Mgw!3 3s4DU$!]L.o//j2uy| rOu..m}$| H kOlf,lv1}tkn\ޝ;;{{$ "0)\ V]sW0GWТſnb^Ѧ:NX ^LݤW_ԤE Z8r?v,'Wip!,@x1^Iq[6-擈SmHٯK&?=Sy @C{-ol P{" rލH i32ڜ/KDA&qd g`FՍ nċXOg-l5Fa ¨j|6Ak uiSGK6-Z$Y_} BJwY'C36AA*AXGcǠfx|q`lĺjWMf7s.NBHt)дcxR6r{b+:KU%1=1H0vP)< v1RToSRakB&DQ 1#[@ TEc{a>OAO' kj(BCK6-Z~TӲ2&(%I{66NF^HCMI`kHciy9Vԇpir-{ iv5j7' ޏnRnvoxf&H>$̦)vڀ$:ZY` 2hNFj{B pD IjCAEu0cr' 8?| -6"Iԫn[lZ(Te0a8q,Q4&Ae+f'"s*Õe0(S쓃b5Kmvh}8 ōfR~2[:d͑:!9ގל󰗣N $@q%,`Nb+t!Ic(ar[fFe2F4^*uY'\xmĢ?lZahtpnk>x>9N2 H;.ݣ[7P,/4^O5a$ O5Q|%ɌcXoc!=ڤR<PsxhtV661XY{ )ǒ\)BRB6̮bNx ſEl=nϋh4\gCc[ȫ|$gI˴K[IBT?wۗéw!?c6B4CR~gʊӎf HsLoߡ%AȲ}ӌtta8q/ZV$su(@ϪS[ 2j#(36&^e]nl3N<'Nµkr >g*̧S(5G5~13ܼ&7#0' lZ0 U]gmd-ǑM];~J>;9,2GtCN/֜4 @9 47._$]LQ%)7 gxJ|Ǒ$;xDf/[8 aDʢX:Q1 ,%HwnUAܹ c,L/7gWY[̦W):(kQ)o<5e3O]NĖ9eiHJEY4cJ湌1+5G?L&z$ٻq{~7Т4ϦE,J[ 8l:a(<<lx:exgʻQbPm]nQ@/L\vks1OH!J0JB59i$51"BeΐTΦV8Ͳ, تB=ȍsu*NMe )uD5Yd:$ΚQ)T 7Y*3e} AIbBJi~GrG")UtZ) ,EI k8fosx_AXlѢ4ϦEZXcV6X+uW'ЁAǠ6: 2 i Pي(n9AɦEK8n3p q1DL|>,a!C[a!4 8f6N?`r`K cDrs ݥ%de&-/a)m@82JmCtz4AM KUeɿaIܹz1~"PPy>?=C{ғN/$K/q5̃5=V$\|hW+ *6s;"d٧>GgW+Wh2 ӽDSȦE aPb2X>:uz B;F<Da͈]{\I|lj YdxHo̙kW-g#8eSQ0 erHn!}sM?~>ΞE&N4(p:B#mQƷgSF/jG_8OZȲ<Ao ^{W9v?Wi@ &t44c]M04g@ t!NY8${((ʺ:@se.@:%$#hpÝp1}-Q"tk>@@hfߚ^Q,`+m5[oiybCe|;2޿~DZeԔ|σx?k~MDEW,H$i8I1~ 5}9C`@HV. >aH2-,xH ($H"d7~mh'YWPKȃ9v@s`f~8IIMI4m7B&Qhr0 Pi4yqf7F^L'?1b2`SF^޼++?&@kqګ&#LdʀxPJa2,y"`,V4C,DȺT6 & 3 Q/z57"=LߓL/Ljk䐾.vP5hN $H9/M2ȡU!udiL9)YR\>inrhcf^sY~4QF%ؔ͊j-wjNnjF˲(n ~C447EGܻwsyA\f7 x'Z !e_ W8UL2yQL@4p7$>޺~l^QМSqLu}B^3}j8>c=3R*nJ PA=$'b!7y<H<д́ޝC{47gu' -KAi8CG8ܖE6е3AK>ԯk6[va-ݔ%ؔӧ~S~W_mR4MTux^M{i\8µ/}CTu,.ۄ׊_)9S10W4tLd(zJ;tpfQUu :#Ni_ bPfs^ɾЮ-#w g&BR^I >KK)b% 7p<}}h$r]5:P8sm6JP/46(o`S~u?훷uƦxGH,o|C!i2H2a\AGo 0 j1jRo nPK"1Iz x􃑲AF:)@B'BC# sQ(<S H#,@Q,fk9 PxԀernC|>z8 `)x ZfycW5MEqeDPEO41p^Felx,z>xx>;VW7XySBLW_ ƒaC=\ؠ?,ۖqɯ H_ǐcQѦ(%+AP/fF\BqMZח _ͷG8-^E@89pIL=>6|G+kHc qR-u=fe zwL'm\|EFC8F&ti2D 6e\Oͥ] +8][B'wjx+,/ZZ`2MIϸϐmDѦ֐r'}\LMRR +EE+gn* 28EA%#?_ۀi0XBHsE 9D@>_AN=grpl& .<4&1u4!c.cdmЅTV Aq,j;m4K(7QM7|jJ%x|6n)Tb0MGuJkx'IG۴ fc&K-'i(=͚ vh!E!C) Ekm憎ziKo34m[2aiњpRA1aLK]v43!EE^Vޜ簭dv ) M:&y=<|G!Zj=fbfH%`alot=QS`S?g=[[Onapj~BweM&5JJc2M(u&$N;&tpM_A jja4"/q2zn0Nα,CJt}EPHc%*u$#@ڹsQ @[$iըqUĨVk7ۅ u ˰H-כp-Pj|0MT+jKϨjy2:G0-yQz`SƧ^R/5srow.TO|^-2s2MZ#Og! ضͲU,P<!C턳)Z V[@DBz`6Y(Xє;kMFXjZ/")s1'r> A&c;r<׾y^'Z ,&_c2:e ? WER$=| F6z8St깞.l9]i2$$J)Q_?{a3~F>< **<~(Hr Фa =%]`LS!OB$ K$ ^7hB@,%@΅U@\e2MFD⬁,)rc::/6]!E7oC|PEy[\_%tz}hdh7\ңgҗEC ѢY/7 ؞Sőceyf%%7\|XÛp| FtyDdBں8d-,gnz8s2]CVo=zY͎Be|Mm׫ۿge $**g0eG>SC&䡗и~;^G4I7vk0+u.eq,.14{ R)@&u]22@-a XY(%ҿՎeU\qMlsf[E9 ׇCE]; ]LLM4Dt`AnLfYA"R""x2bs<6:<@Ly,HWwk,,wHRP$ ?fQF߿`SK%kuԏx_f oR>%&4~PAgqOGY]FV:4p.Ljڬ, ߄S s ,0-~NAΝI_,ZƠE S/zB SK B7$q __@RW>QUqmau^J|OtF<纼lfwe1 IA NHՉ]6_WufA(Xi:'x{K>~OxvPF`S%/0' o]=zܧT*Ti /`Gp|Q#Dnu\"wk_W_sL&>PuU5*4ޘ]C] E!==$y}N,t`r1Mfd"WcR8 DikC`{׮"/#4|A piaƙOA.! iv>ED VEQIJPxpmq4o)Rw;%x]^n/QkF!b>\_zI2~G467X昿ګVext!Jӱ)`0`]H.Y^UX%, >oބ4<8 s#p vLKbellͭ_98<44j . de 2E^U3^ha6p,IQM88A .Z'I!Iɉwi4$l6%zm4ftSiwKV' -˂PPPhQgA(F|5$~~@l2[xx D^_~-$v]L!ˀ+!7vA6ZB@/Y\,یhG{ W{({7`Ȱ)P<޻P\S>vAe|oMc@ wU\@3a]Woxu}fأz|l6;*>#G{ țk SƅQA |d |D?GؐTEs  x'(*@] QLSo7@@E`dgRF~ai%4ͷH{Q~^]mN瑹(Nh&"By6h[WjXhA`٨4{l2guB3,XRqPTk< J,mwUc[vÇGe|oM؎2Sy\&@%\9hXbyԥ+,XeZ>ӤVF7]|_#f7Vu'S<8XzY`$\xf'?AQ44Lg%,~1c e):8J^KbCxWg4w׿,I0NbӇj.]C(Қ]E8_#*ԦRƇH giHzm8}Es9yq67z2t$sP V*.=R(|7_8έ?r] ({ l91Si4R4Jj۠VtI q%څ d-i}2$)f4㑑Ԩ7x~3]zaf!. /-7 ' y۷q} ۶ 8V=zh r}%d_CXh}_206Hy[P/)}uvܒL^FǁNE2<TevHr F8f{mZm!Sa$Tk h< H9$;0MK_X@Eoe )u F()nW6_W~xJ=s_z7؈GPВ!V.2 (ܹ~Z0 Y[=LmdAljLY =ӨUT*$`<@iRRr΍B+sZ_Թt4InPmv9I}8p_cbTwvta!x K"0iwm/l]R.c6AMt;KzHy3wQZ{v?JΟhL**ktt@^'9> Tl$Q,8i *IN+@TX+BxGySPĬ@;c8uWO^/:91Hܽ÷ȑOfH: r2T]4P70~"RYaQYj VeZ j6K8^<&tqQ2`,5"UK_g0l4U1n,u?2n2QzS C 1u\Y\~`93̋~MZlPk_ ;#S^yЀZfF,(/ 1M`\ףS0udBuIc_LF Qjδ&8&QJE8/,`p%}mq_C -A5a6`>,M3x7\$Bmoęހ8M4QX"n-]*CB†ibwycw{&Z5m(.)ftiJiao:Ze8ԓCcDEdii"vᅪk ʱk :"js^E' 2,[\\˅l`6wj5ڜ *;|w9#ٍߐXa+Q߾aKzw{QF`Sncyo>}[\TeIm;O=vedy@- _vÇQli} i TZWȢҲQx,(P2'KVqKv#-5 @ ?1"ϯH ΘQ̩]܄]I,%-'HkFakDw'p^(+؁73L0PXݖHj4$6tWTX}Q8˶9wt뭷ťc=7PF`SƓϟM֛ G*b11f*.<mW>@uT4!%Hs,՛MԦl` ) %|7EO т3 YS`"Ͳb%|$  ؖ)4f>J&<P! ?-Hu]7@}\|gKV]Y>d讠yD=Y 9ԶM̢y3[{c|x3LÔYR,B(HIF@LF#7 e/^~3( 1pU(@gN*-`$#{2MK)65Jh342`SƓ~mc60 _bmM5j!'rr >5d ތr x6o!?W+hqs{QЃD^ Uj,did0 [^v`!Ȓr]w9,Ch\ & A kYX}=bf h)P?iWI3` ؋P ooR<jr~:{Md$+Б`%DhZo~0z /H:(} ܥg#F@UY'$fZ;$I0PFBxܭq+a8=܁iX96h;Nق?oY.qO3@}6-nuLc$ccii.i"l^Ѓ \,ח4`{wΛo@70 1X ⮫33Nी|53rRѴߓ< #*P|N9vE1\&HjKH  )oH @^P2i6ynTom@dlFR%@XP>+KMW"KLsFmzzL~RNƵ?G̿x o\Ž7nͫq7qG;}uP&cL#0ex2r^ Ef˷~jJSߑ%,wz}{>!R3yi(7$EvQK  X i{.z40k tW ?X. '[?s߸[Q7qjԁ{#OR-a bp4]Ӻ{ٽus(6<OuΖМ-7amtèSCvZ^Hʹ>^C$IĿ,olmMᚪbh|dط0 dsɮ\f2bM)Y۷3gqWH 8jۿsvqo~`S c8lwMb[b󰼾U /"qwʻNV)c1t.6_OӇȣX؟N>W@(,XH}@z7C|?"|.рQ jpfɇwq/+B ?ƞi8.ƣ1ljYl;: u'E9bZ6605ϫXX[t.>M.fЄr(uH r$7rPC Hغ`*Т萿U3: 2vɈuEge/`g= Z~9 ]%K/V Q7/2Z?eVꏉFq$a&lũ",w^Y}P-6Ϊŝi/7yܹqx ` 6w5q|7 N_ I b˻_RªIrf3)(mۼswmwE6;%q8R F!Ot8d!{N'7myw_nq iLGs1βX3hBk_r2I k~^7՞~DeR{4Qi_Awm]GaSp߸{72Pmq B60n TɄ6D ZHܻTZ"ضmzId<>do=Ȓ=lxdǣTn_PiA|oc~ Lɝ*LۅUa5fʄ,XbL`sqKh 8LI #@.ˡG2Qeعýܾ۷pmK׾<Ra|y~53Qn-+;Yr2ʞM03Zq2s z8̳p}y&hh ljN2вRkc

    D#0\,WB@÷I:f6~d  u\ӕ5 $hMQx*ˏ0鮯$08BRvgG 3w9(32fiiiP`S Xp} bluuZg`L_}vW9Leak]L+= ̚"2d8z\+hJSRY"|O8DΟcsYD2X3]a~ YAV_NPǩĬ"ZUɔUAt1TX\a8q4\cZߎ18[3 yFS' hNiYhًfP$A3R) p`JݍM,okL[H/=-'Ev߳,dHz8_ٛdBF⭐o>5^þX&8iv'(\F{(_`Scn%0~i?6? (I$\LͨuK-FL G[fffffa&35Q1&_cl͖K:32"#ҿٷAu0U7#IW-=ihXJ[JVo8oI"'2w,'#9U*` q6 r ()4{Iz Թw-#6A]iPNFanĻCMG֜A?Wf."ECJm)*蒔O4 .Davj@c|;'>͖ }c^n!jtރ%V^*&vyr3EZ>={I99 7ab.p[>[(-ty?H,,Wac7ߚ%ZCӌ"+kL U(+j9MYS:oX' ͏Y 9pҤUNNZx}:|97 Svѕ~c"ܽ"k_x6]kEhC\E,:cXK 0uZ /#Vw&`* X 7=B=dkl׿63䀇w+ H9iTk`K4 azqw"E~Ng>Eg2"UJ6̻%0A8Tg~ |xlѶ 4D?ɫƱ[nc=m<6͔AfI$~s]P= ON",q{NRj&&'=^S5TF\!AZ;&=R/ä [IMg@Z>q`Ui~?[09uFm%1TI4n=jGRs>ߺ,c>7/&A!OXXqL N\)D+&VҤRqb + AR/|\t, G$ ڨ7ʂT?w YW#R-q!.̦lqШ8Y:nzū޼4oX566VspΊT>_9ۅG`-5Bn4# {'v"Jgpw%}0؀ !j|Kq͋MxT|@}PA~h:-K)D*6ԓ\J"+N?pa[ 6'hxwqӁZEQȳ x¼atf-!^#LA Qo9hUk]Y%a[窻^G{ys0cc 3?9y8_ kzة#aŭ ʕj"ބ@ 5IMJygWΩ1?v dɟ *L rS ~&}W*mˋ ! 3}rԠ)̲즷GCx Ë|ؤd}l0Gl:2ӧ}{ȵ̈\.%l^M'k_/ z;u[ð&D5FucLZUZ#7d`0ncGN::g/Y|N{c(KIU8žMQ6DL̔"8c ]8O_\ BSL(T6ccSGaK`sg30NCIdɖ\Aaeq=O:oI pɌTDf'Q wUq, ܵ@! 5-K_(L\ gd0.]8ϱ*ru#+NkM&RA~$ HQ&4n4Dj5|@ v/!ت`|,=JRz2,Œ?~5Ϳ YJl6P h"=>hr3VJ_&*2UIjp`IyfE1-d|OM.MR"é79yh'; 0{,>2d X"ϩ?&(LRejb02;(}JK7, Q|TeSZq _3LEi2FO0>ri((%NysG+~@ϮY+n}>v`iAC+Oŵ1R{yCN% qIXJv?œ|`0x3FqrRk-t ^w>4)M&ug}'GIE 90 to4jU, gf9_^i8q Pb7eƃ'@+fx2Iأ>֊?qqv .7ҵҵph^|<@)g@mXfw jUot5]Cx},x#}(@i$ƛcdj<)7/[o#"ZEٳE FG7.g6s?g=MFw);nɆۻ>wwίÖ,oY~=vZ>ijalTH}⅕ ZIs;>=`n}t'Nq#/JAAgЗ8BXg/HPӵAG w[kc!o {/'!7"dKKxf$-,=^5*.-/`em6i(ȑCJǔ MzaQ!+\“J^@>i%ˌMԸg$ N>p(̸x-ɟG8sXr8q#AlS\: >W7ů *:@8-ʟ) =juӵSx6"igY9cY_(3/G>|,4ռglQ ^92^uXYjwYaH/" _w'>'gȳ,W9E3zxA44-+k oιr `wNJ.<` x0Uؓp,rc\.G_й(~nvܺJ7)`Xp5}p jR8O(9-tY៙gKk Wl8w/齇 oeͯ".](GoLE\[_ ӓYyk, zlԛ5n+^ΟFۦcѱp*; ?]|e9((oJѶIds2ha[WWkj( Zӻqw_S)v>JEE a-B8|H &3{zy0u:Yŧg PRI"/5w/e`m(:r)剢k z N]#xO|}C{ItD[$UZS@PCS'-oe 8YR<У+~sa /Y$q] vo)54C1{Hm)T` Oʂ, SliS\?v )t.֧{r$h_EpߏZ8 :G# `Oe feM9k7rz7iӣSF`J9_ qI >0Jfy@+Pi mBRoba/SgBEu0YAtPv p P#2L2@8m_MFAIz ir4&5X{&SQYj=[`3j0Eqr8/2cׂ#Z4VRHk D0YGD:i%FQ0$@Wj3OC91&x{[霟3Ǧ°.~W]E6̏m eg'4B!1>{Odu$`f0!­ҕR꽂{I]"[{*L|\)plnއ_`;cʍ4 EAϱB!gTu-_$rtOb03]|濻O~ gHO#_8 xvs3H`.:% /;vɜjCø;Qk4? 4i˲l#V>]2k/~UT.D{onD=X!Cg]?;LxeuC{= D-I,o]71÷05ྛ,yb|ixB">GE*l%.;?>8.5vry{D  ȑA5 P[]Gc6E )l7LA.MRJbjNl B:"oNBF02ĮJzJ nqI{pGyTr"$e2zNaI"pj  sKK󘝽H|q>4ʀ>ݛMU(vl08kğwcb{\`sCy¾?UFzWQo Gy FBhf`02o#?h܅n%v2^7_!K?"9eMWy0j Rabkq†J|]>HCaA )4~Yʴ׷GfD'3aMpa4:wP!73' '$\O4Rr \?a\ Ka56ʮAp7ˀE$맱{Ο=y3tfsT$-=ed] Zy(u}Tku(Dо pR^ NBj8t;E 6,'tEvXb0:{Iz(Vga{Q"D q? 8/ hrYi /X.hz KAɴ fT^]x, @և PuV*&Ƈ'03 PCcb `lN/3&?^Am})x[jalac^+u&s^`cc={? pƑ-Nu=$>Ǣ#5HOۿBRImw.d/nF7ߏquظ@?Ok}G$!/} *j539+  =Z\Ŏɽ8qz|᫾~kq׍wknN܂]phd:S⺓7ko ׽y/͞w_jzhʃvjTY7XY(AgeNqRWwrE@)ڥ y߭oNdycq',H8ڹ$ksy}:ezT()@c3u("Ttg}}tBΙb r)^_L*( ̜~Rrob&#b@@u A7hXʀjA wDžX__{! S:yqȔ6k[(VW2'Evn<@84IBNrGɦady'=Sn£e9S9C溇}fZyf ,cChcH'3t2k#sBGZ&^#9TBZ <4쯙[FP5sB 'P#UЮi*D95IYi~SU4Cصk\|9 e,;4.GL1b>!zFJ)5HBm!0f9_͞[s"0$o|J` #(˞bڜx,D4e#lcyi1RL5~,4EPoAI|/)FFURcѳfp4]j.I|e?9H->l "^Chm 7|^dz|Dt[R(dn{?OGEe:;*-8x5= d)/S D'莝h  nBT "W)9tRs Ҋ@ ?ֈ:=֣eTy@=>AZEǘ;;1E6&um+s,REJYk1zNlPi tYSu$` 0tkUح>ZxwƆ#P46@*Ȳ]3ayI1_Sk^%*GrDLA ֥]qRPz\Wv_RWN$8)Cr`|7DUN,[4IBCj%'w "eZP$DWB,ǘNptA4CX+XY#i<ː<BRhN;NHZNRpfN-: zVk||'A(r/uFZdžk6t(VVn@t`{L↼~xTt )(ޚa-}-7xF__0 'd9xg9DnpSx\RC&~AgNx,I0KKr,:JrC@&?Ng@ -ś#$p-ӋyV1Y㍨y!!{3> &&1>1{w'|wqaTOMzmn7͠vT˭v =_99ʉ $B 9QGIA=QJ)NW :xWڽ͑Xd) Y6|Y^F\-nNhuk%b1sjZPҩMwpt{z|]ƠZk`ϑӜxF5l+r2uu`!P D_L}FsB&6v3Hȧ:ph<:MA+2CWM 7<"Uz:j ܘ(Jh  f KXErRw),GseK(UN `f̨ |4K7KG("xQZnAn`N I8~c/?F@(!+`XGU|r.l9Ꭿ f)d"!ASj#&B O;2`21 aq`&䧡#l5RHad||S{m;Pk4OWGs,]^hi3Yl `{9m W.9߈F/xBdϓ #`Ý&jJk %x0蕂$y#yN{:T^w94OkqyPx |k-LOr~Ď%M B8Bw:kw5xT@X~J@@G=" oy ~K]: x,Ղhjci[ſ%S8nl yx:e&1R"MSwֆZ#FKaD_FC d } kCk`r\wPLup! mI[G((A'3ʳ[X'`%?&[]Ҥmt"ӥ}R#Sg|  QCbtl>7!=00 AH脠T5h+Ӣ'2}]l)S%x:Z`@Vp'ئ ~lu=([RyP5>㷽ԑ,@JQRI-J +HtXG8F n?mE705u"[їqjl ;'{lo+PVk4b!E$3K dI{T "h^}5jc8giMЭqZk) +ʋ/#QV$S?HvG4< &0V='\^/Ϊ| 8 EZ"T:>r=/!cXKrq^@g8q ?Si321(tRJAA8ݔ5ʺ!.\ ٗAM!_J&ryNԀI$4:HI YCkQ{ t|j6EsMg6:]jGwC[_C1Kǎ@Z'gpjSFk5ѨHR#EkWT#_> ǐba:}a4rV[,Y(˧60z ֱG ZmYA6#Z?Ya"u ]#./nX!ܲLgCS7݄qNLR)U?vK7xNYP"DL&b~J:Tu0[U #:~FWp?Kk` Pm0 rగ0<6B? 1,#֨~Қ]J](H_(% xl 7~^zDc%x7iAFYa?Kx#=K/C\N$=Zn=~-' cN5¥ ftXrÝ;}·^f?z}5d(^ P}ޮ0BQ! @Ȝ#,`B;; -wF΍v:XP|l9(gB_rQUYc$V(5 u.<<i!&^]@p:>\GZP";2|:??۬(EN)ʃaI\5 !R:Ɓ[n¢PğqC0ϟ>FL:'BA-X5ˀc tTp+%=O ỳD%{dD:8x*6|]M=uwAz֋-,z2KϲQS(("RJc,%XdVFi;:+n 8Y[_8؊gL궃tQV2t=!NI ߙj_jAHwm?+۱v< d "ONQެ=H#.ۇN؂gg#O_-@Q92i8ZQ^3 .nWpH֛| sL*"zbUM"΢ .ƹty2JCeihSB!tȐ.赡LF6`C@eVP|׊)خ0?#3ÃPoij9  */~ ^)J{w{%tiQ8FqJ\e@S @CPbec ?N`#pRhi jyǴ; 4!Yn+T*C8‰#9v;5<(G=S{''Xkl=?ulSWkWEE5ui ]\KڣE_cthR3ait ~0jj4G (#4[RB'P&F oDP?!-пDu7WF<)UOfyi3N 1͔E 8x25QQ:~tq5 F&˙BnBD{a+Tx1@2L)p>9R R3D}pQg H%=y#wO {,(ܣ&>x ({6[ ? LHQrzv ) n.ƣiEE Ϟhȭ:r>È@c)GFhQ7n2C:^)HCC#,7wVRiLƥ kaqi; 9v~&ހFn[1nni%I;)n qM&G@L*v6aXg<" ~r\ Do0 {~'u?\da`S)8' v謓 T@Xޗ)$8J#vmPZMKKYCDl8Q4+/0ƽ GoB'EOIbߦ=_`c8I RW뺗S)@ Lj,tȑwSt~4M0~nw@%[UԨgEA!$_t2> @pV˴o QG@# Q[<N@ 0T6j `xb < uOC ȡJ]0ƾ.J"׳fkfg-_r Pr^&qz׬Q7Z8jґOD)R C@ő=y8ԱHʛ+t;.$+,,Cp14u^U@;r\[b}g{;$vLfAChc_DMU.Wi|eLst$Zݷ{h4/_&D܂*hlslL\F.%`X ЙaP-_@{0dv %?zV B\!c֙ߓ!^@[QX.7wl2WH)mCEAN'V!pGrk4n~aD*Q5rP꜏C4 S_*X*Me*uq`ojjL"+Z&g[+&ўyo3%]" /S׻abAL's;l R(fJf#n <܅mraΠvNOCq ojڇW 0`s~9j3ke$Si ӲRc!t,8ѨPO)=,B /p K\՗9Qh 3'A &b9lA Q|?T0r+9c浱u`hF'J{ >ކ7:ٳak:xG &M\~O"YBD7=:DU. ?w+(&=7Qua xFqF#(8=[Pw>uT3b$A0)QE"+و\OQڊ1L6J l[ OnLǘd$"yĈw:4Zl [:IMśRdG>d'&q򾏣ItN_J "d@+of&z v^a5 ;3fhΖYJ}XgL+ɸ#PQјmlhXѰQVcqD%XƝH9aQ)F 6XHSI(p 73晥mTB|^9JEzKHqM%<dv觸T DOiAb$QpDiiiaf~G]95>g]hhgFPbpƲDϙz|Ts uuJn2`Z۸=04}#&<{Jc2k^i5F %{ sL0Сr@.Dz4ɥ 

    )Kt4n\](g*Ehm019K)V|cNUp+d:0Tw,FH1`_h66TmZmx)#HN3JS|V9Oy(g}Nh ^Kc"(yƃ/s13ԒW+{¾4;š9Ƨorvx o۞  iYbƑ2jR"cc`i?2+<Hηܱg5~O嗝A}uLkK(]8o> ՊFطzyyΥ\ZCU+[Mg(?S pqh/Ѧ4n@3o>{ʈQ* |:xPvv٨=2іMqNCM'Vv}*g<'v! 1}@I؈Ѥf8fh(1bj ܘ~-+Z0pWq5F-gm\OZyPw=wQKۮظ~;f\t46}㭿xCXJo-᪗.@R'=4;^,I΂Q($tjU➦#1j>) ֙5sD8}ם1y>PܿCƈu(V fsU$ZO؏|?.V'd% SeiLҤ bVMA4(T*ŭ<~ ڝ25U!B<40jUggFRQ:]0&8`G1ƶ$ ck\=N<͓w0K{%0bzpI.U]Nwn )i HPwL1Ƒȼrx s29v?MW[R5agh eG u]z \q5xѧ'∊B3*AIhr͂Y?a4ucڎ鰷N 1YR$lU_R}(&:d) 6qJsTKqgbt4i9G6cL}O^1۲ťH,9sEz=d )d 监YN3\{`skX0V>JxD146 ۮSÁzR#@`0$#%3؃]')Hc>j mG g?leH_~B<f&A~eĭG$e?1|" />poo\Sb&e R( R3sS8tcݫW\I)jb.^OD>A*s3T4QtXTe.&&JTiVRr>Y_O+Q}jJvڭJBJHBpoQzJ8^Bu `)gY8 #,ЩWvd2g8`({ITȊaV.:Qqԝ9VwqD/G`j/`b D].b .z";U: ܤ'"cڪ+NЈbIb]z"pz;P"8㔖[G6h 4\ՉЩYlGP@es;ih A(͝-5bhqUɟӜb 5}7v@'82AǕrXث\ra'c/럇cvv6$0` {/F +h'\FJ:50Ib?":mcfҥ5l]N?._>L91X&~\@A찹]?GG pp!C&}ןg &@]MY/,HsEDfk+^ʁc2x́{w!PLbqeÈaa %w6m TS+{Fx&H&lLJD; ӆT;[pq7PpI@<` xzAՖHï^HxI)uEXGi nxE_AsMӕ,J b&Q <gǂAp 4M e*ً#n6P*-mժuH/("z[u;]yVj61*Qy}yR.cVO5G̤TWSv}v9TH{k Տ&iZoVZ-)YK099 9Yi&֫#KcIenȒSņд"3FQ9.n08 8H,ȴlǽHZQDcWoWL,x.@f.O:ʮg‡پy81SFVjgDkܴe~Po)wƐ' (^ p pI7Q7[OeY%s\Ik'-oZ*u\O%+ w\8`q0n|$\4ns@>CnhZ0=SSul ;{$3PRbdp 4I(P3I84u9K1 e x# i14x6NqaRh>Y 8^0 9֜_*>0B\FA+·L/K)KjV.~f~ '"u:Jc0p(J{=c,2(=BdxQ|ڿ %-?yk\Z3s즛~FkkZܹX_[cC8 ' _x:ECW3J)qߏ(<9NJu(/NMNNeALj.+C7;8mRVf92AL,zEhF25ES{&yθ;mkb m Q<uj ٍ:ҟ_םx=B#^ҷ'2QӔ f/`&RPLt]Ӝ7d 01}7vV(?uJ;LJBÓClNxLlp ;EL@ZJ!a.宰͠;랇/] { 9\D2F2 ({⑏~ImZ^Guz$A2Ǟ'0;YK[O<2zS%3BxeҾC!J-,-97<:67f .Ak5RAD4}h$fqch5VWC+m}왋^7~sF4 #7!=Pֿ;w˸`ըǗ}ӷ+3G;D|dH(OēhE{(:]( g\<(]2 &3g 룣8'[=Ds{f)J]oCI;6AC)Ռ kcy9vg/u;Nw@qO\4Qՙ{V:w^p9Qh.ue nKт[BoN!hW'q8lT9{, ;1h̓FE0W@vє!W>GK7݈OMZJ_S\zs]%D'TYbn >& u߽ޛ_}U.S=۔5+\c.sAuɉ 8@Ag5t H{&q8ζϟ'ȭܡd,،>]? | LOOSjP pei6 B ^C 2FӶ$H!(%TVCBBv:~{^KQZ( xJ8%I8J )|!Dy^i?ř"KB'iT,)`I69+ʺR! |rȝ·w>uq)ngjF!:.=~ͫ_zq(vʕJOt:w4=?8߹/GW+**S(!@ ,STay!]o5AD`R)K*ؐ5.O 0Xr]N\=IkΜfZd!9~B ^?$*c4ʵ \aGoЌ%ct<^FSVZI/dcۡ \:KiɭUZfnPDj:) оEN"X!~٬u86X=Q8"!aSn3(V .zgUיk&Rj"IE(eCe āgk}\+xL'Rf*̮4\wӎ j. t!ʓ em/P!<@L. e8Ff<;` p:|ϙ-@)D1ߘTYV+e:<|85LS/\sx~T)*f3=Ҕgul?|)Oicni#߉ " "ZÉ'!Zdhc[5vS7F2ϻh۬ Z*_`1cX$q lnA۩Vj9ڗMIytqӥ}` c9V1I~~ǐ}4fg173JB!m~n ik?'O{zȑ^GV#è獜[yg{'~jiCdoZnT.÷ЏE/~9Mw $םag~m˟)VIUF Z\C(nAu"h3531.7P ʋIlX D}: -'o?J+W}*XeRJ3Ү.JE(E D1ND(Ɛ#S7kl8ߓ@=fKifliXS4U^ VBQ FiD 8y/&&&(SW571( ڲΘAoDϒH + Ci6MU}T.P'# ԓǁi!Lp/wǶ[g~g6ff)_={E7iɤl~n^"g^q_=g(~!&jFf pλ]Oߡ6;W^>K7?w݋?r=q"D9sǟw<8qI $6o4u%tP ӌuW2v6b+:I_, M3\PJ733+]v[汲w]xk_Cr_: jTFQ1ZûQHmp۾Q$-~TU <|%#k@(a |;и>Jtp wdڐLMdqZS]\wUzay~fO ' +>:حn,ǼXkG#~ _N ۛ_gCQԥj3 ə 5qZ z}Rf#LIikʃM0G\p﵀e9Gϳ劶ar./PiD8ɚx ߇GOCzg 䦟0H5DdQL?q3u$|X8ܟmzo̔Bfd˨4|$$j ^: ӡ9sx<QBYU>3K{LÃKRҞ&EjZW䤓ܱDB߁ӧp[ߌ:ؔB&l >dLИ?rS𓔔4ܾ=.\}f syLwnnl <[BTEFcwqK u }&z\h99|p0'4pU9ggK8^/r.sP,5XfV/꧒r͗JaѓੲL2T./H2JٰM48p>{\2~Fl4 Vj&'/(o@;wѭWǮ涿~~ _~cJ^,c_m o=^;sg 2xS $i֨qY_GhG:' r+N 6Dj{j+JRC>I B3#TJWJ J08D?U>q YQO@Ja'%FFF{,sKTscPH| Z&[81<|G7Hxs0#KcbqqSh&hYs>~Sg}h[:Uxٗ}.kxǛ p9`<0<Ꭿ;^]?p\Yp$h}1 s^1Hj877WR!!PZŗ^w/#F7f|[Vi7q>Q_\8\@ǯҪS2F^,|gj'DRN`r$EӚT1@#P2K(p#H;+|ª=#9y 7r%3g? n<E9 2N]xT]΀ +jhna iCèyV uK'Ijёue?/fӚ$h%8ғhorTRH4_шSKkΓ@cő1R H 4B& gP-W="DRⲡCuE~o@)2'Y-F) ^*Oh y(wYUh$"JAL0@~~[.R$KK'wZSkP,eJi=KF$љ}?@ı;nCgsPA:P QvftSM?T!=i&@_ƯDV=F̥df ήoq'hTuv4nI^P0+O<1{=woa.++/y9[[]'O4b]ܻ?v|;PZ:13T?{d&i _*wSIyRvM@u2 N;Y hg{ۛ`kiNL링&j;0o"bs_M-NLBFvM|~oBwc> ˟ocFg2]<EvCk64`N+X#|2}vŒfv`"L;KZЈݾvQNڀD8wQ+3%pK?_JɔI˘`kI[Ma fd ̲ +U(Бcu&>0EYЍ%n!)xYӯ*Hb/Y 7W&AP.2~ {2㭕tMnELEh &ϖ݇6H0E5 suA7SW(y(уhFIee>Is?x7:5$ .xPILܦb M# 8]2ZrL]z 4<)l$CZgD)#@M똜pѮnrxέDQ)h7051qhEP*")ӽ3kapSFe2P㙹KXAP(I0p̏}^(^ ʅکGR@SA-˸㭯с0dR(eETcEV 203y;c^̉'p-2OzAZQY^Farb]Zja1OQg}Ku5AGح R}#81t]tڝ, -gH0Ҍyl4{DJxz\Q(qSZy0Y*011mo8QĮ+-. a8~~ s{Qgʺ^6nQ 0wbLkg8vctl:0խ]fp㺼i٘%);f'EGy_Ivm#\e':gywx͏K% }C9lƜ4l=NLVʾ*U2zʸ.hXdfYA511hGN4Wv)Az) y>m(zԅUpsTYK^!y O{#J52N\en6, P6b`? ٶTEZΖ*7D&q7nyS;UEwXzh=G?^w6SFzx/].v{rGRk-|Su)M )|{E["d83X;uOv(P&>$3u$@<)To*'j0^C `m9,dbaEa:)T/ x=c7̳~IZOczcHkO+*XTI)wwYf@-3hc:\:!;aWQ&w 5(Gі6GًS[(mi_DQ:8.MJֺ #(.QQ|$TB?l}|rv̰ǜvZTcׁ9U.IFOvfX[{12 4a=eSfjNDThkp,7' )i!IQ9_E( j݋MA7PlKe Sp9F淓дasɳwpbmNy&zMLCǟABrFFN{8+_G8uat łN=: B9'8r=)%>g~*3J{PJezK{AMZ`LW1"A`^cRvybuXW^ƞ|~Ͼ:H7uw'80Ez!6w;jQ'Z=mWCۂH'I})*"'ۿ칋l ECG02IDFo*:Jf҂l-` jR Cg\`6*ѝz!&wLfK)]M B;hCMf0sJQ$I`g%\]Bߘ!$=H CEt~7I=M{ٓ 0jRH) 犕?3;11,EAH'ILi2OM2&q.W.NhDà} "rM qͧߍ})|Ѯ{]x˻Xݤt줇7~cq?c g!!C~aX*E&F2KtjP4~f|! F6'({F\eJaq4,|1p#v:uQn}$Cĥ Gc^0bbD:jhQѨ(1HRjx\QݲF5q+!\hJzDc=Om Gt9Q"mA%FuƤMûۘ*HY+L]=9Lը[0Hv{ۅɤ5 UZҊ;hg`d7}h=zof"($O=DDZPsBȂH@- C,݋zT\H70 *O$UWizc׽=9?T(fӋI{QrixNpIz{r (6Nq&WԪ`F8ѣdLZa$^!W|s11Qk$N7g >3v[z\IF¹X rc&f0s0Nj7TX{ V, 85{n"Vl102ic Țn)JS0Xfې6ў]s8:#pC-厬`22߂:6|OҰkTxS-FuC͆bY.QƖ̘ &s O'F)Q5df\47n m^`@٧ "y#0l`iԈ2R*3hJyifc't>r~;D6}3z[ĕ~;y荩J/OӤi!y$n"7AM zNjvHbN=g&J#7m`0cfe/~Wϼrf0hݪ3//ƕ\W?T*I pTx0Bmz\$xqbnZ]jd../zH!Ƕʀ6I-#s͜i[1Yb- rd.ty7#ިSN|5M`{Z+\0!]=vBojVp1&̉r w\X{mCh?X\EQSNdө8Cptc7LSEI"CuaCi0 ._u7aMn]vY*3OD̘Hu\͆n"I,}.-$%Q5\e`&ix*Q g{kqɻfrvғۏsޓ X( OHTJ!$.Ak6I~T{^bEBo?rfR'YB01AFy+p`Wǧ= E>^ۍZb7}63R֥.TX:ppW1{mȃQ}ɳ:D=@ v>g)=_@9y.rで) `ONpf$ /\y;oW!jP >Ń,lύ;pQKCe6ز[5glx$M˸[ZVvV;|}]DŽrpZL; 9}(}h _"GFA3PeoęhH=AI%{ f v%0VyqPdR#w݊jrbw^%}yآ)(8#MJ$4"B@# 7k%=:^J tLscTBZ,@B]Pbrv{m4P)WQz?Fgg>-`L:M\C)9Ll~M3'S`sƘ4SuMo{80 hSEnn"ףcAiYۆO=Kq(+\G.)JžG {ҲrrGcm3w!VSHj&D@˻5`gnF\n~V 2aF7e8::݁`RiE/ a7JTѸ77*ɟ`GB!'R"$R79i^ioz{|^r(hW[b-IZ ~QMtct{].xzB$]T"O7z6`r 85fZ.$]iu4X]66vg.1g(Opf$aQ.Q15sd&j_w#NQrZCӂ_9UP&w?f&Z1m_Of̗tV7ΘMJ MƳPΤROrp PxSMS8u2si@ n`iD4HDjAX(G\@yʛ&ۯ[E)\?ZTZSuT[qb"6$Fh @66t i~Oq|1qo)~@uww/O _Ym>!8vwK2ϓ͘¾%жmh DDFڌXJSGss% UL>iPc^ǐ2RZH{JԪ i8׻Z2uw;1RM ϏIZA{xJN F $Hxal##XvOB֏nnCCi7{^57B~m8a3BVnu!¢ZX.{۫x%7u/]}.j5{$S HR,^I.d; qJAÃ* g|'8>$j>$~8+&ػߌ^ /(FvG7fk,w˪o)\i;Rm6.1' cuB ̥ w`)cb"QJŽ:8$@F=oz/dM͆$՝*+-MmZfP M1KWqӱF%)p= X έkմn6"mNF`b f?IVHCpZmXUu\xɕL|c8fF"U3wlzꖻ.NV꡺D#PczEIV1$dR8dȲ St088q$zf;uQNҰߟcE!^v߇BG$ч4fsJXCGhvZGH՚* a&~`wO|O}zj'}*~WYȘw7]L1[{OChzǶ"[ VacAO\3S%s.eVQ/?'}u:ZfJlM܉&j {f͗K3xJ@zh/LYa7ǿEk'i-\ʶ[OͲMe9K '}y׋'#OsHO(3:`KFG#YEP M܁Ϳ7Y9p6z"~?Fvsp.6&ٙn]06+^`bm "6:jf%*JU(!R݉7}8Z%?'NSښ\:7 ! RJp|)}ANy%i$xzH`QŅk&~8@if _𣿄CCHhȧ.fuSKfHiY' 7^d-\Yq&~AG U8$kzAL]A6hC}i>\*:5=x􁻱KQr5VX8MYg5@QFsڋHӜЌ峏?7Ob㉣Q$&:Cp ?F]c k10ZE,bp2;[fNsh3щl uFIf m{*篓ҌZ$(Bis>_l.Ϡ^U/7c?X>9FխuFQJ7߂VE՛kkG}J/ϛ ^^ZW=66x+fW{<^}Sr ($E:HX$NC&ǵV3O_`i .~:1(ERx0r ]t02{wgLpJs:ȝJJUMnA{*I 4δb{?a_>~CZ%joـO$,V~[S48$R)=n;ߛz_88*uQ! 0W__ :fPd]*Vx77VOCccNJ~~ Te|z/XP,V0LL>3 q,2|' c]nT l0c7.1椤B)\c@޹!6 hgS\Ӯ˃ {fTOL 7r#ˁk RkԚmLZCybqA\]{3|O3и59%ǻݎ>{D:9 D>}p\kbr @8'ҁ2p*ʳs:/ PBub ^\+U@\EuMkFŸoĻWܤ3DN3&KS`26e6%Ij)0U&@=$) ojm](YZϹiZJ6ݓEVAp8QT[;B(v ©s̃ڰ<`A 65aeɴO  '`<v?g&51&p)W[p5GL }![-`cde0}3ct)\_1>bzlħ-O=bsiߵ1t)WR?mpsc.PljnukYuJ˸p7JI//טu@H;Ƙ \l8ʋ(%Ɗ=qX=cz䶸LQB/h1ίZv!GǮd0g't[YNp+']2Z,KUoE͍z1J:ގniN}IZ":v9>~U/qצj tf[`Z߈9ǿk[iښc7WxfΰP7lA+ f| kd6]gFeomW0{ i0>Eo}kڝβ:f{qxȄQ4 -ǸeܺCT+/%,r1BЊJ e V:SjAI*_85 }Vڽ`4Zz0$Ih-k0ѝ`1imSv<Ν=ͭ>L=Uj+>e !:J<9%>3gX;Oթѕyz36Ƥ|$*FCkC#6 uۻ'^B]?8n}̯l<-vLm4Yk*%"(jJc7/ʲ3AXs-U=~̫xGWkuqxU븨.%ճIM|w_BX18ɛ|Kإs\.?t:v" 26TR5،ɰ0thUNW{,Oti#YX̨<^぀"N.R+ncGyVWemUUs'^4byTo­bs9u[:)EDQUm,E!bvNd c,ɳt{Jy'څ%R$@Pc+rw=hR 9CDz凯0p˔ey?h`޻ Ox=N9r:ހ~ȡ?c(Gzcu4ULY``Μ)z1#R+ͭ!~2hhxSЈM7lj(븇=ϋޚ9G-7yUC޻ N` DDTZwKby7u?"YuQEKxA2k-*a??Օl檪944W+ .ZX3st+ƦT4svڠy^1sgSX!t(5 ٵsӧ %444bzw7~ @Xz^a|Ȧ4 9,K2/bAf6έbi}0_R_xza44V'˼g^ };~:`V׀$ X z:1OS3Þ1ȶKOHy"D9J)9w )\3 @CihwۇEEˋ?×/tw=hRf[dABD .zR33jI=}~ _y\,`,S43Gz[6+@'PZ `q0:qkG1 q *㢎0ԏQVi[H{?4TCD y. zA“/ }ʩ8 o|ihExaYOe/q.; ܱq 0ZI `ḱy^%FGp[* "t*X:z9uD1` n dͺbNT#%4G2؂Q p!\,'So|qB 44iMU|"[c!;#@4650~mqpGs ؉jᇸp`[XA8e$Z>d:EAsEd} 8s?¿ƩS*E#6 8~~0p;[Ĺ/|?,r5X4bm dQCwB݅ 0Q'tnu~5y^-6 vܮc8 C*)d}kʋ3O=LZEjK#6 Hgy ֶs9Ԃ5D1渞11F0q]֝G*e-RDAUmjAݩ GzZڵ#!H,[+/o<+O"Zτ\zԕyA@y1iȏ#͍2egaoBlʃI>m18}iu泇 o;bpk3hT` Q( 2ί6F+bq]/*bwϒMmԡo֘cS> &}K6;[ nvE_u8 @0(s%9X4{ s9b:mHUx|=S!CMHc&xa &6h&(BbcYDh4Mj.:rx9"_E2sI,NֺÞCd2:qA :@[9tFHhGO%:D9r?}_<W)p  F0N(*IQ똂DW|@ $SFcC2K8o'1om"?J4x&C4,utF\49">6٠2(GQjQO99%ۦʶ0~S"xΝHb2N6&)@Th&G8آ D>=EOWj*<Up8 ADG/EQT4*`6>؝-tOdhn`l68(&p0dCt#ף{H,}2̛2N:C:Pc|'uu .-` Adh(ZL+(+G{HW((``U3#$yͿдthd -3SHgX̧&50DGBK;N8/³p'uQEE v3*ّb6SŀZ-?"'ьe<ҿK.CA媖xDt:ڸ4 b}YdCt{&d14\ KBJE(Q$3wEٰdgjkf220<)ÔA0d3;LHfzbh$! fN'ޟ~U}£7LEQ(/0D;a.~ѷ`C0u<}q-3Ԭ z 5Xowd>NCiF8Sb]<_!ߤ~nop; MQT4ں(x5Yd~PC˲d5S`f#++'k6 #"C@3^]PבԡaYT\ktk< ndmEEУӮw189B''̚H+3ehƌn}bٔdi" EHQ{ .&J ;8Aud ¡~#et%^ΟDy8A0QFQT4d\'O~#?2'eYM4Y=q́)ŢV֢zA,E4@&k"0ݓι]b V!_}#M%^=  Eҙfm 3Zm~Ƶ>(*fZR,8^B2b⿗;%myMz"E(*k:rH4Э3%Sk0b~r>1['r!fKb%RVɁ!Lj5N..Y@GZi6W:jG #Ec1xw\tjEEۘ9g$t$yCw2'MnHlA'lM0˜z20b:fȊHlT}He<5Nc ^D16eWۣz๒1'R{s$'T(F%x> /?OG^ i%S50r3Cb+V" '%YWͬ $%x4lR?ZR'g9˄3uVȬs?+hJ-4ҁ&s5BC6_n>An PHLǡv8ۓȡLo'f:vϗDH̊ 뺾4D]T8d9ܾ;ODcSo{'̯3Q~Ʉ"j#tS D0 DJHqKܵ%gAǖA-)n\eB8G~ "(9EEɲk- _9X ou~hDf$tLmBȄܾ1 V.; yYj6<XoϏ՜BphT45~? "d,V2ƒ.L[ar(Tw5mG殝Nȅ&cwI%$̃BIjqi.UhH}q_!X%6F b0}Ǵڛsul҆D/ W`ekkH.%`tٹl<WpYRJ6hŤD2WRd TceWryDSO)hhh;6"#yhvm ̤f}n&Ztpcm瞜o(fl&t¡>D(&J:fzRge,5H!nX0Kܝ u3*PfPwwRZB(Ŭ f~gyY5 ]D$=l[4 P;q̊av8#bHc")Oy5.$nFrNV=H@k5WϩU~o[X0ШqfpƘ]:yakCjWǑy"@5l\s!BX,+45JRX_#4>5p@S OLt,3QKY4\{^]qʠAG]9<"NC4S5H%Hm^úҲe ʒ]y @iWcXԅ{ȱ g6(Ň#cIwXTN3_:ON\ȰXcaF== #h[+8"Rro/z{K]KN-ҩS'#6Å X,+4DDf)"-7ksлVS04{iZLLP kjeEF3mT}p?Pn L[^n12%5U>]_.w "Pbf j@g>{ZĤyt8 Ȕq[XFp&Ř%onKHfAdٝwɖ/ݻuٻw|7rw˘1cJ&''g2LZbXc=V=8*ywyOG߉S=qQQ $4b\j1i-jmKdˆO?uK)))͛7˛o)'Onݺ^zb]-u Ie&cmst_ 5MiMJ0Sւ]tf 혦^Jp7jJǎW` \qZBi5tY:PlX,VhB1s-vIEܟE ,yPaОd/9uk&K˴cx 3btӧifլF`4F5MONR>_(_|ّ-[C=$C Ѩ]vE16}fX9L8=57*d}ߤHs/튷8}3y̰'0L5(}פ2([o?QJ L$0ϗF6k׮YfI=ظ^Ts bBS .dP B4984E~"B΋fݴ7@]ӉF-Xkm.M-`-,(&=+!QF:[>͕=(b*..ŋرcM۳M bX m͟!4AS8=::`(=~NyKZ1҂:;ǎF1^ע/&a:kJΛ:Ur_|Q6^-Ņ*2p "SlڴQfΜ)ݻw*U|E5s `E5 M  /:[FͽRSSimH7ߧ^e]fFP(rl4Z1@=r(.ўӒ2mcܮ $͙#?1 ">2Q Gmgɒ%2~xZ*/yF@X,+4ua,Fh>x \#5 `)^Ca_a ϡF +sfvf^7-?ƪ:f8ih'N;,;G:wx=پiIs9i=/m [$11q,YP`D5 McxAV_W`7H;)@)?gǰ~X >wbGop=gHd GDbܬһM[9yDyeJa~>@iz/_.BCC?g^BcX4f)& bSIl5/aJH {qeA y'0.& uCt]=͌ Sަ8P*YG;/Ob*ްa\|žjժ} ͻ0b+[h,d ^~oS8ku +\]vR40҂眣,54x:"$+2JjJfͥKj|y '3O=%h9yuxkϞ=sIrr'>g7@NY,+40n)~B !=(xX3eʛa !VxL4l!qqI+:&&99#L2i¬8J! `ZnLU)SN>Dv%@@%CpiS_|! =""¬^YM MԆN##OgeϤThy/j"j9h%K5hifsXLA_KĖ1ĺ㣢cb$ɪ[Ϥ4鑑)CIyʩȼy91;K|?h鳭[UW]QF?}դBXe bNKSיto~v刍 ȩt{&Kq: hs3tMvRMZ+@:ɒݸd6iiL8H.1C^x<#0Ş_:`fSTT^T:LJLY,+4zUAdʼ ([qѺ ϣP cN۴6;R,- M2TkՉ"$32Z2^:2o::JNVҙ#+#֬:wAe.K87n^wuy 4SonhU+Mh,:"s<Iy9e<⪀8`oXI}o=wr'\ew&&'uAnc \:"0Lf z $t''G/gx̽Ytl\ L+2#G}T8p?j^P҄b5H e6c+B3a浿Sgn& f8ȹ",f֨I ]vjn;HJӎ5^ԩ+6]0\9lѹsVڱC-c>P*SpZꫯΣVWɅ!҄b5llBY :h~ I htXn` /u&z & ,iRs źIK]q#Gɥ\νY+\Nm&{h(l"|>8:o%N\IP Ke`&=4` Kv1&ǧ4\ZwG"0a ^f{WG HIL C\ݨfqI7BҖ%~-dX2e`Z[o¹,'"mM. JʏR\f*C߂sq~jԨ#n&}v;$C=, M$up^wtzel$ LU#Yf*} >u @45k)&-V<5*&b~h_=;EXa֥[F24:itLy'䵗_'Ϻ9묳dK&M$Is=f=qr{od4&}كb D]s B5y3 ьƬM44&m6`^ A>>wVxnL^ı`:2.2*Z{!(±(L>Sҋ~imdR~rqM]'O#*/>c/åo߾ҳgOͰO>);TP*իK4id Hp{0X,BC#)4Ytz#f.ϐv(& ID3o܁7xX Ry,Ns/D/0:Y}d@{Cu@3H>>^(4o.3dʨr<`y3ٳgk2n84hJnJիI'$>z8DTؾ8 hXE4OCJb4þGhAMoTwg\SF` <6U\XRh! RaIoܑ<i 5>y=mnfX6-ZʔCK/oI9s5:6lF-7AF!#g}!)ػ7 Ty'SO^ʌh,(&!6ߵBhhuޏ 'f+E̍6c>]Oܲߏs7gp;fP2@[k.*._Zn o Kc{t"zL#חѩ2&3S5J†I.]Hgf̘1rgt~J3Kq(xst'O4,X,v`3cI 4݊n̰ $:UBBnJHH{8 HLݫ1W 0N7LVp?[AkH3B >}".!(5i"g1vaz\ˬ,޺ s;t^ )0 2 ¢i!CCV'v//,ciQ9i1G9g/mƕ!4 Mh`]D!,aO-B߶3 _0'I!r\Qz:9l: ̱ո5>o1D.wb3qD_? D&q{o/X#Thlرrǫ~kdv`rn2 Lp| kO.`X-Vh¡; iIAD">*ڎl'٠8ȉ Fġ R-Mвj5s+!(%qiV5\稳%VmhP6?zl8D$;Kٵ1#GL1{<ڮo矵.AEH4j9̗{9wiZY>X{ٲeXQٿ6mxX~͛VZ 4EbQY-8 )mśhA}mm53HZԌS 'L$ϗ<" 8|R8P71y-AeEFD~|wfz]en_ w^m&rLQQƍ9@hL4 BTe bB py-eg] 3jU kVyH>hY(hBde_6K[Ѵ_G+6M0#К^|饗$geeEKsIҕ Yg~bXY B ;igb߭?V1fљS34_h&m,ٶeK)z?h3O?U_7R.y;vվt5##MNN6}xbX Fp J`6r^ 7 M +>Y=\Z5h|) *9 lz̋Kdmv3v:uD/۵k1-ڱw(YG ͸,e=,Zt6GkuÚ#"#UEGYN9+>t"c"?/RaDlrE]D.%t[ m `ߎ+::z-?¯0 Ke`&2h^Elt^Y FZƗD3@B/i=Y[[9`v˷ϝ ڦE\v%&&JzHF;h Wj*N\r"4ҴiS?;iLf;bXY|cbHȷ ^eY,|mNBB߲@<35uŤa6np1ȯQ_;Rb SM ,_DFҁ9%\5F^iɥ5ڍ*@Xl8#6kƆ|e^ô-$4=֯`p6i,=kW{!jà*sE KH#&۷oo](\vejZNk<_xfvI]ڶmywj6X+4 e4 ge }V*4Ab_;!rLD wŲ.e/co2]e ]d.E&Yf=it(>Jc|ж| 7bBs鳔Ui?6/t[ݺuRpyޜgXԢ!&5lwƸ8;hsvs3Mwxd M#Z7n2qE.Vh* 8]v7M!ED1hS];b{Y'5P`B$tӉPn~O~;b&1 3}n2};hs6^h1[1MjTTIZjJ`Ԉ˿nJik:$2n8#0.u\mi1,%,{MUApMeb9ą&J 6ytFhLIVM,i,ߏ瞫f_[jo&NPe,Mޕ-+V )N49`W:tNJ nbYl1QBzM24uh׻gOD:~g MC+vdQ4nV"kW^H۷СCU`>֭[ :3gͳyb& Ҙyf}BH3X^0qURܫGw*EƬo"5iڐoy.7DXt46 p/foCb_TƤGl6"6%4w,,#4{ ZψI'-H*Yq39VESNC8vLj2r}/ϡMC۩B֋jكb蕌iu.dƹ)!ASeARk֤3K,a!~Ə`7䪫RQ!u;,:}ty'uvk1NfjgVH]ŲX:Z 4kX*_h;mU+NM f g.i ۶ b'cK?ыt)sT P@cV^"s(ŋg\gqHm64X +4\0Gvbu\\@h+ p064MQ Act˥r/B`ҩ7H]ݖ 62d6mF8n=M2OX̀`s$+3XT(j4I!!;\Gh} YBӥvMcʚ<:7H` iE.427|"6b`3g/,_},HsEe.ifFӈp:%%WU`+UMXkfinNH: ϔ-!aR5Ξum͚ǨQ޽z+1B 1L䫅?u#sK#+jo3zh1h#X*Dg5Kv9BC#88 \@@rZ^lڸAM3/ u 6j4~:"h7<$ĉ9C;wl(O'.Q\M6Zf7|dJ;Ǽe-×?us/@Ćh:σ@6`Ba=D,Xphǣ6"TɏE4 xf̀uvxsaY39;56|'ovWju+UL+8UwhiAoܸOݥ+~?:sœi(1:Η]Y:М)4`MSln_I{X,X b'\Ͱ&QM1b4Dh&FE=qlp4fsܨp-HYJKN,ӽOY#ZdfF!P~H8J6%&7of+zvvIo,Y\Y}b`ӯBjLJ~CY0#Z{q|lO;͞ЬOAbX h'Ӱ_EM1dDG{48`kX8mXP=4T(f%H %;+K(RG^#_ с&/+2z%ERG)۷muk57b}ײ4 ;+߈~`W骏eK?p_}{;+~9gM2e;(F,J)̽7jD>̑:taE$q;wVUV'4[[x&KK" cI~/ȈuL8oq2Q3z5RFs>MhػtGYcL4Ja s$**xc;3MnoϋFtYYYjɮ]1B,R>NJ4s`l`޺՞- ԫQB&RVO-+<׋ehlDsm|onRRYD1V>OOHp[K\dDVpDD51fCcR4KM!lu!0?{٭xu `s r _/_}םK/<+K-/Wƍ,K4IMIhAR^=>ح_† e&U<XaQ/EScguvk )<u4I'kkS9~u x= v3 uI'uɓet,nۤIaZSVjDgo D) lcsҼNmM#@8IDj$Id5Me sWM%= DG)5MG = tL$\t.>EhL "SV@llL] q4hAI EmR` #(9rL4IN=TKM4AAA":/kJӤ {~мoeLM8ށՐ %"qn9a9[ҒǷ/BxJRcbN:cްԭ#M A=i\[j&%J|l$rр~}F>t~' nt  9rDiS%Gval׽{&R^KOQ۶m#{yw' bbbu1u0Ҽysj (EkHwu Ok&MZLEٿaT^JTPUlh:R͂ߥOaX a<_.(AdlR(Hk/Mo}3qO6ѷӓwRMݶ)MMjSi iMKkH3k*m[6͚J}(&N4 䕗^[6QJf6`iRPE߉KR,[Lmi6B~`=n͕x@95dyHgX<{CQDKV_1ޥ߃(E-H%E8u9`C$;FmvB[lRaX vp,uPdDTڝڋ򱴌)R=mtg\Xp{9gG}0-do+YI6P'۳L=\&hdaGlu,O?cvAd Edgy95`^!LA-IK[?3MY!^&BQaZdRAϙ3GTs4\ԣt? 6V{]/b&j9.|;ȄYh4s+NX+% nCR~# yaZd#gϒ+/L.| .{]K>[TzhFcUlF摇~^ڶY ⠵"]ml#H#wyGn6)@S4hTE)78@Q39wVg i|.wXxW5r) ᣍeS"u_!A52y< u%afMD7Wo,vlqyGw.%Pǽ{'}9ϵ-Pv gfrtp "k;6)8:;Qp<"@Pay5-6nXGkI㴦#G#$|ViZ#m_~6ع8+eBv[~ۭD$,0.GWEIH)>=k*Qyp )P# }{"y!tRdΞTjHi͈F5 [Ǭ jRKsig&]]Hf FR/4jDuh!KN*԰.4MA鿹I&4,[CbB!PDgdddȲKeo~'勌['<~` _f]7nҸQC-K=ݷv׮m)sQw/F <65KEAdtP(CWŇ}WhƆZ>ר)CLEF3^.~Jl=^mr;wʍo͑ Mfmפ+Khh ` jnU螫X49.,'5""b=Ê~3s! 3BR o)g_K+/L+#-* i.4\?_C0i)  x51=hׯ_v:DZ]lbNE.:erSϟ?{R,O]q/GzFn]p9wF5kbCx<2b ~c[_!\e#ހ#bBS΂y*BʼdބVԾJ>uO`L eEM*.V=Bsy2vD}mr;nW2df ]fe'ؼImjZ34kNNΟ oeVl- A{\Ͻ}>BSC[W 4boИ|kثиeD;說 =AIQ.Mwb*"#HC5 {q H7=\n9gϽ֚k.fFtH3ߨPv-S]xeA6`ty~ e_htd˹u1FZD3H\ɗ H<1g$"?| 'ay3֒E9Ut:>.'TxZ$ pٳg{ L(!\(\#4&jOfe6tR^II4ebB߅|n Ag^n[X|4ёz(s̝ @Z=`5LYdoz1h +l,u)U,u/bp[fH"&n+u&Mѩ}*w"(4#Hf!ήkM_ $q2̈́p DO 4k3Ja۟'Ŧ Ku3eDzl4c2?[^ S"7zn|yGͣM{}25c?t)\)ZJX2qҏHOyd2!-;"6|UCt}9rR펔X76}; 0byѤDӦMkWD@&QIV9C3O|g99 )t*}GB(=(Z i.};lv4765BYQbD7|mҤ7gذab4( B7jZa0=7u_|!~N]'znmf%F2fv Sv}u]+˹ABJ.P9{ $7ޙDr[T7zb̙0q%Amhrl.uWh.SY+f40sڍpQ ?ONsUخ!lhi` &N8&lQ~61i8)$8.VMS7>`^P-RZ)8DG)ȷE4ǸQ+СCq:Yv<&'Bb6@ dCd##IW3>?4/IM-P[±HD|ұSnUlLSMSx=J,fEJa=hVZK>34Q$B}с )4!% x/)bj=v$?%D: QG5AES)8. 3NX}`}vku͛wұ'L}|I QDoLM Hykݺ5JDz36mڔ=j*IǦ<&jj+喛|wG^dfMӵ/W͊K5vdX͉?*2M{U 4&fn ff['IY6d˅Y·HK6;&2Rgvz͆My jm&I4Cmdp9gWpdZru@,ĦPj H{SeKsH~{XQ?I~/-^|i(m$ 5>WOv6l`Eׯj.D.}A=ID\$(y4B~ D8?爲tsyvD +ZB2eJ'.TI Ъ&)˧ƻMvm̠RÑJfB)jiNMjNMwIvm7;0yY}SRI/zi;9vM+#&ѰЧc3%]ɗcMjf{~ " `hdlg-XuT™eP7HFi-7?SWkM5Fخ6[e8:YmGIhS,x"37T ZYfJ6D ooӦiS3N6ԙ^9>L,giU7X. }^H;&MM6*McM[S@4Hi,Hu(%`10 H7f 29f;y\PFnOv,I9<ܣ&o;L47滉/| H (hJ/M4@z$["^;y|+s aZ*cr`Po]Pph}`5Qy.ٮiQr$xǨTqEX)l>i>3ދ`Jd rzN DLPd1@d&_sZt&H8 mb'"ŋ rsS' `ccȖqϻvbk};w.J yD]d2R/!ƉT5x9~Ўc>Te܃1%SFAUq~YfMB~3\DA]U6yG49S,Be5CBۗ^|hӦ&_C8M<#! "Ra:5Nh})D!QVXT~6H$G8Aҁ W W Xl s;w3zN]BndRٱ {N$~h ˚v\/$%\HArSg#މ-!.ŋT53bDӤΨG4kyU6hS /"WxiRD4kl8h EDk)zGSW=.)ho6#MgݠVaIPS9UtAmi+겟d2-ݻwsަMA< m?Z]weT0Gl8˒̨hH$JQi(+RK C8tuVa"0x%@6/(~A2)U>4HSA;DA#vV$%ECo uN4h޼=mUt.ijsPNE9/N"ݙ^F"ar 4`#G3'c6.RVc_0+>DXdl*5%((llb5iP βHkARqx-h4EjN?Aұ&|Ro 뱐lV xqfP>Q{ hoʳ>G1dQ B~iE\W]'hѸ.%d؏}&#TZQa&_N̼?2*:DĂ"ؑi:! 8ʐ2ioǤV@"D=_|1,6|H2 B4D4tĹcsNc! "1BƭeHoKzhR}Q jpMq@ڳ f٧u""A皨zW81{]X/ڍ6xnD6Xtw#EŽŘ 1N^"(CFܪSftw "!wkZ)1"HqEi#Rqi􄼙3FGښds!?H~"(RkD5~aj$]"JEnRDL],.ҕ4E6ݫCJ9MM!b8~,((wCƦ8XXBC)Hgrƀ`PqzTდ? MEJ);F Qaq@5_N_FSH5jm*ї_~ 2UT1TMeSi޸gaX\JR$H?YV/D!Hkpm;P#RVj6hӸ9T$˗/?h Y3hYF9E=,Z 5NDSx" )h+NT!UXT)qPGEQH<>5~Xi6.߳Qjk~?FYu~zwS e=1 PZj2t KjLU7M Ԝ{R PiorHAbwK228h v戤u6 y4)'=5|bԨQ)o 09llא7H&Mêxl~9ɴ0fIr~]! AX]'cFETqԈZ>Zp;@%ijdQ,:`ܻCdzٲT ^TދuAsD(LD Y+w_+,p58F auogRHa>I @3_ /pMȑ@jlX,y<'[͛!GxN:`G!Z0y2[L4 x B lg,Jtz$G}#' H }:nA$6a ~B^'E?J* 6 6O6q $kE`O>D8MM*&{R@WHϋ!i2z'Y z>u6^4r0S2䚲QX9npD㐉E (j3,n`"N|FHdDa֎|۶m(i;94@+ *R7zE5kh T2D3nM/e#<\;, }OT˙ G4v954-D* '6yR JI7p"@J RkDHpʁI{~ʍH, /N_O"iSiS5Bg6jqfH$~QHb9DqJth6 dK6 *d}@V&J4Fi:XҺ,͍ND>p(&4M )=)PJUZm>igỳd$0تzSy>4F=ez ARM锛b)*`TfD_1w~ (񉊰 }&&i EFL6bP;x}Y-դ˪V"JY,޽{zc6'3upۦG-a Ԏ~9Z;˲ h:5^!"AhR)C|JKn| 8 AW"QqCHB"M:[_'P z 􈜁ѯ"eJ",Y8̠9AD}p Qセ2Ԥ6aj};g#/H"VM<'6N\HAL 'sȀɏJCf 'NYs Q }?pRUf(6O>G2=)-n߸ypcih8u|xl&瞙8h  ba0S&L s"q8NCޟ"= Q QNؼ"!/4u adoVIQ"Ҁ8jΥH9G4a(8cXY2`E"@lq!!FJLz}i|ڵI9rfV{gK [BˆthM%DaNGٜ7(271'PGOpIF)i-sR@X@"m6\!EET +]zV4Γ/"ޓ '6= 21ՙ^#>(kt2h\Ti fB44 F; lJIPrNƜIg[P NI-㘉|GRd95/#U"DDBv*zaP3CٟRhQ?D@P;Re4rT!Ak0L]iM\Rjd HMK[xhP#sPllcݢ^IȄI} }{TsK+Z"6H w{BVa:,b5:&7ȇW0_y@I#' d!![ݺuC%nz 5J[Tѯ:/hY7o&e/Y#~spDb4 azR@a6X'mF?撤 @BVdIFY3|Vz:QL-RD͈S]a#hNZ9nk7{07M3.C1e6Vyy!n6g3)([%ຣ&x98qК.l  _wcoǰ]FPah+HUQA6ΐ+q K`|`=D-Smh|CY)]8Q[DFɉ$D3>x=ZK}Ջύ95̂=yJSuDj*)#G4Ze0<1K4YIm&?sףXI8p+L5Gf91s …=c@P8h}g f65K]Bl -9g 8AIgYEdDBz3 @ )Ee4?BRu;̜#dn%-$ ESFQy5"GE .X! Aբp IդpD]BZr8`_pQ 3$4?u4P;*n@iFm&^ٺ&G׽ƹ<aWxL%XPX&YgРA͈b Ĝ(]xѧEJ @Cch|C@¦&A)C*&̜"K@6 \Џj'ɓ'z+U]>spD<̹^<5Rf_΀3"k@4: D AeQC0͙  hM; `N3M!V-0s6"4 uhN ҫq+z G4N_=!N0"Z$ ´-[t2nEBnNz)e`3iiٲeHssG{֎R8Ms2+"f`Eb fd:#_h[9s _#>4nEEQH,X`G2Gu*"]mr=Ak%>K4kua܊.h^d R*pfr=Akp"b\}ƭl3\pԺuxMߊp#+i `jMȳҶBzGrM~ p}:֊H>ƸVcǎQ$ڊd4 ?QD⽒Vuφ$?Ě~J-998qZM5nŋLFTZ5tWv\iȥe?]~Mڌ<['OT|_6BN^h`M4#=7٭vLܦZ%Enz;va4j4k7*?E#aM, gZmBHHDSxTݸV̨<^m6IG;m#L6cfat7|}xs𜽹d/pD5J.̶X . `Dv t$ }׬VIoD]{ "t{;Wj8Y͈+L3 ߎ0럇CvRau_p/袔F_k1^{m̆#? dBڍ[ne?Syh li$HBtMFsMLѢE(8]ly"HJr|h'6c[ne?Pyl*T0v_NekL_RdOXf^t2iωmD5T6 {l.+PnF?~Bl0~,Џ=Cg·gBq9& zCzMQ.S>'фK-2i^k,RYD5fҷ5_}ԢפD.cȼy/hMl(Pp͡`7ȩk׮f׮]O/ A1]/JK*3A$ D3 [neBMf͚5޽Oz/첓"ONx$hy~͎= K-H&qI:)&H4?NH!3lޣGsP >fv3 mXG pp˭3b?SG쑌~'sgl76uR3HA曦q͛Jk!!U!FsN3XK!.3E0ј|uڙu{dS>ndDʹԆG*5Yj'8rA)E'믿n۶Ͱon  )䑞ҥ"0MHKa9lN%?/_>ON~7#C%ď# _u>09@ݩledFUzW|Wz [ۨN@o; >zfWHTߦh$)z, ?1x AtyРAމL2桪UWcxIW7jhCZ  rAKcY+F)4 1%g"N 1cƌ1n)rEýul3EҌ nf܋l&w}A( SBF̿3|C ױBjvS$lY?"uХK[ s|^C Bul^V^ǩ@4l =O܆8զuL]ɨNAM2\dɒR 6^#~|+k[qH"?Rݻ&=IN(g7/f&whzVYb)! Fd:#~q%Fs#:eWJ2f&M&mQF4T"˓jmذ,z{TM ヿti'E. q 'eWȑ#! 1ԪUOL9q'ଖBUi6D32ZT> 3;wq^[W\{V%A tyNdJfJ/N)SfC; =n3>YfޒZxqO|E?M At'vyҒ~$2R O6Z@}RnP9 ΪUĄtP0RIpva'=azJ[o(pp1C̼[c?; " =O3ouDXK¾J*%Uw4 }\ 3DcڵlnަxnyXOrI`.M ɞoN6YKRfLZIs[#t[l36H0OˆRa ]Ēp"ls<F_?1WQa\\Rz\T}sa##Bo E2y#U? D;Oi@u)Wѱ:lk3JRh.~ tӌI&$° WH.u:t# 2v\f#2lx /X+9:Okm:gRBեbQt$eh>f毈1lڴ 7cKvx |FnK6ҙ'|$^1 GlgP!w+W|BYq7.9ڍ1xwX[-Əj߿̇/85р:k$mRb~ @m$mޙ~IQ:o;ˏ VĶ1"H2^AY4SLPZ>-[b@>D,8izH]Bj!DMv4cV b١\^dWTC/MxQ0sƥ6m5N1Oy~ (!(tO @}@Csm&HJ;H s"e*VL."לD3YXg&NX&WzȗˈPƫb;>uRi*%ƦG.7jL:^Px$)et 5G.MiJYh_!~cǎ[śoG6ЂnPT@{I3R $q!p0W[4+ڜLzDB0?#2Ot]4"K}ƊZ}#h ESM^j))hf\3x_'O.~?rft?!VQ;?TffO`E-[D4 <$qf~[PgR£"} "K49!Bg5I@P; (' SWJU(-uZJԩS J;n=]߾޻&K%fw.|3B)4T P(m?*Ԕg䭨i8gPoܔJ37Cfw̛mE:+mo7KE Hfx!V(0ȕ 0Q԰0f =Q) etɦ!"jk" amX؞tA,6 ;?D6րBcV -i/ް=LVk\0eVؽtP;,f&X8 ף4G"LaΖ/Hl S:nsiExk} <_ UJjCI8c}d^d~H&{ m Ё0%%Z"ß]qsIC= A@F$y & l|4؇(<6>){p#0Sj5? }\?#Γz USSMf\W%bL'wĞ{UL &-@~f /eyu&N%?q'k4?Bj[q.̯Z̛?'䣰VR(ѽ ]< A #€b| g]$4{!NE'>][CTɤ|U}Vȑõ7! k $cwԔ4K_!|vFY$Xr ^Klj3>ڍBͶPӑ:34B^a'XAhH'Dm2zՑ;SB@֛󫨤Y.$ aeȓ vzorp฼ΣH!7f{-Rq~BHxAސTsח$3ѐPl]Hy6 vwv% G4P F | ]`cp۰w~%&"0c%Ehdrt&SNs#p<^dKk'O~EB.&>xx,$>y  dq )1 * |{^n o@^$J.Bi {]`f*"onc[E+ݥ\vetFP!( s|>[^|} ŗyChl1a04ǽ\B$A&Sxz;{iB6C,9W#Ot<RHxDU:^B&`& ;zd qZ\rJc#Ї #!az ,u@JKGfȆ w!$ @@lxX`O!Ω!: 87jWdB9MDu ;bDS,&$˯Ja ?Y%o'Ɵp B$.3‹R/0 "So0 /kظN^%I  b,ß-+ּ>8+ҜCMDCyY3dקOSO1xӫ~e0"o<PBT?b1B 8űGF951xF @._yp.<0 yE\SϼP$Px:}f|3q,'/o;ij7hg?Cvm7>tlI)_O}JHHW1$ r71u1(gGwD?$vh(d ၸ5a9Q#F')6 _fC5Zaf LkL$(Mf`bH =ds5PTj &7y[㗭]m6lK$@"ϐW.c>o0HSKx1vTeKҜx%[^. 0F9x؇mqחO2:u#^ %긻k=09dC!8⌑-לS5}!oM5ۆ5O>qSNyԨQ/[@0z+B!sj2SWCcs7>%0`<" 1I?Cx+a+7~C%'p>='<|< ^5]ېUoxm5Z4MFDcI?87ֈ ̼GeaaS1fm9gOF1fo{udM%?zMv,ǘ\w#+R]|2 )cǨch*0Αf &1U$PYJ'Cn%B2Ț!ӯyIhyYqEBԅ@j周;"_MC&͠5*_,,&K| 0#ly?ؼSL!aPt \7 ~啀\j+23MYk-AJ4kYHc Lzs'Z#(4zT $"cH b!Dh А blP%p uȄ9UA]hg _6Q $Sa%0F okWnV/+f cete@6L@6=HK ȳ@ x.dOY8(Gh<ŷw2uFhL!4yN&E^ ɞ A|G jB/)3"{D, p gDso8fVH. 1TYi_ @4; /ı k|-]7U pz R \ . DLjf2Z lH 2r!GF?1ʻŀ4&0c L8x$r8 r|I=T?D;\T^6'`Bi/cǫ!_ӿw\n0)s&4>Q܉mر&"˯?oD 1^ whkߺY1!*ǠpVl6F Z AǁiY^LH7RX˫I9iUkB`\f'>.xK+!iA㵆/zos#ZȗTzBj9f=[f͔eYG}p̙2C96ȥ)L>=h_ӭ[7G枌Af*%` ?{~WO:~fU#L!1& 8c cdO Y `d1ʉhDa;]z My`dTR y9? )y5Sa@! f{s$<d2xx[x{CC?рT||xs|?ADa!/@ھ}w85eQ9f̘`T&r-+쌴-rad1`{db6sCXgu/2/pg>x&FcȠ'k93 D#5:$5/s_WD^HEMȌkH9&䈧t蘀WpxvC\F] x^'r4vj{?1r?r {DlC66k?C$, ;!Gy$ݱcGpa7g$KX1AP7#[h5:sok~nAO^BL;@NRj%j#=x0U#UDvc*@ $Pn됏;QU >xo/C'jyy?xZ`N LzU[Or7Cs{> ẄpB:gdRf :kI!űEB![|<y\1vts~kB*N]dYrǫ 6>Z<,HĶh<:By({3T\YjD7!%*OBŏsnx|y&NsA%s~՜_TNc$;0d,cDb(W o"Y`UH >I&R$ISzWxmaY.Tۢ< $(7WTָYsJ f!"$z A668k5 DA/U}s0<6(Br)a{H. Ua 1Tk뭷^[lH=X׷o_/I+$r3$ zI!=Ιh 4!93^RfQN͒:c%vH Pwp +|WDykj[BhV} |x2d6t\u~ƣch%ë<ݚU믿zb?8 AA_~Z~+wOx!6,-@`R'i pD5 AZP~ $lڿk\eU dCA3ntMeՒ!X7 Db :=h:0PmA)j!uٍ9 J 1" |#ȝ7!1ܭKo4> RM׊V~DդSOD,!1ѐ؜2#Oukl@<M^o=2P=3+䛗o B+?غM~ "/ ԟKիrpNH(DFcAdj%Gb3SQgg* q'Y"Du@6E ">@q 2D>Z ·DD LuQʋd=bM ^DC|8BgV?F osMkjfNiEF8U묳;P:TjRĕ0k?3yx4$@Rv{&dk!QhGgN"H04- Rp" d~KV8ߎv"֗WݽnO4,zDS-NY#M7u=&!Lx't00<'oq<'Wx,Ӏ\WaA\ ^ Xg~O .v mP /OMdfNuy퉦5ꉍZˡ -qw_pZZxT!'*5;C!%3 Y T>E~ sB׍W(Kl@\ؓ'V ;0 w30@ꫯ^ꩧ2=6zh `tE&Pn}lŜ R^@!8=|_M !^#'4ƵԶ2cOb<<Ѵ6yadN1©Zmfuҥ i~}0r"LX{ܩR!ۀWZ"ϛ(f45L%\ mHr4=TMlzXc M 湬7h<ڛ ?9ܯlk;H$U1!M3!Zh>7q>Bz.l$$fg|_(CM!~( dDpNe &~YE%ԑ@Ѩ>nR Ҕ, t j+!&M&Ug&H\n0eʔ`Ȳ.ۘl&dD 4g[ﴊPy!0E JTa3쟪= DJuW/ 90"<,H*=)D>f^KOw衇@TfD~rl&n&Y͈ .`?oZt(z%#qPH )y_| !Td'|GS"Kug#T퟈dSn^AiPvi'h"2`ohy)#ʣQGx0HG=ֲÚj1EGu6a o GY`XÙiȏq* 7_Յ|6'u3H1촡QPM<'a ,H6 %3Hf_TJKK]׮] ;WSoeCӲh<ټhU^H&)* g}v٤I_b1 &N z< o&7,&UJܜag| Anx8|BYᑴ>+&6minf{gQ!ٔN+` fr- ]g]O{PHP PgCNx_ۡsy5 s9&!x·0 V5J(/OpТp`sPl5h<1mfHC5Y^x9z  [@0 d`ӕMƒAk0?=A5A:rP-h|M6OKUOѬ|׮;]߾}_wW1_&'"wz3ud&Ǖ /AH b#=x`>KiH D0*tSHO~⚻NO_^D.傭D6ÐMIM v%%%ί:2/@PV^o.wv;7r \B2ғG%oL!<ΛMhU\\:,+'ZQ(K6 fWM0iy뺃>8h8k֬0ӯ}]y B?֧M= X0@:z>`]OKy]t෵+3Dc¨c9coQ$LzcZs[T?A-Y?e] 6u*hӂe 4PKt{dhV7kNz՘ǏwGunt]7h3nqprK9?ik6pUŚtT)@7 ~,o;_ʚrkǿӷ Q˔du#<%#$ -{]>[@6ܰ!᝶hs<sT3P'<rzA>f'$u/RfTh@ qWQ !>ܶQ|QCNsfBk-p*Ģyzk&js@NPGСC׻wogc B_ ʄ+u3v"  lh*FBH @.fC2d}OD熆h[dh92z2R< Ȇh@EF9n'GĈPDPj Kѣ""hM6̬qĉpmiCH q6"aA ͣՐ "^qf OSHD4 mt/ Z&L+aZk9No1xuaDNc!O] w*LTPb# *˺6ϡqdB"ƕv |u CPzXm٤E &嶚J4 ouY݂+n+ȩdaMf9 3QxH1zbTH"LL VUU**k9C)rV 9̨+tZA*g&@W (xIƐVG[xs=7ehWy4-%7mKjx]4L6,/xבIτ;d޽ :Irdy#|a0y >6\faq1i0)L\wunM7mgoSO4t^2mz:OSTk Bɝ QC"4< 53j-1_c Dv >MfJ GDԲ1}iMCAȆpގ4h۸+--u9g < xF<2DG~GEHP'kg;$C2Su3 fцS%^k̅MV[ O܈y~y0eq!Ijj43v:<9-0mD4<'2v;jmH86zA0Ը(t'"QB>NqAŒ|IrCf } '! b"F3,Cݼ* E)4mCˣGVAJg_<jRգ@xc@BE}:nU[}q:dq Ȯ)2")zAx*8H*&2%_#^QQ!]'`0`{pcEQ7mH3xI;Ѥ߿NϬO֏qS0u9wiJ~DWe_lj "Ԇ0d6pH$A _L-xs1̀sNI'O4Sd@ !Zoꪮu]|3JF69 IKMHMBFzi $kZ0цy@4a3<3238v4x4.?ۆ4*q[S{챇;cvmoF$DCӍ7ODdVdy_gQel?!)7kκewMwz=;˳;= Laf0۫ݼTOwٹsgW|Be CX U(B椓NrZm@zɘh0Zf뭷v?@BFHGYZZMAbDlGUCBp~j,k-Eh59 &~632̔W }h6DIv$!70kuEEEN ӿ۝x@㇏Wueoyˋa[R]ƙǖTEeP Nsƾ(8v* pPW`@y] pSD(r1lCo'C4t h ਣr~ӲlQ9)Q$Ϛ̴Eٷ~K7ofc^5&FP2<μ~䀀Q>X些`Bl~[xӎ} kȧMxM۬WhNh<<36(xUW]ɽ;)f fvr9]Ou@,4;a2o9ǔ9x-Hi(xaD67\+lvr uWІĵ[`a:z$-] uq٠A\g) h9xxyǀ(LM5_{5hȈφr}W>+iȽV< ZEDx/Z(#҃T fZ8n ! Mn3.q1i.&=gO4hB4 QSMTf7!{ l^z@"sUK^yHyT1> pL7Q2b2Q{#puBk-F $ D >PrZ\s[c5\^kOh9xx'{8gn&!Kl>3v&[Nd뾛b߆OALhJLWC:HILvQ۝nSªT`bdXDJD#ow.S.tNC~ji@ sӖ-[~/0|ШVS >nŤrrӧG5;ct`nϘFhs=f$-bf͚W$)|RxZmFSX蹪 jU ?M*P52!Ev)Vh}XlW?F2ϯ+Byyy&VJ=N?k2u6߱JF#ILD>h@o{4P5'F̯hb P&Gh|ۭoz/hfgsb4`<8xk0l _3aȨo0,n> c#xd4B@k4xc)2)I~z|&:5*`!(LxՎ!$^9Ak6t'4?ޯG%~Fyg ` E/芕;f Dh2@kM;w aAlBҞy܂mFS惓\{ DzӦ;:7QX_pDhv3gLF#lE's=e&I~yng& ƀ;dGh{9oO&j,O'W

    YՌ|!aj*_ &Dh(wq?6/W@rFFFLVP#\56-Gh A'|{wIqdz f4Ustכy$4ͯ M f9 8L#Iqgm5\}y籂1#tkuOli`J53Npں#$ꫯxAgϞmb!xwԃv8c2! `pRo}}$~wh8N:+;lb E^׎LF#TМѠˢ$ yf3cMJR04'tO& ͦ5a„}h0YaQ;1Pw4x.̼H~!*/|O{ ;?Src@F#3TE?4u3ЀnvQ+b?# Fۭ7q$Իwo3^:^dhbT,µfV};6Hy4w}#&f"LF#L&W࢟֒1L&sN1ԋf 0?gLF#4 xwIbȹ\Y_p!34#:0>gMF#4%`-O#mI]Igwu*xKSRR#l|T \ 'gLF#4l>裏%W^}JvyGB0pqK%h[Aj8k2~h~_sJ%Kt{_"a>ZxqxmAnT5L8MF#u3h7.b駟xxIr ~FC%Ở={ ZVmU`p9k2ge g6?j$[ۄh87鍊nGkj01P* cl>}$o"~8h6 ɖlTm&JE2g6o5АTۋhv=s8 Z)lҜ?o'f|AΘF[@ܱc9UIRfTVVhg d511'` iu׌K[wlë猌N?,`!4M`)ϘF aE{IrԩO6?ƎB9ޢ8#gd4B@=A*u?P*'x444Ir+$G&l>}YD{g|䂕?o2|?q'wom$wI^ /{7Mn:s 'kz01@5g6h㲫?HĪp^}ш'\6 Ȋ"PzK39%^c$׺* 5R3rgY 7~ΚFhv[g<󍤡2lf3g%7ow>5`GLF# Ӝټ>W=0Q@W7본I2!`  lРAHɻ" l۶{g.pdaFh)(j&ܜcM6)9Cffh`П5bNnqxd4B@VPg4tP]wŇn&C<3wujSN5!֭g6qڤ)ڊ¨ Oo֐F|̘[WkhX@2gV-\дQa3\Ys& vmFW1<%`ܹ$rrr=Q0V+>Cنz "1f2hY AC.4i\I{6sgpc2 _|1Mᥗ^2vZ#)nB_ng?.12LF#4J+nF>Tɓ'ʹam3PB@?V^팛0  fZ4[Br`p3FloLԞy 7Δ)a> Lbަ'l}YjQ,PlpZ26Ac2dS1Є<7v0I/k!}lEbn3X(਺:t7=!42͈%\Ÿ1:$ΙFhSӼ6a"~׮]7S)nō 0_=_$+?<ʻ`L% >fͭW^樣lFh TٌƉų7&U OJoC1]v5{6^.5e$hՊY֗?vmdj$=c2! ǍlݺHդetOpb[f[n| dMNZ:aGub➹TqL o͔=V(77{k`^5zh*|oFِ!CXܚ;yOs@a"Zk4:_ 26l22{l#ut Jyqoԩ/hykA0zEd4B@6x2KLnmyc#g~87t͘1cxmEH[4F;o<:^[Q %lt0&//͛Ǎ-M%XEd4B@AhDv}WTTf/_7&sh># l3zL"2!>F÷c9t֍j:flgaC%3y6 XpjsF|X XFE~QLAAy2sLƵ恷xDf4N>(`Ζ(Ig}J` –Lȼ zt37sN)mٲvq[wMh1/͸k7|RY[w}p(mS 4G|C]xBAx2p}h#ha\*B#Df `^>=bn-y5Ny¬!}ryꥳPnFc/@9gjeee)o=eÆ f桇;f9pM aj@Εl 㞟ŝ'cǎerHUa2;vIr~+ߑhf=("v!Qa{h!oB|eD7f>С_k!ῳ篏ٍڇF+!r<<6(i>}Qp4a~fΜ!z 7 _CFt 7pJ@4oX(fx3:[an4m7›5=/Rq>)-Vșehtn~0OLkLNNoNAv aj:[M<4|XDTP 9K-XZSȕmm޼};P+^0BW_}}hnb<ܹ3smh1"K.7}[o5ӦMk%'AY+*c؈#|9l7W_}U-UzkX tɯFkΆQ/(}Ed4B@)`75'… }^x1gr7.jb*tR _İ7Æ c&nܶuhZ$ꪫ8%ݻL2CVڊPJA؇9X x c'|b;M9V'wg-f40/-0Tl0&bx7p&{`OC2&ŸKA[w\ȗ_~-. 35|h 2M1p*!Cbjk{ڵ 䧜r Cj(ev!wy][lZ%Pg5 EC3&:FD}}Snj gV5.?عC%--Bx8VA>U\J1gٌE$:_DF#4@П茍0%7d)4r> a} V@æS;w8f5xZ;;%:_DF#*@T! u Ev`0VӐ{ 7sQiܺT[ 8߆]"u7d4Bi4yAeLmiҽw,Rpr8ZPIb;l t{:_DF#BMs8dnQL4a>]BRpGкtFu M\`is ! T_HMr M m3:_2!`c4x@A!?s9ˬ$Xl0MYw(qcV3& ؋r߰Fӯ_?:Iw}7cw< !KK`־Q\h悠kfmx}9F p?aBM6ǯ+>K3uƈFho!r &a )~?qtɮS~Ԫ3Fd4B=4bҴʜqѠ%Q99.ߑ|+A+\.]4DF#d4k4x!]( [拰jW䙳8M6r5}sd-1|'X]+~%˝12Y y=örz[xΘ41WƯDϦO.gfހFgLF#=3 dzf⸑DԚ5kXskKs gl7&%m܇FhFMܹsg00k殻i>s6:5m`3&Z Bn\ a 47xf+XQYЦg(pf/'5yhFhѰix$222B-λᆱϮf1?Zk-ud4B θ,55$А4~͇b#q )sWް5D2\ڼ#2!Tk8eŊ&х) i8U1۝)l Mخ3  ١j V\%ø\NFжċ3r}&B{UnU ULF#d4K@Ԁ|!4 2 ys׮]&ߛ.]0 iO#2E  ]&|_|EO?>~[HO7~Ibq*HX f4 d&-Enqr42M6dPII Å-;uG g=裬6kɐ:P %-^+֭ٸqc$*D̒%KLSxEs4 =hPBpQ׮]i42eʕ\o jIS3l06䂾wd4B6A=yȸj*6l&Tnͷ(Zm۶-ʚ|I[! `"IΘFh ^/VFg",;cQ"}MFlnjCiI2FF#4 j)o(o&IX{Eo*EuǦU#a4`j1 ѰOcF[aÆdI0#Gpu8*?Y4{l,J#h4 N3&JទO<}4ɸa2--Vxj:wl[sH{L 2!)6Ai3C%ݻwd\UL/8.nx>,1Rh&>H;<ӣGnL|6m9SlVc|ӯLte?^>h4 H3&hң>tM\b$Syy9Km7[߭ޛ8RSl4M3&ve$UX%CȈk=Xk3Ӄ"N&Un46hqJYamPlĭo;5Q}vf2v@N`0#0ƀo7kIPh{qqX!>}pV=6!W*hg!`c`  l  @!RA%ZP *A%䂙h2FCy'$ݑo5[:'`_ 'O6rH 489Go փm`cA[| X* P S fL l{嗓u<>pc𬫞1I 0OGevfz!`H]RJύu#ih "D9 ˆˌT,<0)ЖMos"3JMM5_|1+͚h 5E/,vw[g?`>;A1̣4P .n>4y`:`. F'EFs9|ظk&e?ϒoۭz$3pB"^x}8Q< _Y`A3ݼ @+(]ƀMi?dY! BN orWwԉP<@SN!ݡ K@.8J,RAГsVE`.xK"hVO8p sV)ZYwYi o{HsL l u ^nT_2- l` XD3+ a*3n޼$h> X k26ǐZP*@ ;8r R^d0 CF7%wٲeH&33_Y^31܈q_PXXh.蛌,{vQ AH9`lhL2B\s5p{994-ǔ)S5Aæoowa~GڴiEhDnZ Bcdv# e_%)..6M_| ChO?45kp[+ԀJPay ]aJkϿϦS ~|7t0CF}pa⑓`ٮ];wwyO>|W&^xfr '#"`= LD02{`q=g~y*B>}:14]7uf- N J3L*mNp΄8묳(|:tr4>,kÎڋƚ]xfY\V@XJR%=f ,eG*'jǎ3ӟMx>c̙3ֳgOOW6v:UR"*4<зC 2 /_HA"cBcZiii\MnF?PK5%dM8"h1 L;>!8 4nZ$^x!ߒ]3sgteܸqq5GKXd5QxJ= 0|τ#<y[ *ci|f}-gX_n5|HVS`;onæGxHX&nb[ݛ,~;O4)n[n~M)ly*W7: 09qg4B@>~#U0(RFӼ89SOmv.b4 .NkO[fgcIrRZpƵl>qk4B@_#P I̿DP]vq왺KZk2gJ9hwqh_03I9qiJ. a:LT;n̖dЖe[D+Ν]i=gLZ}8aV& x^X>B{ahBg{]Bt a:L'I/O%MhƎ9s8+ۥER\Ao 3fk2#8qx xDjܫg_1EPP D`,Y_"Dl';ι[j9ȓs{ƫik׮ g}v57/Kqvy1P0EA>܈<A,Bm&aOzD\ĻZw~&zwԩSm.L.'BP=ӕb g[K9ݸ 9sL!MYbƌ4s21ri9]w]ݰ|A׹9KfZCq+a,#8 0M 3xrPA7JX"l„ Nii)KX f5K{ĉÇ;A;C{Thzha#,J1n,jBCVh^`F5ٳgmټ[c0|KwhB*Z Z C<P`!, ʴ4fbDk7}؄8δyjpsA=l&x 6r(P2U(v/WLh$l/QlhF jVZh.ӟ3o<A\&`b?Ɓ~r+aw k`GLG&@!M9` M ~bh]tn5MMM2DUUo|#Vp'gun>jA^h7A I^>x n=SC%)KLhR/2FϾ/jEoyZ! mqpKvЄZĦBaAQlJx~ >4X 1Nev=#!g-z'c֭Nz?KMȅr14`B/G1`GdB)4tC)l5>L&0wdxf8t6IĚ7|>9<@{^~6CT8eee?]~ 4BVRa2<!= _5p' L 3۝`&xC9˄& k`BFN_ל5k8y*/^XZ<5CGgAJwAo3aYїNz85!8,qs׏5:A Mu7x-ǝ^z)ߺrJOq:Gn+81 C5xlRtFԻ,ݞ\gw/M_qNQQo٩+=K_?s)dtC( k o@[QJ̄&)W`ZhW|Nj*XdBcPZrSh\'9FêEvɶ89&MӇ [3pç5(C5ڄƈ 1=6j1q$&9wA<Lk,: F {ukNy%4[ ` \L3Y61ƅxV>h+>Y.6W]uUͮ]TpG?Ag E`4AP 9-0(&4Ff CBYU\\pJKK~;"$)2 } PՌUh$^^̶ntðO 5 4l(Z_ņ:rׯOiٰan f@uHu̘1Ca+l#,4^(7N JrE y#72 p@ .3ur-#c鴷'+2=$]Nԍ9' ilޮn_ oA8}PU YH/Tl`zn J f:-UXXP:$0)(Cӟ~4 R_~R}>flvPVUt\ %pG'Mhؐ4[WJԒR;4eڴi#]Zjw06_S5*+eƓ޽+֭[dy%VShawM U9q0M 0W^yШWZѣj[NSI_W܃7B#25Uw&@fheճƹ3KSfU,h("= u>g͚5NCCC&ԥO̜ॗ^ꔔ8>R ?OjA(KEaj9 q8U"An'nNF=@ȴG57CHhiuX9 ᕜ]`hN9;Pqu:ܺL>&3Ph@c1`vVu?9b"u1 b2,Gdw' 7ePV*AN;Y.М{̙3?WLn#;1= 4A{#D蒔/\Pb'3,fܔB#_~ٗzȽ.4FTys^h x6CK4{-EV1,9T.#3,mNTh7>L]~nBc,(ZUPد~0eX7vפ|^&vof=gj/dVnj u1 bB[7񶷽Mmd*.k?qmd%9S<7 窫Rsf M#t-(li3Bc$X >6? 'ٳc]P& ޡ̠RWJ2~ąa!;Sh'Gn4CP>*4Ac-{p1u oZ6:Ln uаZF>L 1/:ѤF`JU91B5CQ7yckUD5kiY'!Wƺ+]zW"c a f9->uey2$=6Idl3~-PhT?nc;N aZW_ ^`€1)+Ua(4ivm1KFLhb`1AOCP0x~U s^t(e[TUU9_sݑ9ɕ b%DrF`i,;LhgmU1 hSj/Ǵt_9x Y[x:Iv()3&_*C&4 ot1 a(vB^~hć3^~ݫ({c=&M".4&Y4بULh{w1VASl ~vhm)STiooYܡ ,KF. T&M46x` ,]}ת-&C럹),خ 6z^z饱06k1 "9 V;00dykx׵}؅F~2)rԦQb&4}PKḡ0p֪9pQo޼y"EB#kJ5X`f˄*`21 %я:?OU?APcR 3+&2Гp˔e%K2#ICn MAϙ a;7Ny$^U GhTBu]3{YFq_qG$StWWUW]h:cǎQ3ΐ8b%4nF۷oIcBcbtJ=ÙҬn zZ>1nۍ9|7tt'?seI`_Re ?ߙ9sfFZWX/gp8z aˡ>b3]wi& 1-./D)a#(n=J]{48GN"II|U'߫3P-4559~^E$cLh (C0iΝ;"}4N@G)!݄G&MFiDMwqm4 DJ.2 { T^YYC)@⸑>x.ḡ0Rg#}N{nDC7 D_ }t&O,|L6/4b_2xe-8Wv߄ưM5 0R0#Y3g#E\Y#GDF+? ]pFb׮]:I$%4L>Lz6 w;lM3fBc<ެ7cTUlrD+W' ҍ<`T[xGɪF4\$T&2 0id*~aLdg>6=Ϛ aۡ3vftSl|o&4Y KB:`" ƚn?Ǫ$|S9 C bX xK9:PfhjL<%Ar2I{mQLn4|xLh X Н䡧3v2au%Gͪ577; ݠJ:}aḡ06LaB*&sB#adP5\fDsfBc$xʡBMt2+1F u4L֖BcP^Jh԰AQ^]`>\3 WalZhp2q 13% 555g?ل4Hʾτưn-bìU(mChv?A;j& fC9o]&.c^/>:D 1 W[[[rgƌrצ$ O7]6'77n7́ѯC1-bDH*Rbԍ[BO>QX_p #ke-go?ĭ[ ʁsΑDdsل0iP"{t베%#˗/w~I$- Jj@0r{?\z.,;ϙ a"CJ3FxK_׿6ȑk !"1Gaa<_&4ᙻiϬRU/ ?V5n%&2CИe&4A< j#S (b 2)1yϝvid4òxLh [vȭ:gĉZB6~NUGP"z!@Z}7:_ I>lд,~Lh lN-r"|Ϟ}YH "-5!:|?K_:3 YE6Y|6'6z[Y m>QԛZ׭[0T7ht;▤\IIϗdHdz ˄0)PT c~% ɴ\"N=XD"~~GN8!mh5T !`31 btlHY_7oF2Zw#ל̥^*qpƒ_'I& #k^qJe`^ deBcBha#;e9ҹCO?tԩSu[:O*Ms"EiisE<7TUU9Q\\ߓ&tL'31 b °U5kpl0Z{Y444Duc:@?w:::Xr}OBf6x3/ f@9 7]v }AiQ hhn1r"EQQj;:g"xΚ5K3nQlK`Z?c&4A,4:a?mP [`(a@+ `]6;!&`_>lB?OΑ#GOGitrg߄ư-[Nzxf30) X ;F{ADLӀfmAZL.}{#ήZƚ5kO;$lˑ ^mP1i&roKX>W`4 jk<>wH)2n5 2[8KSE2wSf&4쀣Л~OͤbXs`_o* u "wdƍ}k}eSN: ~0p6CP"cBc|-gнtCZ~X?ؓA>Y 1欳RX#3;ԌiMD@9ɳfBc#*,MPGwzAtA'Fifhz8Pa=E0 ^'aR}k_1[BW^8`>gϞ aDǢ`-yf ^)i 0Fx=LS/>x&4a@In3 >X S} 0`?o/P ؠaA<:V^A3T@S}41 #K "fo'XA (5f9a{`?@FhVh( j`7l^`r}em?ptIENDB`PrismLauncher-11.0.3/launcher/resources/backgrounds/rory-flat.png0000644000175100017510000012405415224505336024533 0ustar runnerrunnerPNG  IHDR%ŗIDATx]{d(c۶mY5m۶m۶m۶myȱY+ ^fO?ц>/M$I~2!1 0Qވ $I?iB!Kؠ"V/ !=!5R b $|k$IxU1g/3Q_$Ir`5B978>HI$zk[(2"ÏH$D?܁rAgP_yI$CRp1(m8y {QI$ x38((س%si4I%ҿU4Cg @E ԕF ԃ)QPM0Sʇ9I$ؘotqΒ)<%+;tr G$Ir!SS^ UcAtB$IpHBb!UV-"g?""9R!&IH\[}е'̘HFX no"+Zc nc*.IdHa0GMU(11 tC?Y@,}x$I/0)p(=*2s7i Ftx= ^BeI$dH|)^BBU39x nzT͗1Oq p/ j$I0&!(}й?fIb΁*ydH5ݍ.ռs/&o%$5+aBFPdL 1zy`CTxgJ [ om3IC;qp1 1c۰ CPiy=jN}*c'K*K͚T-E`Df $I.5QW /#WUܬC^\(J.[$IN?A0q6(;zӹYUk:?dɜV9oz[^OGDw4Iv@)V:^C9RJS2W*XaEszuZ=^f!&IsռO q6(gѼZbĝj[M>-rso@hINdŁϠQ*܏IeL~ jP<~6rk$٧GC(sTzd^5UZӹJMJo^rɀ'ӵ੤{MŲ=7!_Y霎bk$: %11ʎmސVOllTʯz6,+XbA~UPK^g1"l-r܈KB$ɔ'ȏa8W{gWDuwww+N1 Q!F qwьm\]oO/HsyMIWVox|R-i USHD)ƅ>+\N7xR:ŽU--oy+(m_‹lۯ Ⴔb`h|di1-oyCp3 uG x?v`XhӚYN5Hus!@IKz$vB~0bEoy',o;MHooǧ.8"Q) KWe$9&ײ$͆ ^vi ژkxN͞ na]0M_ڋh1,/x[O }~U#`XgH @)0ALPT`!Ծl&*/"Bpمs_#ِ m(Fse"Y?k$fH|~/*x[AO0( !2Ydר!뱈!:>6ԃ9c'J=h2pmzWZ"K DE oylLo %w-ح @?;KFhq !RQ @\Q$j8d&T,ZsEd 94 ]dodp"ސnR̍ "Nm&Q2ZדFF1HZA*|Α7‡@pً։WIE` 穗d~6KdM'SY}.-)D*n i7C0fgs6#YlU1@""a!aݢLK|%b౞g+cLx-}݆kε%^҉xF2Wz[|\ fWU5m[?ب_PcWq (} HYA dۆ]+ᮙ gƀ/2mVWل2N׋&D.HϵR.hԼ[.UfL^ 0?m-oyK$r!\"<,;m8FD29;A$ҿSr & C)E#d" hPPT;B |YKlnBLM@ " 6:2D׺8E#L͟04uY]+!(Ki8uQ5D-|e:[ǻH | LB"yh$[?&!I5lnOn8ga/{x[5z4ZA|?@z$qpYpQQVfSwͤ9╨Y\ y62#@̰ON^j-"ߒR^i'Y." hQ;rCm*:ҫ^OnP%IkpR<.i>=4Վ=4޸w~^HZ r&pS8 !w}FH\"~SevR % ]gK<d^͠{WԸ/HL1z'ݛ a_fGv ?)7C  `/\5}(BrBʶ)L!+#34m IQIBvCȵ`#5`u;AwgBѝK{|hyXk~w[:ZN X4>_ 31x IWKLɻDI6$%LM D򌌄ld=DֈH}SY+J͞!}Wq hRf7[g\Et9jml;sUm6m@{7ɚ2!3Cm}njS[MFuhV9U c'8 ;]v͙CoBP:黧㉌v@"kgڜHSh<0e#qHm6$9G$WmJ\5 R+9- .&kچH"~V4g]""D#9L/ɍgRgjHQuֳ02* Y?iٛ 5T kd#GgG'}{e؛T # QѦ"ږW[ֳuE( a7XR^ Y~L!H'` 8^zM KhƼ޶Ld6*E?KH-YYx-dӰ _zvvI[MUtUU}舤ek Ks%C 4 ^m|C&ֵY-I+YP%jq|}8L# s&ϱW)%OܳbfimmǧamPꙍߦ0~xz3kF0r)>.u{"XiDRL{vpYAAADrzedKh_ Jds@dz=mDﹿ85I|}SySw~MUdHW5=x#RqpJ~laiJ|w3m4KkǼ6o9f " BnlbfLyYQ,(_rh<#fڲ!+uVi1u4eqJ'Kl-l]_G֛̣OdHsNYى֛: j>X0HDyI>9Mꥒp{x!|&oy߶/WD޷/w.w\lgܛ\G6wtD{:iw.L0mct5$9vo_{cwbsN];&؇r/R#yq-/&inIq=#i |"@|\rj t땉k}UvM " '/Psp~dBb*t J0ΝP-&u-_ҼD) w~Vc< >qZ#B(!%zCKيiE^Ӻk*!!y "J$ʚJSۦhC'zp#mTI İuL]{496Fe<&[ @¾!i YjNJ6?m.菷5r>ǹs+,^xv(kx=w -u   $i^Ev'Gu*G9 Tb<au+'O]a>>{jȄ E9< Hʺ{i- 1H:mSA۳2d$A0I`7[`_=\B e9]5wD[ӡ!ڠv;釻wU sIVA-cȃ@} SWbc!D'ڶdk틧7/n:I1nMD$Z7NQuT!"DepkD>Y !re!wlʂ< ^b<9j}e#?Xk_=Ѿxf{~񘠲@Ҿ|v\ىO: },d$2Au-6:Reup~&YY't?izh7Qݢ5%nЍvI;NYdC)uHvMOAR6BBHы^n_#n./ : S2 Ιn C˔;4=ޗʲ4B*U'FL,`bI&jJ G"?ˀUBlm܍P^j/lx#$u9 ]7PM~DBs!c9dFl.w}رK7iH'T{n=$(\VeGXEd"ҪԀdƠI|K% طq$(|~Uj̕Arc[D~Q{dXj Ne`  rHv5%x#3`BȫdG^ e+H`^Q;K}#k]U>K)iu!D|9L?DMùPi+'v = aUn1I51],_zd+{Rg<$Iߚgvpd"Ұ{hk7#_DR";g4#"tRWL}DYUhd,/gΘ5!x#F*AY)rQ2e(""e%{f'iIIiH fUa.|DFyIFu8Hb8e}l䮶f [WoR->HY{NWXUeͪvJuDuGGh޲A>ZwѸ U(KT8zŕ9K~4J=75EMHwd}_H@"M~4T>2c?`8|:oy$rxXeYUbI`(N6xMtKH59IJJ"eTs]ғDJ6򕁔B$i휝aRVNUŖr7X y*Y<G{yH2QSڲIYkaʲ&+ú`4ދ;FNFtz/?2 ~1%s <.1[,Vp#dHM&)Mߑ ^#yn&?e,V~Oj2mThT~Y݃2K%+Z2]诧j|/hU@rMZHBt!diۿ=dD`Ȣ "b摹Y6@y!Bj~ {"d!W]㈷ÁU8?˕,TVEDxuyd$˘l>|KH)1ZEGnW.GLqH~>m<-IZSSZ~˿EuS*Kw"kqvPZZk߆@a/djL P$/Q2_ҷt !4׺좀o q[<@ 3U+RUU:ٗLΪK q>͖Mβ;6w|z?%v-m1ɒ6zO`kkش~-szC$tU7 KGp<0c^M=lC^_d ]VYwl!M p$By2WKP2DdJeeP"(I** ~Z"jCHֈ[2W43~1vLv[5&3#)e9O2gc׈gHޑǷ ]*cE=v&,? ha˝!SDHrxTVIA_n Mi-It}P*HT.DV7-m*?8-H Ie͏|D *HTJ#&ۃqZBE ߔ onE[3dk+LJYOx3, 싇N ~Ϣ֥-c->n)w,#]ƻVH!]d[;dk'iLAI= Iͳ ˩IʈbPvHŸ$D03ʦE.~(AO~ٵ V]PPAȃVmw{!>~I{*opUEG{E3W؁M$pW,#^SHfH0[7ګB _x&~DCS5Kuk${ڐ -8d怱!~ݕ fWnQ Ǟی<9ۖ^<dt~W/eU8@"4kB8 %/U?8-/ +16iC^L=9OU/Xȶ x[.K!BUspHFdrW/ī` ǥIY2>oPPHeS&'F,ovPoQXxcB% WsDŨRkL Zr aR|p:Oŏn#CH2#"2Yȃ@%ޕ=B!h jq-' D>uo;Xd; 9`ea_5-6%qi=Qd⫐zVvG$tMwۘ߇? sƕ߇ /45וVd油T.6m@;p1)N!5y'=hz~eI&ҴL#Q*4R {^zUnOy+\l;tRGhL{8G,]!C" h+7ڝ@' ݻY M7p-=l.xN˨r$+'u>U4w ^  _3 XBon% 5R6",!^l3dr[[2O^(UuL$wy@4xS"z:yDJ'K=<qxm\7 w/q,^Ј&BS%j')KQ:i*2e+N; wm&ȬWȧs4Uk:3}/oF⛩ęAyZ xflM$t8وxgꮷ;:E" nVVH;L>pw.~RȾ%ckb'RLr9RR2z\Sq[\#y=x Q@:ف%pKZŒGɭp:=AF2Gw M֒@(ݙɭd/4ZLES0bk)*i?$p" {$*PSi,հcTlC,lݕjTr2 ^ GѮN:]6W!-.o)ybKBDt!qe</4 -oqG|mߎ^\aN w2DoG;fx%hjrcÃ)@v*v$csPY(+Cdx%ʔp=;<X$F:ėcT:ܦ*"і*'aI[eɂQ>W^G̏ OrjB!s)Tz^` '@j@Dm\%ρ Y~9NGR,D孜:E|V/pDWLݪ*gBM_utyEh@mu>9lI`\kA{y;V֏!s/"ͪB:-2y|&6 E7'!d&.mݷHmm2u"gDs"X*)֌3tk]Gٜ16u`GOVB iB(nF5W"㹌LCnoy+w_d&Dǧ,XDʚ  G*oFGҿl?E\Dڿ^ ~z68+QNo6#Ɋ;wYcc{Ƈ'I+:{s}$yVGb}F)cӕAmwo$YG]K jޫޯg$gs *OUc큵㕙nxV&FխtN"3wXc%mA&I|-HE%ԵĘ+s--Cֲ:Xf׶vf3Zqg+zGT4Ru%"!#Q&ɝ$"<~ -o``Rܛ/Q!HG2ayNU3t~rC?8Yi̜4C_a W$l,H>OabĐ R\[86 ;H%ّ LѐU\\\D$={گm__&8.B[f=tխSg'G?to2n"gxW[a@Hߨ :E( TN D[gWOi5pYH5}_L؂~ZAH:Vё4]?iJ^WOMXd>);Q{]EJƵ/Hi:ymhj3q[唶ld mjEW aW@Y`8M"hq+$+}ժuHL~=kT矖FelbAPUvhk.ZWLUZKLf8~+^Kˬ]U@֑ i:%Vh:!i}yw"hVTcȢd.$.?G?1WCmlj tЧ%!h"=k-3rHNJ'yB1xEњd#bVI0OVB}kv^I"QrE]d?E""?unMϱqJ\8H:0FaDlW/9w}߱|Zx{/}\/'J2-oNm*)UU-3^>TA%rk6"v^I!pDݽ|=kG W0p@$i+U+>:Q\|ƵS,c#kqhoB 7/sҖ$o;_t'?5wgc]Z^iˏߑ @PMU_yҳmCk:[^}%;o]veQo?(OZD\d[\oio5"KF| M쭐4h''SE_i0[.PIgfVVeWLi0d"/P%#ngsB3Ƞr*YE"Rމ,]445BSbi@tunV҉;4=גZ^00̷L(;_Dm|2ezZ*zv6jpկ~D_s/ ʴ|WDr;qc<yp=W %4lhnP4Uјx&ҩ_ 4"@6gT:Ҧ3$U^ 1sV( kz"D״?9ǟGO~rBVuE=)eZ_U%oCEc2[M\׮^^ЌGUU$y>f j$|tO"zQR+ArdY*/)MwH#LB9nWC"S{O9ί\&"|ߵ.? G^`{6%;*":O4{+{KD9_$?H )WU^ `oyDrx=M{D;!\ce/a3%XnUGi {ch/w{ Xz<{xWu#:9.>"Jf%o;xJVE!$ie%-JTDWL~՗"O) Ӿ+s{%Ld6u~DY?5Sf#][]ʒk=_3xFH4 .z){FWÓ+H`J۹`ZRT&aHڷ^`bsH}HGWZVv* IpH>W#;~^ 'ج8;EaȌ@&IԘfBW{oOG9H9%^Cv/ND@[&ɼyhcdYLxNWUِ::-8Q<"y0֙țfs+c ^eG<Uq=O~ eK[̞]V,Xh,w5tǠh;N۵^ٿ Dk:8#ɢJheE8I W5v&l^;1@!Vڢ.3uyV׾~zyk{voޖB$1rͩ]K`L炩#9.^ho^4,` *i$%16^J (+`S" (9l KOَ 51|JͣXGdx 9[i /n&;YZB9;_GäOR\$]b[ {}0L ^.r㌗ٿa+>q1{q8{eE_["hZbԾ 5"b</TP -Gc @zQ@NWkaH2R -j֐Nݘ*&3>LB҉ЋA8РVzObL6A"G* ǕI u)ү-UbI-ݩ"3ee olos& ۵dm*gCҢ--Iչv(DׇO_uG"B[٦PUQr渓gU"vٝ 3쵻#ݶu}Wgw,fqwww7F+@$ fZLP^ TcG2:ۆ.`t?=ҿ)=8{*82ʮ[k}""Yp X [FPѷ6 }ǘk,pD DnAuNa |,fkA"Y] BeJ27dȇȎFId`{Ew0L_(>o{ܣh5ijҠJr/-{0ⷋhNV.uMmNj1P)&kD;AdC:>YF|y6҉|͍rfwOd"_$ϊT=߷gS6a{ZN&# |>jriDSXgj++x4#-YG# MTd!G3Q^N{j>d2Ax戈T*ߤyD"4\0HZbr<3/liɞi'׈` ϔi̍Q?42 i~\T?!@& Y۾;2㾺]>f5@|xkm{b1 vOKE*kk5hւ~AT"f}+>IJuЇEB|=-H~zuއl'K$TIoyd9Rv}R—[I|EkI)2MLRY3εf󺖤6VGJDHr.5;>qMkۥ }m'3>HUωR 8kDR}2+w޼`np4H~ǹ&J1]% 5?v]\v*=a%kսmgW8f-pѝވL&aH==[_0dKQwR\Iv"s}z$O ݅n!BnBwۇ@p~qEFݕrB2jFczp,gC"'D*l՘7lۏ6IYP{fwmRV} f ۻ|8j;=]G# 'AM*,DBD @"z )4 WgTv'!ϳ=>];BlXtR kT!$"QU66]$R9"D q.v"B)ɞgLO.~vM EDo#eYFt/*sWF{>9MJɱvuS?[5 Ryj|]))"*_DMUyʋi BEB51{OXe"y~v#q)k$AhdyRYD& L`n# De:87Sy6xGEE. Z*sovyV{zZF`9Cr$J~7ݻC+f*ߥBLTitҮiFq>rıYm-]/d}YEsךG@$?K6hg`?up# qR0( #F)!FٰJw X*"!6Za'I7;J>YT͵g$j&?-DŢvr,sHԱR+ 7Hđ4>Pu>9)0Y8=K 5rT))`~|E&,zHNRӜ!oIkB G*-L[a V:P㱈/StzRgJjҁ 1V$;W;6&DŃtȃT,(}Ub5~Hl:tKj#<40 NE L}l/*1sݴ"8IK`uNb%Qv\R Dvq澹=Yĺ~# "ٍfB& bY<)d"EE !rc?HkI6F$ e6'<(w?(  B$0Hi+:QuX姬ǐ#qkQԁwG@"+SH4j!v,Mޑ>x ,~$ϔ[ne+BٿlJ-g~zr _sϯx[W=sd@6JQ!qsy0D )z<yp*d(8:Y(ʢVE"8TJDK3ED&TX(j" kˌL;%\7.LdF0J*Bch&*LVDll_SseuTD<}D+qZ5njd@͆9G@"?KAhMF Y,j(ҥkx,A< IY!zL̦ *Ht r_<`@6wOlK4QJAFO"BIDT&vٲ}wCrU+TB:pȯ'-utUILH)* 7 D5RJ*O1kL#!$9$!ޥ yDg2;ͦrSdzP~{Sב 篕dBӖe~ [H0Jq2DzDc$kMqDBZM 9H!UT6'#5 tԺJ.K$/m:yVYd">5iLSi뮌1jdWrAbql5=O'ʋK #eKX vCg_WB4 9ByD,۷vL,}W(A_ot )+)Ff>1Kdr7-&_A#4XBSVW MYC#   3/M凵Ԗ"Wx&; ,E$H -4Q8 6J`EDi/_sR.|jǦu(;9.`wߦ4Vڽs zZftr86mU).l7??йʮXݽCMYG# o4#yb\6Z wX)Mh \a}E3WɦniJ gQ9~.ʳKdl}h|Vߪ蝽ҪF0@BE@G ;ED|"<*ͣs{Ao=&xZug2WS 2q;Bf饄w#˺Vu ím>gG(L7-ED,1'"C$חc~0LldHI/?YЎLaVtX-R%G.z|{OLUL׽$H^+CX]YY xEgTB O6e Hk-FQbh!E[03BJpkW1Jd%K2Hƛ.*D)G  """bI$W&" %&s['=SuQHUH/i]iʇӇDΛu]TȽKE"B~xhב@ q-^5ԃq/c|?-Ĩ p-]DYhͼʀ/)0$~Y*/k "+ш*n;"v H _16\s4Ax֫L95Foz&0{13D0n"b#K\,Q*pZ&37[l&,E`RR_8l>6[Z&hב}T}QJYЏ0IDѐ))|0MwD z9'YKͥ01 kI\9 Olt7`긳}I@$حca4+7YC# ςU4WhT-w^1E.ʃI/vߴ@@OQS" 2d|;h0%NIhθ4-)>Ytt ?'9-i 2bEZK8 22zuWS՛ӻTk,*yhD8^li^N2f75[<^/oޡ,yCH٫ǚTT=F鋕rsJί.(GG@$Ѡnf0Tx7obdx#5-:FE H7mS a)T5!-$Rw.Y4 wC*;"I!jD/LVmGh0>u$8"yqXSJk3|vbqlⓋ!u˖>R;;lu((V)I] + hŵ/8_. EkMTUz^/i*v7-:h4d{CW5bRDrY7.(qxckHp$i06 EQ?k,*%м Wf+D_lgݎ -) BӑW\*.]!")+Ħ/πs`-4'ψ8)WvIv(jX쪵D'UnG@$?$B݅ DrUEmJeӜ^Jueh/~jf$"#Qe1Tla )Ќ:*AlWY0였8!\KGh:D)}o 5G@$JX`;Qb+E" 6Ib<+ VBI]h$24۶LjH*݌ԃ״fŭJ+5w]HDuZ:AZkd@Jqy^c|/v*ɵ}A_YPZVDMi[0U3%J C3п$8/xߗBY8Vi.50>T%&%jQK y+&[EZd BbKNL6.ˮѕJj@sz9ٜɈ֑OAa@ zV③24~w\n摹¿ |6䃧l0X3*ZD:bHUO]  Ϫ +):9Π:<B! x[M[XKGwv;MZNo/w G4r{k " zKxzzL8\EgVhpU"Cy&:  &hV4HIM|vhJ_ҿk̑olW,OCL;0p%"[]i~Zg$r4˧ jSkHp$U9 tk-L"QI[ p5TE/.$"DC;}b GĭL٤i:,HF U+Q(Ԙ /#^)"u=#8|"epXأl\{yjQK,0ӨR5Un61Z΢i#T,JL!297 }UKҿ{p~ RtOGpD7q3{/sM>rhY3[ɪbW/IKl{CXTioܖ o ^X4tIkm"fZDKց^ JCD,mTy]Ȝq0 *ɦyE͈T.~㬞af^6t[vIO-ZiKƼlUL9)Į"fCމvqx;j-?-] וاੵɌKc";1" {KE$9JID 5)BH91Φ6lʖC"Hf|C͇@o[LɊ;jHdf??_2]_5oP^m;h@"&E: y7 bZ$hL!6NET(Ԙ1C۾,洕jƭ勏Cqi{$;i&]6اDe5 YgDbD=> v`;y4 \=+zov(̤ōa|<8'! =reAV/)Ȗm?Uר͜عg':Y!m Ž)no@$rثn ԚOZkz˻'(%Rh$V7_4 oE !oj@iA?Jgi8FNEro+-tWdEE!p9u {~D:gkl]_Tl)u܏J9]JiEY%->i4.r1b{% m O]CXJbtC٥]R8D;SB~Y݋ }(՟hD\+!#.I#S؃- 5!D eZ^89ie5)uix;a8Q8D|sƺ|:WҀH~쉱&Nq0o&L󲡇@ =Y%4>2uF (ڱ'ǍLQnS v-4No1ܣkfl%`ҳ:6" EZJk݈ܗ[Ӣjjh ~,ȉ5]c~_Ba"k< WK_=G6_OP6ꧺbK">1_v3JDȃ6b{#i[̊֘ڪ->{hHSn>Q(r X7uWSdֵDzv(Jꤳԥ~"sIBOE#}" HD#Z4_1SRdl&2CImd h0y&esHg@$QɝCI@k3fSSxo>\۹Uex66~$&wUgEGTnsw5 g¡ !xJg,"0C\(^`lRewc&Ta&3Fc'7DŽ /3>7-@=-*l%!*iZ'2du4ZxFam*%E:i2ʧ>yuqUoxTm7/[y{]SH$nƂHpՙ.urWEֲq(zuTNa@# J 3D-O634AR?(rgE(7 {-D[ְ;=8H6 FDv$[DoV.XEA2p6ըoĞ0R}vF׫c"/ϴY?*Wq{$QIDwD$ĂH\R4; V:Ucu8 PDA Xj̦,2D WVՑb=][-N)S;Š>Ok;C Z_HcPDluDeH\JR .5Jx4R%&0+5R%!qn0x"8#kK6W[iރ5-*D_As#r-EțjԎDR7҈|짡} EdhPtăHB⼤j!y^4(/Ů^ +t=Y|rT""~FRvUoA&JqEե Xjr2{~hZd[i)*H*WZ[ 3A)(+T '@MKzוg,}eRO A dBXBc"_ Y*R"H"Ek(DB㦪_hRzkdE$H( QxeUtpu18' }m\${S$R8kӳy?ٚIT$XBb$Үkޔs;n7irdJ K u5D9(E"ŏB"I 3Z2{] ߤ5ٟ4ac)`/QPD " m 8\Fbqw;gWxǒu4m9%$H$\K[׼8 $Z@" =CQ zzFo-$E!ĥFW^-R AL_ng=4j Z>jr痿}9=EΒ #UZ /7hHbܡ7<&E{SpapwyVYxdͳ^zHV4OGS pF7_H$TGVU͉VCi9"6Bj-6Hpv~ ˯RٮZFd៥s44L.orUdG#7P)KxޅE$ʐ }Rq?'kF,JvH E%Nho#xpmX4wFpߡRpjfo*n?8A"ch#Đ7;.UX~/Xf¹<*"tr]9ݼ돮?ko)e00ho6Pǟ\by{!G`x*q 4ȧ{zܶF$LB֌^ynt g)-gDݎMkypssrλ$˰qz DS4wi?֎Kx9/|+~#iD@iF9q$?$2:{je}VD[QtH)91E22w:y+cw5ϻ`;%ÎG9Wv9ӓDK V/%H> tXO=ٙ$XQ.ǪBpgDQH҆==^]gWAeT~*\Y&& P" rw"?-ޓݞGpK|E"4(d}]uU%b箐"(G hRD2"v Gd\$5=V*젞fWUveOJV2!_L(6SlDϸP=SI`8]$G!Un7z@ay/sHޥ " G48Td@U|4K/0c/Tj9c!~ WzvhNJv!#ۻ zgUXpŏN.8)s x"sЕ#޻/W٥kEK% 0SBwdvD&vfo>2Qu!\3Ѣ l[9"/3vO? }!\}MzC2B~ѵy׶X=WC&V}]m֌|Dw\*dI@;gjy#0B$D"HE$'!h0RYV)Z!+m U-hYT#+~+KkL8kw+:u}Pocc㋿j qg[#I\Z ڿPfn~"OooM{Kǧ+^]5\Y'x.zwoUǦw; gwNNiŧ6byyCmѹhH-!qpF4 ۓW|inf:'FؓQ %mavTG<[y,B(Ւ3Zq"qdW)BS<ϴ4zOi\#s/@L5(\}{U>jksV>qC0guamAUF~6$UG!:pon}suߋNd͹=_ed=R ʓU^)'WF9C*n%tW7D 'fw; ,ɴ"w"w~ [4>gEtK{軞b"Om. ɍl)pn6h%345flAjpudeaE*4=&ص_R]x+)t_~X{T!'(%Zn*"i,E" ŊFXu \tH(&M#@~?QISD D69ޙkxXg9 S5.nu3^{$+rU@$1X^;fX-hrS y& jt\8=qdhjFU_wOgBmff/UI{M/d]_uQ*{GmSWHw)NB2HW=o<;"A1}W_7CDk8q0i7]P9lևE_*åI=aqg$)^ q1J'^l ,7vXb_h3˷,%nM?[H(RGc-c 2ɽѯmߗѿcٸNdn ֝ajz|Ȥ!1>筂?>ivg:F$ LjH>HMDP+R?s#Xp\M"w:9?шJA&X{k^J9F)U|ww<"qN;LSױ$&AIEY۰h,߶IlRo=㺾 a5z=Փ;ٞ%rB&Pu*Biv_O?I1 qD{F0 /1ߗ-ZD$DHkiPQȃğ~tn]3! {{{wJNO+,Eԩю)])N^usG!)t4mOHGЇD.̕W˜yv`n7t R[r7M*-ٵ}Y5UYThO G"Nq>Y|0z%n9ٹq]Ӆw炩^f wf6 p6y1pR_n(;km޿IKx[SH~z˄ /lj$7`~e}.MHH!p30s/),_םjzCHE(6-LL4wObEZo{$B9;* N_sgRH+`ިdwdraXf+'4)m$1DBJ|e"Cg7l*u0dWpQvkOoEP2c$mq'E>=_޴0 yi|ώD^Ou"P ,ك^޴ z>sa/6H#Dh"= z9wB-& [[qz+qȂ"]~8h_Adrx"[o{$ض%*M@t!GA0!F--eNdvI)DAUȷ썴/-@݋޺y#î,8,L ="ʑtO<^WrD$osUʜI$"w&Dfhv>򪽂1Jxh@ezK76D7ő"L(c4-nX8ƈLbF"^lߺYŹag/+nM\t"q(ӀډHxy=,i忊H ;ʓ4JqQ{E1k֢3on9McA@&%l̡-B7_{kҞDlp/8"d;k.ɒ1 R[W jk](dq$8;Hŕ&ÐwkG]Q[t!ѻWӭ $M'7*`~_{0$k_-q K1'LSrkڴwS.k曎D24Dv'ۂ:5<#zx~& q"&D$sCXx@Dy{Ɏw0=^o> L}lx>Tc.\ 0 E!k*% )ɠx &k۽ODBD7"qί[ôG5=EpɩInazKD ɂT<&/zτq VzfD;ZD]>zN頾-sG[)I\v7!OCEZS'MG5S[wW@uʊ}TZѡ *I A3Js qDPD\>.}lOFߨ*m|X 6!ֵۤ[=LSe3jihjF"WoU,nf? (Wv`ft|Wa" [*nlIYڊSk^ɳzJOǢ䨤`O9.MbN$BIֈJ@nP9͆9VSϾJ(xʁtˡ%8:"k`.I|&7&5,{ckmjTRM{IĥJNF  \ykP͟~u"&>o>*"鉝͏L&]Hb{R[ ˟CqՊޝiSWt&;p ({Y?Z3v$%FyjJ{ChC`DvTO 6ś(U=WHbd=TYķP+>b! &(\+7DA)li3W.̫{3j̫fILfi*y$RkLD 4*=H:*DVOjoFm$U2"v5#HڒIz!M퇒L"B/q&560(qʬEʢ_^޶N$_ %#;}fxMhm Pt 5JoqZ?.x"ċHd0My~y\^@3iMpU: P`";6ggI_;eu $E$ۍeD4dBr6uN vɉ!pc@V47@w X~"Ep"DlwE$zk$tu\Y$]=Q݂oILym)A$NӸ"PYx4\[c4N& zI Zl.9#p,hP鶕n`1#E%\YjLז%e N+:+zHWU4;\[;S|2-(L IwԖp/=PuW[<=xn%'4L俕QU_q)A$1v>;)gUw3rɗ$[ c//ϭKk+F[6*UZBci(.Z )$JoHt@"Hg3|M#qv) D7QS9+{&1>|Hh$&D>x$RfF%W -/u)C$oL$HgYfL$_" `˜ st۲ mrmÜV"v9k͒أH ,f܇HT%pv)0͋9)I6U%GVh~T$%B7en[Tu.}]J?]4v?̪jfTy%'S7TZN$IHvvm^4]S<Xו, ZF$Á IHsj~wN&U[N0H Ά4"KH6ߒg R9h"5=~:`ؐo.w2MDl\{+:q|QS=ުm.}G%ndAq/][WUw߉0L4 H597k߮Lj= ei^̈$"٢Ԗ>AZ&G%ǫR6Uv涔!gs[Kv"Tjaue48~k6죧B}|Ԇ~$xFuzVOBg'osLce6O# #ºDAڕ:q4f/_bX.&'md\z+E62oYMp8Ȏ9 yCް_~ HE$2A}#"u"ДA pfKX`G49*aa # R> S)%h#ÙdʂJ2=_6N-^[vԾ/,7lO46F4-EM3],nC $¹/"y8ŵ2iAQ:K:5^&kKeS`0Ahp-+`L1΅AyN7-nVC0 Z5#ryFu|V$ҽ7B&F&ng"Y`vuگpk+\HS۔SzM9 Ne?*mW \Fշ~vv,U'^$sckةzYvM֌6:   Ѿ1= E#߷ë+"i*fJT-I^UMJż|MC\c] RzUGdg'VOq$s6'!HD6&2plc]O֡K!oHGvҏўٓD6D$e疵C֊;7;Yne P̈z̑(+CX dLb dw\z'ٕ" dӬafdzWD*46"S=$M"e%[s7Q4B TL;A$',;ݷy4L%kSt8qIǵ}#H0"Wr/h`[L𡈤k#-XҼFDFYDD"E$ёr:%"9DrUENto9R]$J }"x7wa\yj#tR,pO\0t0 V_zځT "x`/f!>X -!+~fF'x{ˎgKg("):H%i:ze&'VIKGWπ? ~||| M6g`S24<04C2M0DO G1D][v-ьڝC&{$;HidP=[L ) 6FK"UO @\gws}9I-߶m۶m۶mKk۶=Zc4mTU73'nwț/௰"@aZ$ ~  .֕wS"-3%wa@f]?rO:m{k-ϋ4rj}΋ҵ?pPM6px|C0.8u(>3q3?,n/zEpGeCY=8~s2v4d3!~8(U[W qնLr"rE5 ^?[46)@ (A;\ßwOp~~7εW$pJމsCpӁK"jNʹBu!+a$I;ӀO֙ZML$nHCGj[~n¢.d200I&V+nڲ345HFo./8y=;"P+QؑYBg$=[C>$9-q+g%t5~-,L&0iZ:U9[F[$'1VUÍ0vzIWOflxƩM^tԶH\%2xf,:&cTzIrV\dUlsdŸ YXXg;غU8:v]scG,F,/oy8 y~xCfV]u¢~q<&A׍IbꨌdkSGot[G3 okUT Rb|B !Tא[H}AD&,,,Z&ww(\` V#˂uDY\ :zλ8}qh?yYXXԥL#Ê{t5QNQ h'@#DŽ !4V@p  }ql:9ܗ T~l%s$ɑIp0 F"Hdҽ䲰s > Ӆgp݂8c qyN9 De°P ֎Lw~l垣v23"!0,Gj\%QY dĊC;׺?B$Bu/`9;GX$qVb ]qP!M"ڹkv0paf!& S(t:4xi'8M&qDoYIvyV! ߡb)@]!5;V- "X2ݠY :~ jFBԿP,,L&s%=2xv"@V1dŏƙ_[S}3ډR̵GJf8  r6=CWپ7jv>N2 ~;$=- ΆY4GPǖI #~o?4w m= 5l(xI(|s3gqm|V'|b>fZ5h *d>I*[AjCƱ]Eu>yq<:gKǡqp at̗Ku$<y&CwH(,kH2岒R|wBGLk4W?)(d \ ֌#b֪9"D' v`PG@Eۑ6tTn O?1ZtxC qydbi3G&%J]t򝍗;]3O]r@%Ѫd"k?0գ~e\OQ@&#ۅ,,,,T(B1*gwW\V⶯&rt :, 3 ^n;ilSEr)lNzB@$H,@l`0tW>&_ Px3ogɢI9S  $NIDs:e̓RLT>Inn'v"IB0"Ufj4RH.XNJM%QuVr 0YQ| g/gkהIݓ2,Gvo]IPbI|2:]&ĕҹVL3_@),Jeivǜ_x[lUj::Ew$.REK^ux+'7RP2Lq Y\8'_SH'MMz.? Lk,iYK-.tbfHHn3_[퇚QKˏkyTRq-VDsV,Kė<[ H\5GAc[ c:q]?>WQ>uVȰK܏8RvdQXLk,gBp cz& e+(NvmgK® ?غ_+P$$wTÄdn˱On-^MUBQ2o|o4Kɍux&ya'J,yMV4>> [C)+tp!IqP V_ z6ѽ 3)˭d+Ox`Ҷ7RfԟK E͇oeA(_Cn f'g SP=3__`s=Z 7kPb+ȿ+.$'4B ɵw oc[*|I݊:9+ߝoOV0|H!-ѽ] ȫ=^r3ajOI;|A'ѝ[ڻd"1ne Fc/G'׽$h=Ih}ɪ)r6~ F_%9;/`4%5aWrvW| ^L~R{6v2y2\#o)(4|uf'rz_^__/57RsWv n.IENDB`PrismLauncher-11.0.3/launcher/resources/backgrounds/teawie-spooky.png0000644000175100017510000052211615224505336025415 0ustar runnerrunnerPNG  IHDRmAIDATxKsNw\ٶm۶mWڲv2Fٶq {;Y۞k$#/::::::tvvu`>$f(U} /:;;Lෂ? ~73ǃ_ ~5C :;;2iqT/4mNZT,13,2OD6 1?{߃hEGGD ,F^p1a0%f"-rvEovTSbxy{hZaX Dd_l7޹=p:Dd*p{,rKD*]O `+&ڏ6L @?x?(u6fkqٽsDZm  )rdYQAĀ=h∓4qimc䄦iqx39:+B(Se ~ȊHJ:PJ-1M ܋6&恰&`%I"-|"iD 86QRzY|LiR_JM33yں>[ x6ugQym;gea4G̠$3y@^s}]kOwOGQ}5m(a̢EIv^9koU+m^1K$=w&97\ "G]r `+$XVgGjθ|"6lzͰշ^(m-MdLSq5Z[;?\;M#%JQDvivՕч!(4_DɥMS&̴33ojLaŢ({I~.ˍh릭w=jD J0鈢Dh7w>fޚP1v:kZPwc93ȤX$/8{w0E=aLi|'p PxI/b/lU/tʆ g"Lo3$):aNhߣA'|ʳpu&(aGԆqJVItc *ER<鍍m/"cE~b;SvXࢦi.Ν>ȇ1&*4Sj9 ACuP)EuSS]kUCYɳ,{ZW4HEkp:%03XLJ&sQ " bC) 80DZGq udIҴtֱ(͢j7Mfmm1Iιs<^&87KF-6tf;@97>n!l50t}kvhx1WIF_MŦ_w4Z__cP&&Hgfb)**˫e>4\-Y1{aNFm!h@m5!Lx.c#T۶=X)5U^3dmlf1p` ?5^yhkd"8A#m=/?Y][_{(͑0"F!"SSU֨C'lVkE1Z h]ֶ`f8$)~Y^LBXOnw%cǡ 33233wfi'YOJMcXW9tB[wi3Xj5/ jXT֒&cVbT#?.8㼨p'q0ZRq'LM ,`q !$Fžtm Uuu@ :=*'dW0 iX},1:BnhW[(Ejfz%3HOE[z:gxq5f-78WZkk!nyѺ>ֲ2*dSp X%X@Zf/yHM3O?Ba%: "N)YbmXԃ\""_Db% De,ҹg7\Z#:tXUum?t9ijkyn1Ɛ'5oW_} 5ְfZ֬^ˬs7hksκ^ϯ,EC%PCG"L1ֳ`BR)***EAٺvn&"'vU4^$vO?C Ē -A왙2z;|}c)a|l >61i%KC#vatM\{q⻚'E8QyQYbDh̝%+Wq±dzzJDtQXK!D><#}]n @ (!!r!m}zw>C?/B8!( ;b?溺RTYYrokfzB`b*ۍi*70h a"J&#piZ&R<g~qLN\( a|1%1{NfT* ,XFT0g?)"HDs~ ^O k!"RhY0 ̙00}۷`ֶ*zݵbxԌ̹ TO[!d<D@н{t{-XgEg)#ru"~Y ֚+FGb}N$ʪڴ'.Km@,x-[h,xAvwkw=c=(eh0нɧ~a Rr;&ǝ^yX'ϡVɁi]u@il1|].lvgZKߎ́yk^`>e1|0 x?DXUqqшC (7}/kTFL[Ϊ7}XE!G#j9G{zïob!t&o/[~RWc֚7DHD#1 "0*_ 7AetR={] -SDPv@š^[zrZb M,[:Y3gh*m6n b[^~o_#=Fc?É2 :,_VJ?+6Q@]oyfADV)C=v\Y\PJGG?Oxbھ(T/HD|QjR:@^ڱ:7&bEH!D$oy K70yk4xuM]]@ߢa:`%J J(x^vٹ(!;8{%2;lE:u#S^sc/9#%:=,4"b,i$kUMlopq]2j-_ 왳E#c9'i,.\-[pb}KF#ށ=Ꚛb(dž[w\'Q'͝ϙy)R[Så_ak7DE*R eZb-#(B1vX|k -Bzlnz7M$ZWce"0gƍF?Lmwq ESQƘ:~ }lFc[ ( PZQ99WPfa'LOhlx8G v"vBWh4nE ge%o#=qf(BXEy4-H["6EnG¾'̃MqH$YQ⍯z, " 4E5͞=ի0o\8`[1]KY0 .>|bUc`m(s7eG䟿Όl˓O>n&0A%+Y/1q퉄}v7RfOk: Pgy'YF|XBd\ZJO8ȡRJ1F{!5F1-b/s`)ڕ"7 1/eɗb)}tn|@v?t[+wOP_GgyGH&QI㒍hǁQ %*,%RJhHYzwalDlUhQ.ԉꌸV5!|2"lr\ǵVĺQNĂMjRDU^`:QD"1+3̛7 /#6l$L#,XcB8H3۷噧ю#5uGu3.8<( vs٫_gC4yaQlX,ӗR:E=Lj/;XBimj}!BQBq[6?(DMJ@,EۊE"h4v\MDXbXkwֿ-Q*_QZSJ9bMSX|廨l ϖ5С Y޹eK"ugq|.OQTG&-Ur| "Y]Om&^BoG(sCCCJb0ADnTTl~a4H"<)]+Pœk>̤c. ՄHIz>pO} nB$b˜@l.p#zlFStd/)cL9d2\|$huh$z;`#qXk3#hN䢳%W)!XCqә_80uņKʋC;J) o٬^1q -eDh fIr3:)6w[?exxȁ9~| n8+T4y26cbûdO)*X.mϦ\Jev~AZeaz7Unh'\]+K/zHEBCXݧI[޺Gx33,n|%w7 g_J\n\6H!2>|8Uʝ4qʟ筷IQXkKVdw2,\ PX6 IOe/xG '.z8;|SaշMN8A ݒ$!i6hLXb L~$tBWRiEil-k.~,."5$VQbq8C{9en_cOEq(V66NTDQYr2,T=<%.JJ;‹زisHwNիF&̠Xkt|JQWPZMFVR#-_j"Y|NGQ:@Uk̭BӦyaŏch$o"2? S)~Q#*Lo Wgz@(ZͩLbchE2"Jx^"qy/LG= [89FN,x3\Cx?Z~\v/ꨒ?WY+.9 UP^^[+YqmS &M|OQ.ug֮%b9T*VrycB C!c-xw;u(ֽWa !i2d;7qo`ը8OXd|xe,TfB XQ=r9IV٥QȖ0k9y~t'4uBvk/{"QG;G)T0]Ig-tu[X8q,>ON,WhhseW,/`08Jmu9 |&Q*2pSܟP*ʓhT%HL42PҪsp3`-p`jjVG4BR3WIeWQ9y cw|b )XG" kqn9]@֠N%0_Ӊ ?Cg9l}rBۜkZ.+%՘: [JrW&)((ʴfUڗί:k縳bI+G;0^5Wv=\!rvB˹_6;w-B_ќE\6 R]Ujb1ZQIڵE,eӵm0*h~?cyڰMgPF+ JygC%Lt0@"ܓUQ>Z3'hNhCgѥVy4QjK^c;ZnZj\:V>lUr+p]g>uy]`팭etj8m"L\P*|ٴ Mq]T7> XyYsADx-/U@OFu'OTJg=)F)= 6d#ޞͤtvy?{=i̫C&mud.; g3E6A#‚NRw;mx¾M0ZEvY5:oFUcvq睜J073hl7PFm9^[TG^JNEsZ(;p/oSlQ/R2K_J}5gS4*_ȸf>%N$ɑ(R_c]ٴ4'wGgWMkX|rRL:Uۼ,P.r3Bs:9a.GxP(/帮|_$uJ+/oӨXZ\ӷwncf- M4ʨrKyZshGa ?M;:&`:х lb#s$#d"/Q:qt? FHGC8ů(d딩(j߉Z&MDq\.De&be(JXi1^"2\e`ץߥSw+T@fNp[?@mqJRuTN"?w?(rOb[*Iwoua`hx%v `YjQen`dx"㒔?zL"cӒ,i¤Wgc %J͜_wzGHgh58ƠQLit5o{<őqC㾂<8P88RmDEPo`Qm/u?y+3!I,lPAJhIКsGJQI)+l4$+M^)*IK7psD۶Vɓ@_V*ViC#. .^Gn4ìYl~k|L&Vzehۑz њNM۷?ˬz+=<}nDa zn922҅7[Ǘgg8U7+Wz^HNjNɧ *CN<__rۭ7J$u:7n 0.erc[hL&i,zTò9(1Oګ`-ъ.`u)>Lsc/4e~v/*q1gwr g\t*U4d.J0R*CR|30ʀ@@<\9Xoi.^s!Q0 XA?z;p^ll6_M%:P+(Kc(hi/+lcǘ]`'? hy6˜?#r_wn#z.~W6r Q|t(XۻBOry:s$6OJzv̟-3!eb?koBP\g+ O!B朻 Ӌa8|~X/aXS`^Mh} U_&F؀pbDƀC+0X & rDFyҁ"] njcHR)UAǬZʣ̯L^"139lq=F: A<(H}| 8q׭JQQDciioR4I7+ͦ̉Wm2x5=[q=)?1s׶Uqۛ~ޛ^"hyrPc4iJHm69<0Si.Շ*J8pgdo}+;xc Qsn9IQ 6aX䁖\MswʗS0C+WSΞlM]<\lTU*e#㌎P켎K%9k(<t=k=9AS4!U1 *[vWPZ Xe q"%ADХ#%Xn\@jMBn,&.l %jxdmOQGVp1:b**}d~r{qU(v(3涽wO.3,FY3vZz:yey.cu@@nE-S9C&Xn*zzN!q@]PбmO\ijnsbdWڲO(?74>:E.hÿۛYn}"Ad - n}ӿwv`fZ %[)820o#=x9ODB=Q啟mLoW։隯PZbFF[lƢ}!iT ^3_B8Qe^MW-c9L9K u68X.Ȥ"Q=d?V)8K<4 ##8VuR i*(e8q/SэC31lOPZbjWC\YnrßbmSj[~~ёGjT끀4DG({rSoүJX^Kg7/βO_T.\ro6w:亂VNH.ЀvD|溏R-ob˕עВ`M}n<$M}fm b&@}~",#U Oq~p>mI$IP֕H i%sCْCjHޔ8>V~(Yda~zo;mG}(KM]E-skuk(9wyr=FK% X5.4ONx>^=\,h[uAsp!'UC b>x`R#}(;TQS,bgFVxֺRuxS{Ha[r=n.#{__š|:"  JɄp1h(oo`_צ JM"AsHQL'zŸ{0)]m\bPMz#Odi %:#p9D(/lA^};PބrIZ׿{޽#} QR}+~?>bH%AҊw&AHK*tvuG~H(`"~_ϵRXܙ?q$iV{EhS&At*ѽz3RRy'iպo~?O.Wdhtxp*JA3L̲͹GW( %hT*Qp&Fn%8e&?tvZG B",BxqM%ςqNn#bVْZ/ުG',l;NJaAHR[gJT1sFmfY7w?j/뷦)%"rgb!8 zbCmL2? \}hYaVh_:g< A$_?p*Ĺ8U쫸>B8ҡG[ |W=ʏ:b~xtPk[BApSmrt* -7b ɥ%·>ZKy>~AtsĉI[,[?A&ho؀&( NҮsҾG`֭+_#u==@S yۜJG"c~3*%ؾVE@A'hC -\ON#"Y0F8MeV@:P1h,? &@!֗(0Ji01v-ѷd+-XaKx>vU(Zg'XHP3=9dS؂\SGwH5J~֞0z5m S6p XY–za}H'E!,6WJwlVOr }kmňBL5se8PHFO(s<ñFQn_g/K}ahPQZ EU2@yRq䖯'\)%/_32< #E2ŀX%& $c |X%y ]aXg&v9TUrHm)է 11l۴=*FBoᲷQo4@Z\ If $-Qb4,d}H! J(Z>|eг`Au D0h[~JG;V+~;C(hE=az_x?7nR$a^jwX1zU(0Y[Ɔ I6'pF?W&"/NT.,T8# Dp9xn&<.a _^'?(,F+KZg`pUZ& ,Vc^7пYK&Uq"Gu*kFj ߺF68z+ (1lH&`||"((МH%29\DdahS?A:o I p<̃ dolViʉ?"Q*fQCO0ĺg84F$~"0B\ggށm-&Reܱ#7_@)*'e L}`srry0JdCj_M8M$Xy`pGOo.O]̌9pyOQID |ԩ LLQCmdT",ay.~-%IN2M2 3lV1kٲY8f PR#B}FJf@;͈ Z PdF^GY,--zIuLxgS>Bk_ɟW$pM=n)ʈ R %e17ޏj4u'pŗuJl{s([wyPC&{]O>2q %FX]NǓβW트X|vRQ]z){c=?a9MS(.Ǎ̝zO|v,\l0YmD$#-8b<8] OpbQR%CWx;RӨn E<Z5ɷP<|O+֭e6M}|O3 A Ha5!MyA󾛨#R%/g-8=o?mt~+ 3p% r p]i=I#i"MT+ v?JCl?3}\Y,89֮]zX(y<9ۜWV 5J'&i륤:A9~| EldVِ֞>__I 2`GdGLT9gwo~Zjb=%T WRa朣T,d|*Fƈ3r2GߨMo(QT*)щ˔Uڦe0Oc.m΅I9ȟwO 2\YP+L^{p&3J4D&x׷R[GlވRk8ڭwb |)’eFƼz1`SZЉUPJ!pOP/NH놮t>ᗈ휕BB>PN%9.ʏtM$+BRq_=\y!lR&i$}z :=yXS`xׅ2ٵ6o3t&Tޒd$^jwF>vLeNX$mGs% t!˅@zwXlTlTJ;};'VBSS*(?Qx ,,,`h.(ԗ{[L/i-*n? tJE۹ 9!ZIӞRR_k''>CT2~ySo58gqKug1!628B(c@EX$K W1w:r/r9J6+lZhsP'D;iW$RtH}-PR9yV By ̹eBE\s/Z{nVw/~.#Я8봎 #cq!.7kC ulI 0 h8{&/<c(`\b~>w5 Gp(R 1LR -4 i`R 9d~?{Im .I@8 |B/M^@9b$C}D(l[B6eo[:s';9Rl F6bK;BRy3Tx]0,:u~m\sQYa֮EHN>{5[? 2f%=цmHC~\#Q3zom±Gbˀlh` .1r9lXQU}XXO3MFI?.8_1}Euhj*~Y7Kun|o0ns0*$_dNlEiasFU63pQ`VR-Bz2Ud9>l'9@L :XIP<_[)k) KU+kϠDA4‚h5 rh*8v(*#7^O:sb!Z0/zMfnRa\cnKp,Yfdsu9hyECnr7!dm$3TX) òM)U~:8P(T<)ײy&p!9GF Pľɗr~ P죲2*g=(Wᙛװml49o2b*#+Q(2 )o3/# 3e4';~ PZz:49_ekdSb`e8tTs7lP{$.P˘x;E[ذztn6tD:)a!IR,S2v*]iFy Mu&qI"eJs<)\B$BSp|SA@r=<#;`{߽Ͻ8#fѮIJM+(2em 6n(P.@†k!HK `k頮 4 \335M-J 䗰1;8Xr Džc;ywriRm+\3A)YޥRnD%_  3d٥VvHpR8Hv^ݡWoF!CklX1R;a'aG?Eu:*c %TC)_[H %iH.ﻓH D(f횵BVI3B~Q)No.* AWN 7OAP|&C{W "Fv*`R~L 8?xsn溱իvEQT FV\x2A8MgO5^4 nǛ}C:j:|O| ʑϿQ.aGmNԛᎽ|mkQJѿ.70k NQIwOY( $]OhI:$8\2/ ڲuA7NXP [z1w?2w<+s,9?C?RG4k[$ԄzXP{SD Nwn_Z9{7rzBEXyOHlYΊr8.o,zV!Ȇ =㟼Cֹzs^V*YGbud\GCÔ nhEe\\T)/=i4ꞢuW쟰QO% NYX_BUv.~כVn۟=N`^FdudML(vU 21Ug@F vkj~AH FvzHs b >|/S^}M4j3,+VU}%ŁSy0HlvEO,p6nTdȀh}GǸ&R ЮZ rX[6#CNFFxLӡ9B G>GTN.'h4p8+Z#<[eI@MJZMF|3,N 9 \=Jpu4Ibgr7iml`WmS #wn(-ï0wn[E9r7.(l]əX4тYnLtg)6PHppY#w)(Zg=(U9g?AZ$hX%&Mgb!]h `Ŧ۔-銋 RBj@;B2zC[ڄgu8 s ?Nj \?;;u ې?,zBHjSj6V9jQeASZƘbgE1~L,mT(9 `rX0qq|vr63(PQ/e?chNRUcߌ # 8ڽ߹l7%HFGذqZPcL<"P!, =q[c;An[7#w.'DϿn'm>VT4Mx$\lQ?0<TBdrElS=]DrNou~9>fa[8'3*LPfj]~3<~ڠ=uZ:P`K>;mgQX%Öբ]\F(b-D<oFƀ]>E~3pH,#LDʿTs ϿNZ[DYK۱_,.]1Bd48~72q5> nIT* !Nq|Yri7n8qCB41bqrN= @&$sD& {ku>JZ89R9e`rHmNiv>6?3 vg u0b./yx2O(S$_W>v! }?Bć]ugN:y鮉r#-kyږ5ӄ1sg8q9lXpJq[U$dLi Y,5nXOY.n& *{\^P>q|-s/{W; 96 3h[ ԧTM\-B̻INFAJ9 ҶR}B1fxx!Qz 8J\tzw@O4.W\i]pNϣPU  gբ4.ˆs҃ a"1!U3 M _ l&Z Jܡ/ջ.+{n8uREv.ʥdpD?v-5MI)UpINaf*UM޲6,=Z`RDej3 ob'DF\4kI9Lcg,{ 8w|ؽG2m,:\ LDi`ԜiVp9ZH%s-PmѭE*~BR(ryc'^2 KPD u:uj8&8 y% 0D[PV=7Ekmi7[s2i`NLjv@9HmJ2Azh nsaqU}eq)vTW5~(O8gUJZQV.L\jHS_akX{WI/*(,Nv=qVBfCo]*!W {7VM_b=Ir/~{Q@hݼ߾d7džRhgEOذprj{`HE6BEd۶@:}bk1Zn|0B -rXBV$:fmw{f]m j_<<毲zHahT~0PttZjL:}y?î{5&.e_:<im#eY1&j5( DFQkuN^M$rVqa}Yp'kQ2GH E:]F9Y)=7[|cplK Mj:PB^^SRA|jVnwFea6@щDx#hTM۸4&HT\/Y@$x#68?Nyp-9*`IAs0#BalXg0+Waxhc+|BZ>(IE2@|QAc Ë+kS*dfY\e{ַtr>mj "h#!^+&> D#}F QRnYU=U(QB ?OT}i4 O|MLźD ~FVy4!]i*kL%FԞYi?Zih`DgOHM5q81zݟS]rNmE*$[Ec(DƇ܋&c!YDB"O6ӥ6V=ߢX!vލRIܵv p˓Zr9Bb{Py,G,q C G@ɶAgCw:gۭfn-β=^8}z݋]:t0ZiKs9UA#c[nŲ9>ojsZS'mԁa5MA#R۞:pK~Xt\6ewrm6Ic}ˀl* rJVXI>P:=222k,F)`b*RaIfS`K]jEw2,!2, % "nBfBBK${uuހP\Eh \Ŏ=׷|.Ƒ}4 CŘfDx,@oNChPqE\H=I3BRmW0K}Dmp"mp'?N-m17?)֦y(3;ْ9Y_AaPm up;JTWIKK = 8vZ+ *utE9~/t4IqHV9}Iv}Q _Re6!bcp|~&NuCGXg:"͹ig-BrO.V'?9 Yߐ\h**`K}^)z\uCj0zÒ਌Xr"'@^Ìa'=B'<32b5JRdBzjJk'{B>|fd<^~ 9jaNkB ձKi <֥^s2,i-7KHnettqja;33|o{3_4Ήs.;w۪@G$6xm4+w_3io=S'4F)k6Q]NY7M.hAڜ.M*`!<4π͛In+[g e)Eyh>L߀/vgQ7j:sx#"D& fM2%~=õ[GqhjQGc߈] w}oSS 'qDZz:R8G~C~:dfcK%V85W\귪C4GBTN􉤦xHQz2oL3]nK$/% XXnuYZ4۞ 䄨 $ٺ@ z͸l2"H:c^箩ٓ>!\ڰ֝3 Qo/s16(l*!ԂvxP;\395዇i'1%ׯڵkC R)hdR=uȷu ?2 )PXx柘 wDw}#R_ZDkm6!n;LL]Il}mҋ/[oRs'oڅUHe fFDr/}[bFμ#nfO dAՍt6)Z-EVh0Ayá XvZxd~.|]8%ܐ6괓;yݹ;52GE8g_Sd 8)ZIAnNFkE"T5 IpUVgPbh14:JX/X(}d,h!ޞ 9#j[^>z*١r4.X  *}8jsβ="SZΘ,Ñ$hA07r3ש5,-,zɉ bF666S46lHX6MX&ng=CdtDqaLaծ ®s.2~Yqu|d- y zɼU_ZL~ faFQ\40 UCV)m9 2@+*215o^[--f-PƃNip B(&d+%]"o0avl?fX=.Io$G3-O , aT9|J';㜜;{Si> 8H*>+hcXX\3 ;#TuXrm#Ǐ1;3Mw}{fLߊs9վZ8h֖X̑Lnv֙+ι/|!*R[ώk_/E+Vd(.:^N,R@*Ս@ϑr׺.$$R)= `Qё,?apz7y\I nݍ2{g /|͇QJHnжgd*q5ӨF^` ֌1<6F^96Vaxh#dBJo|s9yaykiwF=PMZBwӡONh7cI,β=bKvmمy$/ ,r ccGy1y/-[)@y7Д1</hlo=}v2tgl[D;!V Za-di%ې~J2^9#,h(FP:RjG/ PKQzIƁ*-bvbUO?x'OOx?}9k9ZiSXؔ*Vo^ql2'iZK U\V6GؘbxVk &l]p!QeLK ;XNB=;<7&$kzm9kyBJ 5z@v-6цN'U$r2ϏjF b4Rk֭QW]CTE$o;m (׿J `?Z&Zye3<vr&J)d"Ċ*m^a׫@*Yj<1eݚZz`J C䅽Đ ¨UA.L%"D`[Ⱦ7`1|f۟,#b3&2f٭ v+iZ4Ic_R#a$M>~;Y_q0d{#źVhZ )o~C{OIceP:_בɐY(4-;`CzhnFew4k tr~ddg6YwLlqmE.h"Ǘ:RP]v]`E^Kv۰RZSRP`]ƶ%Ԗ+9k白b<>;𡴷̟chdJ*6]4\J|Fo|k23bpf >O|SsA4iN}B/]NQYHR+FWXFԜȇD"+zH1N$NΩYHMnYf۩$ܶp8 k"nYb1Vw6H)ޙTc 1:7"4 07a֤Ѩ{f%^ A_°[pYY4?&KCGs9kc-4uZY.βWZX\lz*jz]ߑ{=^om=]O})@Rtٳdz$={wG>s6%=cq@bžA/\XQUer*<MAȈSBA6h U{YD6 |]҃RBq:hxpbjpiFk&m,59^oTN,$NW%rX*42A E;gqifi23u{u|J1AX +s| Tm^ˎp<* 9]O |Yi/eY)e4_1 Yizۻm^ElAg>'_o ~vg9xjY{,kty9Goo:Ţwz9f6'v+Z?w6 ڠL@̊h[CFS>$ȶy9{.PkLP` ;N-BXԳG @20}^*HPQy~? e-\:̷I IT!`*Jkz`,>e&TPȜ_?C_偁Po6eĴU @ו` %zVs-7րS~!*lsٜk; h5{f>@+Eh0~e H'5?KZ[{o<1+|쓟`{}˄ˊR*~;_$WnֺL1YPf#-L! q2"@;q:螲̫8ջ"?ԝ] VΡǡv'ڰ- %D+%-N6H9B<3ix}7}>?-bFrٿ muEA"~\ {h(X]U^EA6mt^.G$3\)իY`J\BXٛ-Âm>&d=tS'΂R.i6nYi<ۑMS&9>1$*sBv;Z=7@T85Da6=奘J׸N }>Fk1~ٓ8u5v?TVǙSSv$0J#Ռe % lt=%>`Ж]ۂyf7}7 (.6xPw"d*1VXѕ(FiMyv6> @M*=[)=K!/D b促l}-4Ձ"m 4 䀓 d U- { N)[oalc"(J_eZicp>P.rS\y5lX^2Eݷo}3}J^[<ҘS9~pIcqI¹gR_8q3|v})a޲ƺ,!'zD)fA4p#sd)D$L.,,BG "@NY;Zsf,0uv ZTk-##;"G9Glͅ 7Ҡ F ` S<9/}`]>RR><`f d_L- ,l*wˉ޿>e 3Pj^}-β6V^)Cj"0]C~egUk_F)9dL0_xOEQ֚y?@_“t64{iveE4D<9LC׆[@9$&w&*Y7n,6@Z|%Z(1JN$k@z}~R~./6MJeLxbh 2u@-(u)nz>ڢ547vޱ>0=3*)2s-,R&L' Z}B яDGͅ\jx@ :t"IҦFtrjjueY9g}zmZ/.bm*= |~ dbr&q{|tcE_眗yOusg\ve<4xobbw "N JMv@UL. 2vA픤@drOEV Wo2ȑ.%J!SɼDiT5 (j^!Z+WW:EXfdPIzp0E!KG;ɳ_:S!IK}tQW_\Z4&m9eۯPVj AdXZ\7ƹ/E*Hl#M+Iy8Zw\}ǎpWr5ד>J jwBc}&J58*Xn Љl'<UBm1 Ʋ%J\n}g:b( ɼ_<7dI#B\L+PSs,4&^fG bXRèld,7" qu Ĺd*h2HXNR$gşZ8\ 8Bz^',yt%BFJqȮG 8DݒÑէ |3cS,,,hrtjz#,6'g`jX# ]烉 ?Q4^RuX2,.Z摏Cqiq|8V)] !4 R罎s~y!c[Қc蜋"~<2wطTE:1(eB{(r7tLg֡ݎ]/"T/Y84 u&/ hր3:d=)IjRYJH= B`EwU9Wj¶Xʿŕ>Lbg9~3߹C5Eux%b eNLL`X\iYam`-ˑlӢNJK _70-ԗ?ڲiQJeGGQͦ7'>!kgg骧P݊lՂa,&W7A=JHR_ߴP]pbN2u-$ֲNVdJym5͜m_1}nԻR4 j36Tsl<dH䋿ɉX a]6PEV8@PR.J-%0Ej۫EeKTȼT,`vבֿ:yͺ Frl}x1XUGFov#0]ytmy,Gu [򮷾YMff뵥7w-βO͖ (Ik1)՞oCb2̹5W߯w*7"q+GYq%};ϧvQ23"eIS\(Yci00cgys;ȍs֯rv;tW8qw{n3qh2*B@v,@ O-b5;;G&&c)1=@Z;=Ƃ 6+NjfJR Ty%Vj@bdmP:;/9C?bqQ ty_~\x*E#h#@9C~}~|6y3<Yߺ;}ݤ6l4j GsBXYF~#P͘opg#:ۇW Hl.`x.R⩬uq. ['Bۨʪs#Ls+ =Dc݌% f:Gx`/Q 9JSExS:(XscXs5T: XN3^N`t!Iˈ\L EJY6Ew{鴻_!")D j+]ps^Ғ`$](AkM-M};3m%r*~cm~\)^g Xlq;}p#`G,ڭ%.Cf(]jcpkmFN?;N;*bm f-r=1g7v&Bmo 7{^ggIN!Fg_ߛu')Y8Ǐv@`lyl{:YZIf>ou$y0p$X9 Cu?ξY?Ά+BepXMcnt:VCb%CiMĥ{hl,i7Q(戒O{9q78y?ֳ ` Cj$/+% ;Ib4D4dS-ՈzƹLFnz`4 Sǘ6;yh/v8K'i0QLR2(WQJ ~#?򊵨R?PD;$ZB 4@_8;tcj@!T(-1u/9q@}TL4- 'OUCWVy.Td}:V cLb8.-ro(-Kݎo׸qpa&P(l/ѷ 8PBm,j>,3=zSi{:s5yO֦@G>^3V1sUC_@l~?{#=LQT Qo;<ʪB >ۉIAO ݢG6 h$RXYPE0s~jG g}VR~)IҤXm&6ڒg+KssUWrp춯R^3.dܫ}6S,P(D С}ePx[$ÑRBK椔1H_g{+ᕌn9MxZ\Z`ai7b@~ aKAeB}4l/T׃n! %#ud }C,͊qS֥Fig˞\ N6-9-Woc3 mZ(Sh'=ߦ5<0R a۳.DʁL*A!y w@+@W Vf|O$4y7ԙx'ߍU6!_FQNxz|ECQ ~.+kbL\D7rΕ(*X!2:ˣ RöѲhTiĪGHCۘ;MX2O/Hg1W?UWs֙gMm> XKjs>Zn#x8N{~:鮋Qijg&ڏ_eڲF}6MgjcsġC\v  N}r񼟦40BL%r%(xdRa-sqx3qH$i#6>LOW^ 6R*ΈM-,!&FQh >UxF_yZj$m? c&rYQF6BGQ:~"[zq"luУ|E 4 l}suoX<$9bOI\(P/ѷߏsV x2W*Qo|}zK{o+ tkfrDuẈ8㉻"t !!%9IT%%"L=_oٗ] f6 6<9(<d4SVXZi6ihpH)dz#Fm՗L1Ձ*UFcqFw5>QnRKj=K'qG>I_i4TU>YW2anم)W=.~whxxE$*Ҋ /. VO/|,Wɵt EdI dQc!8R;Q{((U񴳹ǯfh*X, m"bx8)_jТ, E (}8ta>E6qoWK6ZBnA?;Bq-(?h4#qQ*<ؤ.eRBu .[sW2~JԤX{p8僑~]Yo׉t q#m‰=7c-Ӝ:)_|4 ׎ RԦ_D rE RkeiT1fnd"v!&g-mڅJ"3*)R!u߲:!7Ҝ:ʑ?ȁϿĶACuy?uEx5ULـfD)͉ 2<{F:k?mtK{[\Zݢ>}d&.Q_)UJeURD-@ЫRJ*rl6e"Sw݈k5\qWBWf6Q7;[(fw!&F^^\(QUG:ɛ}S-VK8p㴒 ([8wj4GDd!(%Cp%E%Z%3* ),TW~Wn0X!A KW3{>zT]-`SUU:#FJ._t;&a.BARӘm0{9KVEMOPS3L?6Ofvx1:mګ7)/ҿu!G_{cdgU :2r-y%t7/Q7.ZPfF7r;l$nbxnTtU}>}bezG 8C8ٓ Ӗ 1y$w<^&׎YW$΍ fys/|?/ҡFL=oG23oQ=& %""o2 U@V'幩dJb L1KtovEAwk5fy֞DNP1Kǹ̝1Gܝ;H{m^xK׬=4iCgs5~]<>I+POAQ_ETRb"'@pW(Jzġ f~+[ #Ѩi1yW6CFY%|%t{ר) `UQ9Gp矦Hc;LNNVbm )ۡ¸p*Ѝ^aG3-{&/|¿Y`7ǀ{8E{n5~=8LL\njt/B:胂yG;x𩇩(H)!ަ@N(L4)AHHeȐjmrMl&)Chx^A:ةC?02PW{l*ƚ2Ǎ@Ԋ׾iq!!c77pbP "%%.%;(kZJRkxG(dvp9zAzqN|.fS0wW"VjW06SF)4{4c4Q_̿cO#?O 9yOmToo3Rz.BzFdovޝ۳:=8L|+>Ro6q0s!v%_'$!CmbjWzmyA׾i|)V( =l d$Q10 vH4ީDKpGKZ1$S(( R;iGq"{3V_Wn-RsJ]'tQ-%4:.'^x^g Gְ{x/?CQQKcw҅Y >Rf->%fK*(ʟs*C=]L=1K7ZCio#ի۟e/i7֛*>rARbTG˂9V61oNsk$'wN;l!N!'7F*#l2v@2T e-M0)X(ۏDBk3gIׇtAzS%rG*FF=NE>X°Pȉ9ⳟ#t4 c#?MvvT@ܾ^S#MSi,o|RiTi+E1]e8G{AEfn4[QGHeUqM\̭󟧻HB3οA^Y; xIT-SA26&Kܸt/cW.]2F!kX&.#F9d RJ"{vl٦nӼn#`rN7mODΥ?_c]|GɎ@W9swU L6}<7;6Q'''=Y][~^Jb *0T$*Rpy7tyMPEO0qbF])h$ l8b@DkuD Ac$ȆVr &X{+xYozj$KU$)cFq㟑ՠ">kuH;CQw@gpvpͩh4SYPwḿS[7y*_d+'Zf8 AsϾ Zs &}t@'H[=(cb+gi_4&^}O0Xl.q[?`[&W)מ#u5?.TǝKϲ}@|LUڷ >á9dh?ݧtԒDJAN@w=o7Ո%'`&|dDRj%xՔ倁 _Ih`9d!+&XD roc :pIFG*r) 7Š=#G zU^y:V b\|qqr;Y渓u^{Y\`8W߸<÷A'؍ f=좘 5ZCPW>͍/~l:vGr&<tC_M8*{UQ*1 n/™lwVE#55YHNͥ_!sqsA `gMocw_;(c;*͎ E٣欭.AΆ)Gf2d)M^>U-rSW野k߸L՛+5 v۷/ct5&KrεKgf KQN U⹺C)U ͕-LqCTLeClSj525NA&~eC;("b ֌9}W?78(i2܉rD>"+rΙ@׵G$4f[:eclO&)CztWZ޴SbiV1)z6;)0a0kZc}ԤFL HWYYxH%fjl^ڤwc)~lXcb3U1prʨ.vN8vn,S)7x*^ܤgx;}cܦS}|O#xs]bq~_# *PdYX^ Eٹ=ƷY.$T}+^W%$ۗ^՟_HSsHSNP?["2l3 P1wuJ9W_˟"kdF =ց:9:='^#cW/ҿ!>k8ٚs Wl$phs/Sl41vgg8{yo`jfWnD CSC9\Z-eL:(m\4 xpՐ@`aa?o{[9|0V~I[ 4XePWnjIbMpU: =>g '~?7X~h0?7=1H|)U?|i5P6g_:͵WY[[&ӌ0(.a(\N3ff8ȃ,/=8O]\8}k7`XfC@ &&#'5͂N׸x<7t';{DW] (D H&\X." S˅ 8 tBZB@pw&{'i՚DDx(t1MLFgCb79&8ZdPY؏<I;Ѣ v}5w pG*3ks۞y~AƩc]ףMyyuږM4_[_k?՜ ߾ 8,ǯjQ4wkY$7pㆷEǑဢ(9ā}OXkdg9+8ev_lnu2K?`ePKA b )48@bL$;Bl1~[ [} L &Sn/.-ɳFOLOwx;{{Q<'y;]Nw7_ԯןkh(,y'7uݑ'; $͟Yz֖)ТٳX*jD BPڥ10HlǠi IvPQ{^ ʼnoHl3;3DEE%'6RQc`]q*8'x1&H13o2)U2zt-4dbCǘ8xn,eיJ78yq:Qqۚ8g4U xmcJ5φO3r<nn.Ʒm--(l5jֺOb*ۋ9K@M7]ow>F3 tn\f̳q}]"gBC Np^V"A C8Jt@P1N:B0'LLerzҊŰE{+Iʬ!Dx~ztI~<}3h`kcS?ā;b'1U19=W>%>Ox[  ;SL6J;ZKϿ+WimP~K\ F ity!\Y"1F(vBq ya~z/wx <ԥN6h%56 Y}ne!~o0dZsbMǔhTzqbj#i1BqgI~2fwsOf8uVMy{TܸOZP3Oeʵx{mj:嵕giv#b7#5nG8Pu]zQggx֏eݏј GF  J{!(!a*4"Ώ@uK7b gtH1Cd("v1z:k\ǹ'^Kaj4]Boc “;MIH4 H᠆xgn5< TQB0FKa韸׿>EGf`[hK7z l}* 8FzΩwgun\0u n>%I6"Iž}< N(8SG{yo nN>0;ON"Xf @"[TѠE4 "#-cR^Rg2ǘ n 4`f2ԸW1k|^}y1f`w;0tϽKi` L,i4O"G 10Ibfe3:X-87"7->|qx"3AGyP 4h2.hf`~jv#4fi^FJT받*@;ƐV"&nS xTܮs[7CK} Kffg'c6sW7ֿte7vSjG?ҟX^>܊Ȇ~sA_2aбũh;GN` ZgXM䓑+@,í/ VwP `J(RA%i-0,I.5^5=w' -JF=XLkXq֗X]_?w~ S;XZQzl6/~y.^9ϠR Rfs ;",}b 6&X $9 /|[#fGHsÛ!1#g} {qq"IĈs T֌\'"ƗrZ4ycFr XcBmۤޢ` g^@Yè(' -( xV۶ ֢RQI(U7mVch끍@?|zO}|ksiyE׳/.8Zp8}/ .iW=7?巓;ua"]`;( Mڌh"`}>HCyXjMޜ%r95k18l%Pe>\&.L|Fk  4qԧz`TKg/g"Ɩճowh=8H ܸrs&K6˛=}O=L)I#){8jzkꞡ Vwr訁o}1k_P;inS 5#P{^lr r d@9"0v u}jڑ:Og}eU_2+gjeIp9֪NFT&gYBߦ^,PxB+4YݩK+GSTw"1@=nKpE$"3/=ldYba5L ZkMhJ9n0%iil(`'ꂪ!LRsiM)4G<;c!IFԌ1 "w hv EL5f:>Ǹھ€!9Lyy[WWV7 ن^dMcs34-'&MG(g&_:g^fe2 :q ?Cm[Ե$sHl AW'  4+d+,Mgj(Ӈb)<]Ba]y8&dFYs p4hr-"r[3Onb Qލ/dEmw/'r xD%Hd" )4 e:,: {)E'XG  i576J^a5l=%lɧ@ }Yn$2VFD^A6K;s֜ffsl8Xp8DU^ͫ7h 3XlNZ&s_!\+G#{xo| t@&-42(pvhp@AJX:5-&]CcYq,YBB >J/Fs+PR2J4j.<&6Qg17д "f1Xbo2v+'DZ2JF.PMkJ^=FvKqu]]9ǣj 7_gv{[ Ak{s+i UPDILS4k 1"Q\ӘE(@ut$+.qH"!aF:4`OݍRtjϑMsVg3/<ǹYJۣ7찑Osu^~%:kmOzRP@M$ (t:mB62kD!K.>o5rLp7[ze @X"g$ g5Gk#@#X+BAb +P(2(Or\Ce ?KL&~Ewoq"sU6W.^fY{sc9` #ItQy\ _1 *ø-ۦٔ*:|t36g{~sk}ՇMYÇ^o76>mvcw{D>rNz5 -(}oZsrZ+G9[u`g)!`)+̉< R(؎-(r)" Z!* H!%s6`ffWCo|k6VL诛w}{ȝ00i <(Jo͠?9Bb+7{)fK'o70˚AO!]1=cOfџ+Aa`t+* \*dh&Fc@,"g q@Bz Kb15@2E#NWok,Co*Q0tqF(DG_肪'`kΙg;<*ލ@JtwiW &"CQ2Nz4qaTx׾Jv֤l-Bퟌ7e7vByB^uF{|RA*^33ؾIfĀ")-j)3 WN`LAJPI vӓbaNp!*[rʂr5-s8t.h͡^孛Lk<9Zku"Ͷ0gPvvW Y4:9E`"* 搽٠ 5}?m_~ tP3h/X\d"P pMg2h3CN0fbeA(~7ySx_6QJMcQdq:\ci<=:%4seh{9|ՌY X:l^x4 0́ [|@zxN s+Ģ /nz몍-[.] ־G 啗Pϝ_EM^{pcj{&R.ZC% *;3[6D9ImfJ>( h|≪=`bCR%tC|,{TNAd:jBy+ VN1(\5پ=ZP`_ۋ$0 Y kӬ tcs/ :6[[g{C c++lmnZ DGZpQ k(3!]A:\j0~Rx50 Ԥy@'{S:JX1Wn\dCJwq̑\ H 6 8kZvCsOG<0Ekj![o^xe8+JKJgh,~uˆszr ߢ[@+pM~~ॗ_ޢ[uW,]oPvff zgu:vQC''HfHg[Mv;@X켔 Veo`̋|CMe"`)(`sj͐.v֗2y)q]l8|&o=vcp~AEqkImrQZ!ͼO4c[58D΀$/2;V` Z90jRKÌ^8 gdJ䲗'$aVp4S-ֶH Z-m PH7Z,DE{"fCru:mf)OA먨+`dX}J%$`5ؠ]b KU;FBX[uuX@6V+EPDakCTh`$B@o>F*Әj YQX)63մa3E z=fu: - 640R5 ~*EVجW2w`ļɣזVuG-s pre"D&0Lqb@G|qSȭ~C. y<;0lTK 0,"3^DxGi֛h .T+6]|N1+@iht;ݡ:>Bi݇ȒӦT\I#ywc?5|0tN΍NFX3~c<3v{ôqрcfyygM\3tph[0t{n]x}Qo]=CBy-Nvj8 HjXAz{h2e)} H>#h5r> 5ʶQhX1Kn׼1 lwil ԫ)z!i=S-]Vs ͯ V[|vE; 992`nY_\Be: IG3V)@" 3Qޢ{ ec@?9 KƓ`.^=n7?&f&u8E /2[ 6g3u z9=;ݻlMZ Iz/0y^\J(܎ #J}ǎ)ϫ:|+krWe!y7"ﳵE8kX[_g^>zE4luNA9Bv5]=N\SC3ι @Tjz w9c7V Q,:Ma(VƉd8@z$`!m[OYCQ̀Yl'Fp5XyE06x}  uqj.A"!,-yΠmvN lׂ+E6@!!k}c-P;u,U#Inu*jegtzx$:5Pذmk=(/ry7A|@o8/:죪aefv!^|c~]={Qw;@Toa[콹 8J/rʤxM}AVlZ~$(a+X*^pIiYdbrQl ;t'篟3)5`&dZK/^Z Z3DU_ s:f9G=jQDwΎQa92e0EP5$5ـ(- lh=2{O9&x"S~KCx7Ud"ssv :0"N"1@\󣦐cpkWNc1k3,P RnnGzt6vekV&h5"(iz qsP0b:v[5c9of-] 67Ɇ׾Un^X^IץF(4p8I Ne~)~`7vz]t G DB49onhqeSe3@ pr ^5(ҁ̈jh4@1B>Wy=7kLZí!} wPelO4vn/zȠp& PBQ@WDcn$(PX]~YI?C|C&[8t8d0Ԓ:U c\䷷2Oӏ&qr.E.F+V;oZ wpߟ{y<m9u2C7S"%\Hn-𲔍KiRw@CU&̅ypcs}C39vs% - ⚆"m! |: k6'Z3,u9\ps:*-)&0-ijlr`pgoFq L}cуp(. }lk.L1R3@CJ#N++9eoMY b@/ S!uVkA %abArs.(%ڐ!})ln-ɣnCSIF2Pa\lc\Xde?+Ro t.L52klnn`=WYf"/Ḻgv/y[=orbVk;ۆ׵l.DT{78>qR#cs?Es(Ew ׁN4>"PK Q:t )ي4o3`Pp,+Qm_~fbv; Z1=?gFeT[ln}w8!"HЬMWP݁HHtսSJ@rhLfn8u01)80`H @{"Bd/g ىT]bJF15Tc*t@Q7wdz4 8bDP$KcHzm:܉][t!ޞƶ|F[jNy+%'ZI~@CiNSd);3ho {8 "'wbks Qzh0}eHG`7~nLJ}sC>ڌd8?P4ڦ&cfuH0`qxAR&?hnODrnM (hQ e&aiytȻ.j̈́e_9( ! rL.@0XA:^&L"5(乥yhаJg4M $6F~GNF:2,1nC,C(= m;XJRV u:c45RcBFp8W~K~dܑ2j*aT h B7"/XM=@@W!BQpB&j-45z^R :",$xM2RV2tl@ATO_vhEˢwg7n"&Dp8?𗈒5yE8rG7$pY8\D)Gu+ЇLpuP˕(g谔B;lc+:-Ί-C,`qʔ]:ʵ/PPvRet%ʔz”j8~k  aڗ.$?=D̫gc5$>ݠl@DAC7 ܦ~@Z+J:d9xj 1H0je KK=ZUXADh26Gd(AۋoB!8/ 9LIAQqB7fI" [=g}lM6)]""vd$,}6"U<=QhCFv&3ySӻypK`V+BIfz';4D ~W2]ٍɨv`cujC~z/2mcv/}”b/v DZ@p21VV{h " ; Lȩ{N{7ݕ A6`:2sQ S348qHiY*v{^_H)PhdW5 ЏîSf Tвt 0&K7B",CMICf=H %h!bVAY9I&b*@qbi*%*Y/mQQiE&PhF nĠ1*'Zk]0wz򽬽"?^=H֟۫2BKvgl7q@A:=#g TC@Pp^Mm'.N#@hL.]2R3рo, mHNOkyp::*`<) d-xޅݻ :.!t98}E3d?,*Zž={ؿpՍ&6g|AcaAMa%@?` na\\jRI1EsR>+5/W޻ ׶zru\CYO !TE'B ؔt4p$ "p``Azhj^ "OLIDV؍gglz ?w7CPğCDF)k +֣Ì|C!4K+'Y)d~Y'eZL0 X^9P4\;͠9l*"/gb(/תxHdk5f&i5&_Y_^<b阫dkFij {iPF$VRjΉVB @ ω (}雡'\93Rl? 3Ǟ'DQ@ c%Swcd7(3BC^x&SyY"zYAp<8$``={\6 u -_4&ߓgX .wIk 1NB|J5I%F @~P,oŢB1IehF7d)K)7^|#l~ MZlEn5%IGn'5v%ٍk9ιfqOb6SF cUb3{G>=W)$)@5z$bj+#UZlBH 5L+%@y9M{ T@l36]ԁ8 quX) "ٳ`V6`!.9xSvQ\psRjQqQbXz펥ZSC" 4L0p;瘟G=nd6ܭl6У)h9w PΪ3cP@?I+!^)D\ɓ'YZ!pE $)LG\(E\p I@L[Zjvn p@BIջEz yH&gjVs JX춪l TlFѵѱT>Q< 'g8p ׿>dE(nrr9?᜛ionO~4IR[f7g7fg%owͤ^kEq,rwjqDBߔVl%>v/DI*lMMЌI&Pc1yLRzYW΢VLAƨþ7Jo0I!C `j4K#-f, peHR9ăGp΂z͵Ǚw 47 M9~b$QHQ׸~ɤUGc8WE ŸpHQ -14@g~]h6B`ɇ)SnA`1M h@0Rp*;ͱj# ޔ{P10V1 i~s,͑"ԧQ PCT~ݡN#!2Aok!E9UJM.SM2 ٪Rna皣T_ǢcĮ%rRZCOa.}cl~M!s גZyAUj֛jgT Ʒ#F6j{$qAG8}9(E=gRYķjE-&4Pu(N#)<l]zΘ[@9X ``2c>xoM&Wc Ur% Dܨր(TjNN"@5eӢp헊dSPIQIJMF*鸢<ǗLWv,nk4iQ*>WE `GXy꽼_fFԆhL +,mmojްwq5=`7]FLA(lln!`t΃=oњ_qAR:\Q@Ϟ+ L-20ÑPckuKgc9M+j>EelxGB1Ɣm  x|ꬦj?yLok25cX \3\͕u`i:B` L9 D1AwȠݧfb L"$qcҙVJ`j0f)^L VZEdrv"Kps{|UJg/|X>k'穦z`'ir=;df!P%({gmm (Z/gJo9C7#=ok9o>)s%us[ȵ-v:%ɱ8znZ?Q5hͰY+*,{9pQLF zTe8=7;Q`92b@hx[I՜g`9A?u?fBT^zYR#HBGOr_f:A#O=;N8u:G /su3̋M:{Cn5EL Bcg++7zC{^Y 6S\.XEP:ߌb}sUah6!PS8x=rxGՀ 6E)γ\`usC|mxԚL?P Z^"uxw{MZ里bL ~L9fJB R;`^ʂ걌@LsyU@G{!= z[+WpNA.d-~o(^kЛ!r}n5qDLΞ(q)~{Npf5P+PkN7^B(ተT+ʑW_Gl 31K噫yL"LVJ5*4QJs-Q|lBG&fI\[˦RCdWbX }'h!/^WܛNa sOae};Kϳw~ <2k+0YP` _4t+@XwJ X on^@R;F!SYQPՠCcY` P78|]J(:9f BeR;)8pi 3}q\Yq@3E(X%Д G_{Q#+ ;@3PЀ/{͉L1wIYx2 YFw}E:[Si5[v+6彿3Ľ3Bܼn3I#B6&&6=7/ 2;xm+@3 )dFF]Tcƙo ·ONCُl^Jv1=eZiҤRj'5U4,z;\+ < Xvܥ #u,]G:rVfٯr~wΚ?ǿfpQĊ/?27ĔFi:d4;ZpEJK՜fr`†6ҞF2hic H\oPAְư|/SoSB@R(@E?1ŚF9<Ƶ(~7uB} [ƒo\$,0sa5`MU2< ,`PBq6TTmݯBT՟RQ 5@ \0J!f7Vozq6o\ul")@DQ$Z=Z519_Kj'wo͉k2kof?scƛn=qmځ|vg7-'99("s[1!Te+U D Qc틯_ t&ӏ#@&*Cp-v_CNYZX¬vSʃTA@+&` n0`@yi,<0oy{SD /_nހ:ޏ#7 6DMyj8AT(PQJ̋Yj,AQ7{tKx+Y#Guﱹj* `y1[]G+1لM6-ީ$@CDdm  v|lΏ:KEr6Q@+xfLm҆ڬF܏K1BB /d>\GҜd-{@\G "͟#<:ɗRjRK2 JwI&IۿGDZwG>j͉[1='IjGzյ:ۺGe 8bv?ŸoZwᜏcQ7jL͙9lq!!tW9KrH 닼Go:Eg(r~J9sc*vWq!OOr`1z<7q׽07KHg{P*9nj[5^pjNI_CA9tGQ7 ^τEJ;փlYPFcɹR\at(N lse3+Xܤ`9bhRE )<) ԅ2Ԛ7w~`#29;K6R9>V8v G "#h1a; m3:fsmMcURV 4h#r8*:p-C'`\)0e@FKS@+`UFUV[lQk#wsIh@ȋ`y LNN٬zQm|Zڍ+yg.]ٍ(Xs7ɑ8` TtElT@HJ:f60qzL=~:=I 8j;> d[kXX8p{i=SCTOY`~a?I(g=,9©G7>αO0q`\X^\̷^_W.%+؂ߌ<]G'SAĘ ݌$~Ƃ^K\fsk}o;`Mkkk[7*NC V%5 Q`dGIT1/4QV߱ ( ⋬4є1H bOe Ϋmy `za3c43<&~ߘ&A(Ldb!B(J@p έ3J]q_oRWj'P5y79Ūj F)ˀE9eH2BŇ꼼U* G%*BUv_`$4hc}]L=vߎsnf5KH5-Hs/hMp ?twg7n IѢQIkfq?fb&5hVȍ=PoM2sn֮^ ]S%[fɘ9u'IeiY+H4پvĩ<4'z} 萫-M2w?C[):=f?3X]Zf"ϟŋ+"J7S:g|~5gC|CM(@GZXbj+gb,'9Twfse@'매ك{Vͦ)z6 y0 vb`,)liu*5-"vXHjmBP$kP@ss; #{9&fl 3|c;@b-ﰮׯWRcGoz/x@UIU \qTS[A+Tj+ H vRizUc~.|@3Wت*M )*aF#EQs4N<-ifܩ7˻9|xhlHoi-2phQ3Q~ k蘟`(]F%%Qn#kQ\}zxr DӻĠgCȄG%ڬ-_aXe`=9t -.怜..rk,޼k,]׮p˜yyϝ+^^;ƔZNs{V2^ l|cF`94|{ikƜK#/\* *~YPsi0CRX-٘bfn{295Egc6gCSP_esހxsv=!-a5'ۜsh%!3 DZMQQ̎MQ Pk8q+mr)51EZ} #8bS!K7YcylN0YjqeHVre*La,U^;f!W뚔*"Mܯ~-^QssU3vf}稦P| 0Y(Q)d99zdWf߽h3𥃱#@QB~e&xaܞ93KfEVNpIN!&>P^ #vjd!h^z%!jAပ՛Ƽ |5ʴH0UVd`aV/󏼍7**jUS(BǕa¸SETa0z΍wU DG W9G1%[񋫊Rot+}a}&גz"?? [nv.(\ AU [TyN{&[N[v:yw+A~{R+>Oą |٧b P4&K;d-B$11ASS gou^iߪJ~,6֫PKƮ$8$Քk*(AQ@ x ATɱ2tֻ,,n$8[O259A4SΥ\_xWrŋa}ިIJP2}*&b:Ń>Dgma+/ ?q&v{/&_].Zʋ E>2VC jc՘ 1 d@Z[31gn$Wn5'.ղ'$waksup@.cCᵆCԓz_Awe䅣:9G<c3\Uaʗ*a10Rv|_Ns̿*XjgqWMƀ򜖯#ՁoϵTt@q?J􇙝 RpYeה;]ٍn`0p1%c*E W\>ֹݤR~U@Kc88 lzF6q(x[WYY>^H|j>/{mF&6MNM2=5} ' {& '\(a!vRnpU ?,TL?+q$(ӂ#'V@W$̦rK\_UO lP^{^Tkmp#"i9.@h>Ag+\x翟 YYk)oC""j"@gމoDQD:ظA!sB:go]ck>{bu}I&8"•즐׍L@cIZm5} ׿xR _{^ zې:(D2r-,]IksCz` uP봿e1%{-oaDRA Y?ØGRK}Z-W_0Vj{{lRv([8;t"KWn0IpZQAiPgC>wͧЄ<>Ǚ9|Y[ā"T\BR^"2ج>"E.TU]+ ւƥU몫G**kUioURxtZ5ʍO ș ( zN/_y2ݘQhbBhHtVO,_1 ks$  (4ׁUL{_n6M)cIIEZi/Ռj)n.bYztIj0Z GɭoC*G agM 4v\E}ÙhMNq##7>xǨ7`# *BQ R[(.+@"+؁b+W&Z-Cfu7e67lϼB?g3 o7".4y)yr;Xΰ:XL"%X:sV9c~U'V0^}nQf P)/ -@5B,Hh?#!hn%5~S齶lg ̘?=dXt6ȱ9Iq^W`L8P5T( q@u*HUt*|e(90TR6n uQjg 5Nҿb7`؉]_}ߥ![k5#E8՛Ѿ;5ɰU_:+WEIy\H"C׀ Z)75)ϰG܉ W 6ɺl! ҁ5JK}A$;)M!MG̨\y ͑lqm􇬜Y{M&)jöjQN>vO<83S8Ej>t[ .-H<`T0˟?/^AZMʱ?y£,\ٗO3 #$:ő30])qn"[,tX z{"PB?^f̱6Q7Y^&n$ N;OI/ųq}o|G S'ԉDmReW_}x5GD p|R޿`rR,w±(8*€]w@1泖 }3R㵜BNǜ㒵(Uv2*PMU^oʼ^/oƇ%g‰z=~F$(^P=nF !@+F1c_T]G5>M [\(2 "?(Cj'_Gqtd Ηyc/ n@_UOBE=Yufi~{.1g{ny9MfٻLL3g6+lu6Z@%2#k y:Xɟ l_$Lē({'{³g{-$6n_v#`'8B7g^`gbG̿rP-> QǘP)5|ƦqV{oRXYQ Zc^JٳJoS(AJZ: z}*u͋\Oڗ~yF$d{b"Eo.wߣ΀eTa1A5G \UK^(ccGmll[BmpGh|p=>+_$N'd#{g7[0h!̸Lw233333333]d98DC uME<`9)ǒܣ?͐z$AQ W 8V8TalPp`mDYj`BX%:{MalGٿ>) >):ͺ\qO[ 4abvx;Vr~(VK1sQ+*(5nKm hցP*QPJ@ \J!-:nSL捥Bk4 k9c)a5&u{HQhܢ;n%iŁ|RQת$/t3n<^jm@S=7%hZ+=yǟ39fƃ@z:J?/%}E>~SA hSqnO7#Z[2k79a.U*ӖYjR`Ai"1ZLV9Znx:C~N!)Js3g̋-4(\bWûmΠjIm P2w/07ߥc-%#S*t2_DZlI(-[MOB")V3a1Hе]ķ5U76qS(Xuћd/H${셬:S5၅jۥ&ZVdzԨmKӱW^kO w6Ȥ=WNm٧[/? Q?o.ty# TD(ܰ͞}GY 9Sb>)W(qǾ؀6.CP=\Wм6 '>t׮{+V)̦PA"N`U%wc;[Y0aۤ Z>Ep$ :{?&=gn/[YX~J6e}wL0`bJw0C~N8gg/U 45.NȀkhG a5ܭl^0(DD{YoB]J40X@-2mP[:BY)[W[?|c?,IbXmWrѕJeV5w>+m !e^/'򅕞eBJb-9$Ysن%&v(;͜hP\oSzLC1J aQ4N ǦB;8hSZAO=mاH MNp} &ذy=.<3YcW2rN&@PZ H PA1aPQ.̻u 5 DDke@E"O2:bm 3m=@f/#y/I#BYe,=뱬=Ln~ Z3ĭFރOg\ hg&ZlefcFؙZt{N־5&BwF8]^cڭR=C+8XT |' X^MoFx Epw9U I)J~l.O+DGA,ڢx 朐6@v6؈}"kl077=}۶D{P4,_Ǭ13{6x1ԛ-J,9B͌;66Zƚ׷_zWn@XY陵KٞʷMHoֈ6L.HZɵR8izh?ő ݹ?wd,щ<_y(幽Z[ hnKWDlԪ٨Lq`!9!(pG 2M vq#3wJRa<i _m5:lt p;BS|tsAf]%~kW<@?_5[V327AR{hͶX7$Ĵd-qSwNj@FKhcNF X'`sUhCx>X$z ZC)OnAi^'h!KN}/|*ao}}6!%JJ};XyixB햠&J;Y!zsz;v9 2L3{kSV*Y,NۼV%fs߾}k9{d @Cl f@nt?$S8Z.(BkMnės|f3@7 =ۮc{}=] IQBu64:+pn4@>: O=iGE_h@LfE]'놎v@fUB)j쿁 anf@ѥ58GcNytzb(2I_A O6%V:C"IDb>zvf$y6#A@K@ #ki5f071UHgL|&jD5ͦyUW's]E._$C槼 >ҙ{୆?NlS/YBS`JZֺDgYb;&lCwM윹g/hׇa&74 8Mh^on;x)^0 ^zY~i$Z 9MͶGԫO2R%~B"9 .i_-!q|IlfqLkRfə) ]'\eX|2Cd 4eK"5<ݞt:MJ<' (6M.߉\\m (&jh4[(ҿ V=i<|1*Bp^je1I1q`h+u݁ "ym}*u||N J= @+eH+Iyo;F+;RZM͆7&Qˁm ;)t֖$>MxZ\6lβN5,o{ʼ85?b=7^n\Ek c#KVhGҁ,TF3u`. iZHiJ)`Iv@Ĝ'گ-}N>ٰUUD'k5rI30~Ue;glUm1QiM:5U{G]GW޾^ўJM4B;TXGSc=S5إj G~-aмFcn};۹=4Ĥ4OA@U3=\1GUFJrUE,^@R/lI܈( xh ⹈ʎ22T'*lgCɒ1D5 ţaPX!=۶p'_Ccb/AxVp?Go=@%k (l5)>^!2-UL]Rdآ`]ZӪ6\_}\ъ|:'L&eެ'4`J'{z )E夷|]=H%:km7Dխ{5Ru7R z~ιĿFadδu$ E.kJ }TE`/wn@V7]zMBsӟ'A—:n7niJPy*2~FB2aΔ( "SF;MkMĦ_XfN8CVn37SZJJh(a63;kJfS;591,Ctye q  ZX36ŞI:v̵_te===QreMF>l4.RI7=,xr!/,9|w8e޽3:N:7!Xّ%$ S:Y1̤my܌%ɱ>^ >خ@ ^:5E hJ J)j*Cۆ9Ρ {{GЬ7MIԍ)}:3F(jqMC RQl"!>^'_G3xqAa-?!}6=ulih&p\7MyN='V6X"YG n` X6i@p ]AAm}o268d28EWj^-I)GKtB5/uWͦ%p>ړ/Jߥs÷$U䵎_k=F $f `_~[3OUo{ k׭Muɴ_ %.9Vlp=ŷ/,}eK+.j2:tZFcܱcfgd5&\C4Ro Lc@'CiVJoLSzٞV86?0 ϷMm[dBuj/QN"~H&^iײٙW!ͬ@i;g2%?mlnұ-]9U?E ]Y-Dfıh;ؒ$I<T_X@Ӎw#%nWK/?5NlEഷ~ܒ5HRHz ^[ pfGwqӗμ:F"%o|k7/>lnxgTGMَ}3J\]/VHc@"N:S @1jݰfggUx dr9CJQ( Yf2wN\F2KɯfÅOMEV`y}wR54{pԋâAc&9Rr;>X55,αҏ8e2Xh}zׯzY}lnl4 J|1jfJ%VZF?8~GA0Y%~dql |)}>gM_v!fl˵^'̌K/^v-^YcYNz3}9j\C8tX qxX֑EAX=+Ԋ#VB0? 0 А-0uJi[5D)/QHB/+6fFXQ_y'S7^BjB$;Ȇ罇%Gz$e*mav  -ZH,~Gh\ ,-+C)P|lmQ4uYo[~J;ﺼj|5*1^G|NlF K~"?%dzzH u;YMm1p,Q qϾDtxDFNůz ZDV]pک9,6[l'LAp@DG{R5)[ۿGkCwzNi&Eo{雸Px&/]))E+i͞/o}(4\> sgzf t ut1?eʔ,LRlJ)%D2c7_AugFLi5~G( 8Ŏnp7xtcS6+K r:"@B~Ikb븾XԊ([1/99/{'=Fmv&RSvs[ZX\yf$ ( `a f?A.- M7}mjk/u@,tync~;nB3Vf(x9!UN.檏nBؔ2W}kq*QāD*R;4)-=54vLjԝfi'vXLNY͡X34gͽCL^kqk21TJ0I tSZռ%n~bE7d,nzAk^X(jj'LD5 rECc2wk lA㻸KdQB<<``֑wkߕwV/07nD$r3R0]SE d`}JW[ΧʖO+IXu9yoo{#V̖BrKq>#Oߗ )bO [e+H*Y@߭GK'rum gXWGSC}lSY>١ZՊߨV@ D* ıւj ;[MIEz$xN7̷JAG>B% k/ Ljzi)%e!Ydձ]2# 9xk^ʵ8)p- Culgdgq8C;dWN~R;/ s\!Ǧhmw# JۈS:?Hx!3y`k5cH4ʗHv,LhH4BJR e4)4v:_@+1NsfwP.&[dfb\+Zb~5q7jN'w ^?@T!h6 𽅾ұROYZLƃYsQ,^zCAǥV6ǾZQң8W|M]k#r'7Kq SuPv1Q¥/uXgDpbAfc8Sw$X%;{u~T;} ݳOz1}[An ) Wq>BaoO*!O\ ЬҪUH ~q@7C!HBA4:IQɔwMtcljL|1/'23KE =4[ $1jA.=K+mS 膏\kjTNr˥lԙ~7;A9Cv0V7OtX%Ϛ VuGY]XϙoTz@Obg9,_|i}?E=sc^lgQe% v8hv[J@ætHY A M  2WĬ@Z)v]woI@_ϱa2R[0ۦ?+K)c8b ؽ'Ic6FB"*&hE2kUYѼ^ FD-D5jLv,~TAMMMO%㻆8V &d -Zf~9lh!Nެ?!WP}7F|O/b&g9eGPX2-ʼnJK#vJz-S'4;d?^W߁!Nᘪ@nVĪ(L,CXQuJ:g^Kי?9Wh 05 nn:G1_d'3dJi+i 4/ENMut;J8;̡BvocrlYZ@qR> 2XT1EB+ݪ2gժ$YI& M)۔R5z2t~k֍ZɫVkYo 2̿ (Aj4Hfh ͞nV%6jH5Qf>\ +,ZS)ϙ?L[-[L& nzOdXl0%o|ჟ]Ɏ;Yn b}˺j&  :L gCpS.9G[zh + O+d39 C˳GRX +~oB%x:!3lzpº4[_^uְ04PFfGo}ى=~evN8:I s.”DYdjt!ԧ'hR:3c$ 4.V #RsF緛U!&ٿt?]ƝJkJifj'Q*-"IbѬ׈ԇK~sDY<)Q=2V(*Uj{bWGJJ2GeoL?065͊U@S¥:/-8c7#nXf5a&t2&ѹ₂v?asams?' 8se~w~;n!tb=i.^P_}]O`G<yc,PqƈyVꂕ-К2@WL|^tt233"Z(F"n\4!bQIM aJw~(8I̩$M6~y'§No"D=L>?78<+*I lHi:h/QXNɢ&>Ʈ{ѵ9fw@\8&bCJ!'d}bdl˄&XwF.|9S .`9tf oq%W9Sq:?`u={7ȟ3 1ړI!RQO+KYoa1g- H")g.s,(=f'As;sse9˿?|q\::N+ _diyzZG͖~6χ0+͆B&QY@HϓF- [\ qIR(Hؔ 3'*x 5B%\.Z(Ef8jB(^P Ӧ\ S+E^5ßJi?k7o,~뭿߱s8Wȟ!z=h =NGa'4sxfuOOԊL#Gg=|>oE;G産&RΒ]ca^z?sd a6C)$32Q y:m_7_A^ |><-x"` f4;KT7,SK'-R5X7G6v֖$; &FwZ!W(I,T0jE7j4v#h?:bwZnt2i4W+z!af߫򤔞Cj~7QڒsY+k]\RoA&L.wy(zu؋8(:4E4.U~>_fOK܊Bc؇GOkLp3uZp!s;<5''Bx<~|`r8z Ǽ燬>,&o?*#E-xXq*@ ,@^ڷ5DWu̓37?CNUvz:Di QTfx@! ) T!NLxS.L:YJBzb4 l ')Nbl6*RQ!E=J{eoђTr{Zzޚ|lśfMldr;0rQ‰kUC"bb8Z)Oz83]ݸ3Jљ ݧZ>JrRFWᷯ|0NxF#}$ju6d4>f8IF*UI"FQhwOFܜ`eݼco/S{^@K)Jb=[A>%:QR"bKdJHDm+hO7ј"O޹oog.YE7k'4cH3&z︛]9pɲ|YɒJ۶~ضlQHFuV\=rf)_])N/esrMlm({ 殺L0 \gk!clCUUPוRjb8suo  1Iޝc@R dv&z!C?ݘ^@'P-swiZNV6@ǒ_OjA+.K\ u[)5._p:tXPV_<>:lRV<u<{ C4ށGޓiogBS<<zx__>ES\e}D^dH髪gcl~2}lCDN@20w7ìuTs]iIq`k(O)*_[O8`]jKS:SIՃi^l-@{tfGT(aDx_:N&7LQ< w޿~o6`j+2֕y"t C0 ߁cd~$K'i*cL ( ,Ks~KUJ,CC㘽7RޮW(QOfcTBCkYEX,Q(´jDGɌ]iyVTu\\`z)w߽}9fkՊCcUYRեu bV_J{f3nz&ּI/x^umQxَZGcPפ wZƘ[_TaƵ+04j Soy?sQƣaurCG8:Xo9?ۄJowgE))3i*qU (kE=ӛL1x*āVZz U՚7|裲fͻ(i#(JVB(f9ț1 ^fݿӢp5ѫ>$ʝ:$QF؂C ;"2oHJoyQ35Om(zCqU^?T>1tӡC'* 1{aՊ0Jx(JPThG􉌠6T Y,n1MK9O~X0$wq,O;Sk-)J4eEݔfǾЂ?vNz;Q0ɳ9ײ[AC8W3Zڳ:'%i82ˇGG8:x.HÛn 6VFk cmpW_X. g֢BlBLxD8G "J!E{[CBdqx! ܸ{q1(Vr9O/6/g9bk`%پ|r.3M$ةkaJ$ r!6֊Gc)^Q<nj_ɜӳCS@hvʶ AвK7*v&[ંǩP%woJ&@ӡÿum(2uy7j|`# k9ꪒl!fMX!dJ[[c!jRvʪjㄔ*ĕ=?\ܦd>#m2OU ʱo CKɢ[/"c<\r]o~׸4ѝȥ{w;aD5HM8hH߾,Gq!)>l>ӉE0;$Z}'[HBkBj M{GXm $IXksx!ӢdoIqLPUƢC oeu&be(gη:'qƈSJ$)<%i\QBvqh(W>>:fƍ'܉ oBaI^k!}4_H)EBuJMN)1OE]$ū!;|>Sq*7$*P ɹʘTBi 4~|횵4ȋ賓E )7kG`9wO: W(ʭ].̷iwj6i"nt{: XYU %RZN<$~HpU29C,Xz6F9ƸHɳ@n^,"A"@d9n_ɭ#u2 1EK-CA\ M 3@L!O)E[d8Df8?LzQr_\* "P~kM#6@^{RjL@("40ܖ r\)0Ǵ{1Kj~AiN`]qv-\fDkrCⱨ]tIŔaKrL9W)[E+eF>1O>߅,0ځDH!l `4$.>Gk!,s ;N]r:n8`ܬqqv}1$c%HZ3|7J}Nb08('57;#3 jFVv]}!b-KPt[ ҄d_N %--bdՇ'x1B0cOh[k\-+b's\@CadC4 Q>v #W'Ʌqu7Z+hE2prkvciqt'||#LP7 ͦ5g2eƾ{5MXC2%w )iq;l,-n^o{oc ` `#4c= UMPx2 =ȉU`SlݬP[ â,ZB ۠E,MuH7+l+<UR)AF8rDSX,XN:jBVō<*#PaI+D>_=QQ*Ӡ\@[O%PLy'#; :F }ت٭Xg1N Z-xxa ѓ] MT˟zml٬ޣ|<#鏠tˋ}K=u9qN#RFqP55Uh#-Ԡ(m)] 7ᤱ'SKe"tvl>s5;2軼%T (v$g`UY8EPm%sSZay3JRGr] X! `ݻ^A8tf U$8H"d:qZGN& :Y'~ϼz/~<ڜ?w: [N,`,>o# [-SxX'$I4]&8Z[Uh EݝnUvh:iEY| \6XHI=$dwzٗE8Pj!/sʹё^v%)IO?64X(&t@0 Y #KXo.O];qy0(7+$=xb>b1Kzg~m_O|=_ַa;~g޿5_0Mga#x x")5S59 lAZ\pZXoVuYzuaCGZr'3kJk{(N8:>?Itt0ʬZfLxq #I *E +m`%c-s9JrB$3!xyK6CMpZR%w늬'o!O4xbBn:nR-[߮TSUſ~˧ }#@L+B M+hY )$*ӢEYZT^D+6D(Zr#,`DYf\$K$/żj7`S\`CQ8 #:II"ʱ(\gʥLjANbCkZHJ_#gBN!S!O 4lBjBHVJ4ՕPkJnEok#ꑛ?wc7~YX<(L/~MCiNFҙ?8LF)ӆΒZ\pZeN{ɲ!=֚Wg2Ul1]p-NQddZ $,d5ЍNX/1 90F8,?2cj^Vh8p0`l(Q58ul9t8tٍ.9ZrK|w?֛ L_)}E]pow|:Uc)O}o(;ω(ϟ=m Xx0t&qj#J [\pZȋ]ٹ-˂R(]qb8Rl>'{pc~|vD[?ZK% ,@,PnPǝE:x0r܀WyQCXg~x#qyj-0_:4طl>u;C|%qexg}r6٬|4VQ[;?-X\\~BjlDuls?-&Z\pZHh8QiV )g1N+$X3I3Qx8"KS,3&(ؔ?0g0r֫l6!Dh!u=QKIb_'ȵB+M}L+K{$~4H]9$u^HGȥ7Ea1OjM5z۸_O#TzE9`sIo%Q^Yf4֚ Ӣs;4T?@"T9U@Ra8zf{9yAB=MR'ov*0X"dJC@)Li_ 25fEu:$ԡ'Dk!ki)nȸ@\UU-_P7qs8SuQwфBpa84tXX#EU;n\;wno`<bCr_ܷ|,3c#QA~C` !1l$IL(xW%-$I+E G)6da $n-H:蒜9,*0rNV !bw!8!6]~k:&kXk!VZ!IRT$h~B#}{ojZR.e=p?X?i:p7uϴ"^ |싳{wvz˷nDqstDp%P4TԥH+ϷBchǬWq*:';M@!\Z>2=(#IJX@HL!n#ev> PCk߿l A$`X@G+PN:6Mp5 _^;wAs\.,͐&1v0%97>ǟ<O3se$SJVUE>Uh EvnmZAE4\ ֐A޺5#nZst 4צZIӾ%P c TG# bRbqHiF7hq>«ud#"sx^j 9 vH|gS׫U9_}LQ~޽>??;۬r>-FYC=a<_GSW_AxgO_6Hh5hqZi"MuYM]׸vE*B.y_8Ge^߲iyRWl6N&*<Ӽp@9wv>Vl刢8[B6zN)8ٳ\ˆP5<)K1xcI('|rt<߲_@nIK+3Lgn7Nwxi14mz/ewb9+˪)JY}u t~o/œ?(n?rE"6/ ӢRjW˅].kP:+D-kͩ X wQ1,[CB8޽Xv5/M /eFQ003Bbl82 j ŘvG. IJscRAuE\" #</݆+^AH۞.|~d_GN}"uݾfy~^Ϫʋ<H#Pp0P%"@VZϭ= -hiPJm'(+q|\$/ "8㗽׍G#/*hc(R,/%>9˲Q[ #nQP$X#ɠG, hyv.+$ W(D (.˹4CSE#a< כ'VE^ϭ޻uzjzvvf^n7 ɓ88h -h$-ktZyeTZ.n,yd7aeP9RC. dhB4JG"iP%IHˀ_R \}P@ *`!zM9"RåI8JGȋ-7I<;X;?'g)?Zv1_7߾oV?ǛtQZZi"/,fSsq~r;bmHJbm vp<pZ֖Q(9IBmX .oE/ F N^NsY?g)!N77k.-KDFlI~oHZWM}Gk>l.oUy`e) I Pw2If'"MSئfGָq(Z Pދ 7𝝤;vYtfʲP:M2 )Gh1Z/ Ie]dnusl,dOjP׉pɜ-#iVG.UL6g^. en=H%Qlwf g4rWll#s9⛨.-֢"/ tҔN\sX(r$O9F"wX,Ny>jlϳm۶m=}m;vF}k8unʎث~8v4t;BǡZxBIK8SLv=p\UR<9p{%RG޼p,J$ٸxA wu* SLd&)H$: VG\g DIWJ{5V[D-on1ҹ& u tb}mE!m,(l٦lD^OD2 dIF*~)}9f<0D RPB6֥/A\W}J՚5EәԐԛ4ff_3'Ӛ[ [\ߓ:#zqLw{C4" ^9qd9V( xH`^+ fmhQOp 3@)+{ ⽄ wVʕ[(QqdRCIdX(J,CcnV#Nn\X*I)躕 j]ٽXHB)%ڐ:F ѠFCLfҔ smY[rWϞtZ-4 u&3AR8 52iFx6+ԛ3>]7FO.?/x:Kߖ<r|, P4"!y?$@DA/f2P@ xB=Z`)K֟¯-R҆,%r2Yօ,4oM9i!8$)ku4HTj4Er?N LUFCڪ(֚a&R(l'˜#`m\בM߽γ:mZs,ֿsNDpAd1@rB15 %B<"x. / /{ШBq?Lc4M,J].?8Sd 2$(Ա9*p J6VCN dC1S|Ԧj-~~1rUEcto$!N5("('E PTdv^RRLs~ARNrYI\hVb䩣!tb@Ҏ\rh!̶`Aırv4s BL^6kR@\R{x"!L!EA?pI L3Bi\&t4qƥ3'%"Uh1nwTU/_S ZEeQS\MO`3f Jv7 >CtSp="b[{ZШ\x^Ke=|33;VuFź%|/\ɌuA<3V/yKˠA%h+8ΤA%fpءOq$k` %=(&N+Xu}@s-Kf.A {=1rl'r\eT }LxH=tg ׇD)H| 2$[nȿ(4}㨝)"]Y%c_tcJя||(i066bh8~RΏTjx8\M a :`)3]U|b|_Y_f[y]]sF 8~Hfe&J@y32hZ60s 2АZMmEfY HjrI~< of4PSS19(rΞ!ΝƱ_P@Iu#Xu*]|Z*Ѥz5~sobJݷ&C몹rRp Or_[|;P>F^#5 EW apCC|yϞoVw~2}O/{/9B$cp</@qטCF<һpIųNh)eRY7|q\[ЗEnAx(4ƻ cHNOZMX5lltI<, |\$Pk =@YsQmPZtnK3E%!ziZW|+侶7nw4>$&y@|MO9 7+0)_>G9zL g)|?^}W&ݏ^\וx=|59<73uh|vGu֚xC,!BJ>(ԞeYfS]T0u!LlƜWc"z'03MܛRX&I dʺ^Z5R!Y;sZR\䬑ϋ DP`~f8uEb'! qpee\sh(Wjb9| &oP6ON\UJӞV:?'ǔpqJ>h43 Ǒz egfdۼr֖L{n@>#jQ&*D%).Ji4ȼ`:R#i +I^1=(9n̊8W GϞ{ n狕:S™bKn|/'{, z.2{23{`?n,Ӳ]>yu3+"i S䖺QP2.~Xĸq:eZ"Ls6qrgZyc) IZ0fI$Ie53fddFcxfeKnID8Q,u)㛓(3oնI+zh-џXbIHHk!ʵwE/ BI?X,O9`nUx Yv~#1`-_Rg.L1w:7aj[Z6nD:W;A"reKЙM]ړ󨂂273pЗMs硛%ZQ:0Df^;AcR^9AKR rKh_!!3f>#zq&``Rp@%~wkoy#AnVtc#%rV*m49?FAOn4xYЃw+iO1,xR9q$ UM 0;yj"ڴM! XySdi/o̲'.kh֛"m!;;g55z[ݯxL\iRӊ58tJJ*C0t>dly$dq}k:((ZDylN% ϟei]5 D&)]v7پpnwK24H ER eBܒz(W,r`#<GƉGM,bJbPMaYTHlxhl1g 3s3* 4Zd}IKt{?R "&F>,>eܔpxZݞSptꡃ?]m_{칥,I1%\qmwp^*QL\:򈘔 JyƼll-[|ӈtPqcv~&rMEN*9sfHɰr bۥ5\rPL 9"%R97^ @Fl-$ \<Ԓ.PkP@T2~)]㈧N{6\xOX6V ys7*5!0:^J6%a#3xO?lvx9Y(*S™i/=V?``˕7/y| 7t%G]BXR./@~ԕ?Kh籍θl&әssǮC75`8G|r iึ(%՝t:b0vu?'t31d3V>+v:S™b^J=fo"_alש&o\ĕS'p68Ùe̷t +WvF%=Fgi jaI3-~Ja絚X7AO`;&eV~P6MI@'K;9V+b&""  漜[d%V'":rI4DG&)3w@A&\sCA)BrjOq8^l$a|LSxcCWLO *Ϭ̴m4W)yz!-l_J*%i8S-YX\ d6^jZ5L&K;xxԣlSYر Hr5uTdӀg=/IXB)kӞ 6e1ARz"Jt\|rCS#Ó ZVb"4W4@' JJfqkW-<3ՖV8!"ӹz8l[RoֆjFC3JqLE m߶E|ϡi..v5zA1ۄ5k#<<2)LvwVV~7!A0gvܷ^2BnV3Lk1]O>3;[%ӟr)LQ(VZcz 'N qlo{DxNXY6r Qe Vx</Z- mӆ`'(&wq'F&po:G*k"LP`HϞ}:}9}bTaћJm~IZȵrl˷x+O6t]:.ׯW뢿6|V[ 2l1uY|Ѷp+eD/hVWog,t*d:|hvP.M?LJig0|^7N g|CrC~Cu)Fe "͝$JdYQ*XYB PW9"EcA@Uj<D4lFf3 Io =*4I /'ygr&l\$i2y%0TV~9ߜYbp љNB][ -j]Nt"5sH6Eq)LGo/7gg>˧N?e(0j!ai.K I"ХRi^O͵e!=& fR~H{yE:qzZ̆hRBJv& ˒ B&:״db[]O$nd ~7&ͦd3h6яU9z<˗*;\l:АLMϘ&2{98IepT3Bߓs5ZBU$Ac?; *՚ KKwD)%nvP^} z@g@N0#AeL g'5FxynǏk/P Jq<5*7r+^-b9Nn$q G_#"qp 8ecX$+DZuo˼N"dFٶq큲7z [SVq_7sTNN0 >kL7\bki 5?|Fbe,Oaۍ;Kjfb<'l̉ci$eQ[[p%E$I~hD'xO\X,I P2%0(}iu0]us)*BHזj{o߿mtrB(QʟOJiښݱۖ?ոr[[i,hk;n۞|l)$Sw>LXͩT(>@MɹPdr. ЎLQ{ld`v}yB3 L&2b I dc^UtޒB~ϣ 6Y^otf 4CFԩF n#CH|[WdskZg4JǤ6KlfѦCow6mFC7hX~?e#SL@{b~~px2t A v˗ gNBh7I~~v.|K)T*Yv4i@R)poeCS60tP_@zsFl\0q,7҄zE䃛Vnͦ8 &]&zT@3m-=eBӐxȌCiAm-a[28xQ.yvqؚixXԡ+;oYTR{HDl[`aNK’arכ~شjdwҙX n㡙Պ}Çq:zdzTPS™I˝^w~#,pi-4hr(59PG}]$ 2eyMr ks ur 1#d'ٰ%=OC͙Gރ25Bg Ls .Cv6[;-ê6Uu%M*͖({;+UcEh opf\̴ep0͌={H@uk:n+7>pߧTϾ,z ~ݤa'_[w^[' )lҋ;e}Nbr)LQ{nڅh0\P[ZW{Њ H%IHhf8}g~kWeE?ѤyhӍdc4][I+vU³:gy:+59zLrBז+'H`#!d!ݒ, KEtH!Ko~[\$W2Qg2!sm}njFi}QJ3x/˄2MH6h4v.r#O J]:r|=K%4nsYwaqBPy\'lcF?OIѷ][LJk֟|rU||oLW1phGMS™ Aw+q9Ѱ i[2WsÎ,;]/y9s7܀E47V/i.H{rD x@c`+ƮU.k犜|Uq)YfI`2,$MTaIEwld$}g[@gaɠו4T?$6&fLtnA=!,&/^g8 /I\Ry7 aav|+R8+_>X_kw.}ܥ g9 AߠŬ\:~ )dqCgٲQXp-`urMJ3+*Ce`Έ3Tr3L3_]U?X(2 h4R237OX*Џ#9+iKY%9q\:z).KyTPU\EQFҕY;d:roVjf['5(˽kIv2iv][ A2PfRCAso5YBJtm2!7RSӣ\]qK33ER$sRIs96u`lHD6ܕCkaA"E\<{.;kfR1ReY+bMg;uo:[z'Vbxcu>,шXÞY 'O( _ZK%I;"J]6B] I (M$RYJZs uVVMIi`HhDn}6oێ:t{94q[V6jR3پxA^^SiLv|^:-ҮJ$:3@=K0'ѹxL,R#5: 0j]1dgfv^Daemwi !~M4:ͼF VPVy@y`Y]CXJ_5P*~x (b| Խ.ӳQ@)ix%:w X>vJ8S<^5aEznoussu; "(_"we67j-`af좵O~œdpkkQ4ڞv^'!%o|5Ȏâ8BkDnfM*-k0]adFN\5\&(9m6%OFt2>M,?J]A"0LD9)m&R-+TUm Eb\ǕRW ;K! !n#ju4Xα-2cҖ(Zg}{K(Y[_{t+;W.;:I*KH@ĠRdKKpq(W 8Ryl L῰ܽSgY*5^o_w=~xAج/V}+|Yr!-M gϦs ZXYy]wsF4fgiq+^*54Fg\!%R5ɭ/+-z|uk1ncQ q$r8" = /R%ǀC2Xh\%LODN.s1R[s&QJD|%d6?.?}rVhl'fh\)ՠԴ |΂tJIFěS56eGn f:T.Ng-z-+&EGyh!5Ф7]&U'w($픤Hc=2N(`rW_zB@C7|OT>vSPͳlM g Q|]}a2΄nʅlv^ tCVヌ]Q<1B(9U.풿31r̄\;vnyY3s9YPPm`glJL$b.V+5T/ fMŨ~fJl"<;&MD{< qJ T"S:ޖnU]/=h0gf^8e&w"Bc,֔9g@ALGO ZW*s_Z|e |/pdZRw>ƃ-`@/AK?A+2{E[ol8u 0QwN g84;w³]m^(RIZcq* w*FB)QTj/,ly:P(? b0 +bI +RNʡ;V&^R[ps2C6PJY4!I5!>[g4yvF~hSH^^fv)5(Zg2>,UJxPr\Y?7 9Q%FK2Cnwyy1Qуaff3lYJJ _~guwղfq{ZJT}(}\:{tm +򺉹Y&&)SX{~y]f8f'p( Nf5VVG+ jmX2$paydp*#ur^PGJd6 ׺,bd B:N[Xlqp8|"C彵P7u;KVWEINN(Oyox fs}-J&=~\ʥ&^:2@_i$JŦ(\ nDZ7#Vm-z\aklU @Uo6ڱsȳ<ϳ< [D07_%K EF|ˎ8bem0og<c+LPZ) WqsK6sz=S4_{MQ=8ϯ%k:Oz_1P5ݞXl6g}4&mjkzKp/$ɩi8pe^ͱfKl3Z1@JgZk׸Nv-l,099,۷btlC kX>IwA:mwk9e[R D"< ND}?_ml0 O@ ZX!Bn44m*pSH&k[s22rGFT"PՊY'eKZqVkSԆIWfŞ-ͳҨ[5N@eߵKml|B}!-m7ڀ71| M+URJֺj(.RrHk"Uu(Jmld ;KZ17*Tvb|rNfis(¤$r /Ls(ޥj wq3y"77WѶyG7r ɾw]RDY{ lkEKZQ\[G!\̐s˭܅Bg"_*W(a鯐,|{> NZCqs\\`) <Õd=`88yU=tmwx4ݖ, Ώv$ (q1go"89nJJh6A,T9E]3ɵ3 <'T$ b_C4@RH\ 71ˋns<SD[B x 5Y[]312K1췁$.FTPoỮ̝-~⥯)~/yn3wdR|Jq~ c>hbKNJ A0TUq`?x4N7SJ}ł1lfLحCKr6lR-leT&iPBS$g?c36; `opWq!ٰjn J=42;v*P~5rckv5;^z;ˌ 1#(RaO]R\_'W!`0/V3 l)C@68"$sە&e墳>^+AyF;O[\7,po, :y9>?4U'GnҹYAF%~KBG5xZ2-ra,V.]qVj+L ˊGk|r~[|PW[&Tl zo?4# o9>sy|ik&7_}c}׿13G#)QYQTj(ë`JZu~~eI DL |FpNjqxw(8-Z#JkŕWú)(R[zM' ?=BE`=v cj:Ŗ:=꯿ǿtٯ[ T?6K ¯2sgN/9k&A`z.IJyzCJin~ dy,^͍u Ps#s.+GۤӖ\FŅ *p JrkW<~\n=E`4=?.5Lv,N:9'!v$MI(MAUǦ(l8lCaYt])A %8Н#et͑Qq_ pZMrHWsXI5F-v VJZ)5^2[o;~PCoNWK/~Jompdt|늛'gY>0&P% f<7x":J%j*N /vQ+VlfN uO>Jfoɺ~g SڢV: G?z7W#W6o+֦0§NO}Og??<'N],~X)mavx3Pps\A@\g=3YtRPJAs{2ctBJ>Y4 (SzN{S J wXr+<\)q ;[.N|Ocp0~A>:,3ϰ\<4yI)2#AɽVTQdVkEn;+Ƕj(HbS&v:OZZo@PcF&4el֥%ju"jK}t8*b5igC+%GVCXY9O=jFas ׏,/r;dFJeJA?h&/w(E:f1/7R,+JJb)5VpV=(Q01g1>\U$53yp[p׾Ьe<(_+,;O'UM}Ǩ6j g9gLFY^Ef ~ x(p_C+*u׎}V6Uk(ƮT'o'ĄA"}&+gI(鹓q,Z  0>Cm(rϟ- i 6P褜OWLS.=-5@^&DgK])]D3.vZg]7j[`(0'=~⁻֦7+w1Q :TYT$6uJ#A6+ժ,_dYtr,E\umF\(Uc8Diey+gL)*J)5؀6A'}s?OSWgoEɟkuXVE^$(V^XEzN+mS:;6:?hG C}cdD2^[_ؘ ~)JUk/+[8vl'_\B1D(Ħubz,.I? 2ӄ4Kh Fh! H-DQ=.x#Y \HZS-ì.-tZ2,3G{#9.ӑN&>\F#W6sG+G " jLҙI*2tTMvYqEYWBz<~mH96٠YѠިK5blY.Z.F"8~k͖Xbh*˷qPoY `PiVP$R+5y˾}AG*5:_k/G&'NˌW|7J逻yzcM WɱV4b!"c- 8d0Y >/Osl@u( :.QFuk% F1qc nZ6),zl`CQTa'4ZTHy+I_ԫ?Q7I-!SST&8t˭2lģ"^]H&6<6J]*6&CjB܉탵ߒit39(a k\NMQr<ӓMD729-'&|bonPj4 PA@}h T $s=6]@+FX1FyNQDQziћ5hK1gC7ί|QRYkwI@P@2L#"{R&TX,n+qeO' ( v311S?y?F4 yOva%8׿9Og9*k=L47yzmE妠Fȕ#]U@&3Ɋe ^'zpd\i~3[orJх1Ҿ9SzөڋFEgKէT+Q{-3:3fLH;GRc+KҔ̖kuv7g{/]0C,urA M4imJ(\ @nc+@R\@:};(McnvZgX*ԆF-'%6Ա'8ݔimI5 U^s--Yer3m;e*Zer * ]nAkZAM)V/!i koqy@;% `C#tRԚ5VאDb&&'Yؠ16w?j?2FcdTrB( -/L[o>!σ 4:Cۑ ] ȝ+c9V%b&H6NhMcYU)S)e$KPfvK{dWxF)1]xGq%n@&(C]f3E>X2Qc${< $+4$D%_Ġ&-ln׹CTvRlo47Eaxjmkj3(1K%.woo(%J~opfN_3Q,ccesj^5rQg vEDlyC<=Ke"  Hl6*N2b s1ȚƘ!`yy~=>6;VucksÆ&'7GZp^`^t OVgʥyvm03r[}aFY_N@ Y;sa]yMG!TvJ'N{ckWD94qAh oyYp kH<Aej5VΜ$JTk5ORɐ;쎝5!ql;G)QBg,]guaedn` +9SRdrB"p,鯮IɬZbד]tZ w;mҴ'C}cӶ*VrgxKke~J{ɭ1 F(qrgw(#\k"2Pk>0Vc+}L{b1D'M Pn 3=^^$p$xh2(tx7I<rcu6/o*)Qsl<=Gh PLa8qYrU}n&'m׹+iy to[[e"Vk%T8lA`gg:طw?Ôk]AƶV*"-W*ׅpkR أr("ISvΌ0%cɖBVt\^bI)Mu[,;Ù's'x⾻9s.KIљV2k2呁2m PS{3TC?^z,qI(@a ι'W/ R<'Sms p.-sG(T@Ro#4^IZOWMԍOY^ \ x+FdF:E9+V=3 )LKeꛘ,WdC鉧TrNS]u l@-(ZŢRJ\'_ͳkFShurDwKHVnE<`di? d>pB&j qxdLo43՚dD˽.J);j%}^\|\_s]m`?L E!@_s+1<7/C1-U=mfNuz +?o112lA>䣐>4~dsL8&QBb4CPzgںBpl罓&Gb+6 < \&q='Y*Qa,+l&]trqnڥQQI-BD%T(.he!ԵW̔j˗XL ˆFcj*|]*12:Nsxw1:FX2`SwWOr| \gij#U~iyMz )z2./#+uaߓ4e/q8ʺ6s3 Fi(T.wMsע^d<'GdǙ9 Vdt FTxA-S 6u#˃=IQ׺|ȦL: k;-Pۮ{1pϢJUZdDݍ.zPUJxR)wȻGyfmyg Vop(gB%E*_urs8:>= tS~`?锻XT*<8/+F_o4"SNyd̬03.@MHS?rNܟ(vk,Iwht> K݀@6䶓# Vm#UCIrj;7(HpEQsmdZ[Jncm@e QLMOameI1!—/Kͼ^kBY1 e lmrHz}42Tb#ŔVfΛm \6JKS1ʡҼKR3M["dY*g&Oʵi/ln-38?<'qbc Ν:N}MݺZ y7YJd6G]TXe>?*d7OY)dV?c>q4i*Yc2={9JLWq Q=s˽̠&WQ+>l{rQ`:2r$Ew#îy@QxgO)4YãE/? V1@ ՛冩~أdŋy#7y@_f]Pot(ʢZ[: F@i7yy~鬸F<;ڀC9̞*}F_Iۻ hle6( 8@tg>=, Rj*Q:5yL,̕TH0A{s\܆r/ʖlF$vM`@~w&N YN~\r NDc@M֠Ȳ6Pd4TźYBedif2+$!K눂-D~FT,L3zcvb`v*~b ɧnT2Y  lJ+oA2fc K;Jaތ ǯ}WsĎ*i/}. w똽a;pbIg9t*p͵%d`92\ ov)*= "T5`$xUW;u\lo HM{r ''Ν!<4# 5YYXbEpFny6M"> :MVM+Mcv6P \]m/hRBRJ bǞø)aA GP@qy~5HjeoB)o):^RqJ56_ev%L0+/|xcJrI'Wz`ըJC8 ZgJ<ږ8$U ϒ{U&Fz?BHXyBغ@rJ{IlzYw.rjc0 iUbՠ˕C!O ( m)d}mbR,QRV66Pm[w!Š 5r3(W@xRo4H{=r %=1Zq9 ciڧ61ieƁT$ys`F/{n~K( ̹q;I mo<5oۈkew%ׯ3w[=8 =ν?Q I_x4 o{qAjDVJKXRI$Iɒ 0RYшLd6/9rWlhE!in.$lNZ/*W5.H\yҪf0jtd /xEex \6f[ûE|A~gKlP P{蘈!¹Ҙh'Dy,>Q _l2s66h,qX[qn_Õ  73xrlSMOd UtmS#%N<`j/jbYTP!DlQ}-A\B@9P m}u'(<]/y(h13UˇxWy䱻o;r-qw3'nGaWV`-鑘[ZtLV(l9s80gKڬ0% Ւ&04ǚhV_A~Z)\C@+JK?7Cc>|ty_{ad61QQ}IlSOh5Y?SvL99M<.Q.W0 T047m̷25sv:Ϳ0?ab #,K%5E9DD@*8#I$޾)P{>8qfd֜Qf+ⲟ<[ttV Hpِ{5ǔ1.q7*d~c$s8 L\ 싔$n䙶1 w{d܅Z>Eos<ąahb_Zv>"m\ OL`?mY.XW7%,CjS~-yu:ahcOdlas_cÀp6qybeI, ]නg4Z߿chp\-n5I'Ɯ6cwT}+-J\\Ľ+kJ){qdlz*pӻ>%rg@^ZKe9I|57SݟAjpơ dƘ<Vo_f0k4MY^ oxmk՚_̻?qۿJQ(qnCwBՆƑDv^: mod*lvFAr$l<ΗKJaq%WV9瞇DX-P>"P`bMp4: 76Ͱ(F) t&\ [@!?A4F:A(T1+ZN N+ (x魭 ի 󭧞VkUT*78z O=?e}%γ jvٖ&^eй檧T6+P"qߓAdr c,8}KZD!&WOy)~șSB}+c :]jI<˝"4.\jq$_X8u:-fk iixpq.]5>>t 0CgO{y+p }6{a˜)!cL(uQQaL( F* 0jrZɋB_w=l76oQܶB`K3X ~'=}o\]^R"Q߇:@PJ0el9Ϳw&s$l9:n&v_ N+6^SBu\ê ;`KÝA@/$,(\rK-k=@w^#rax(P\m)J=FHz ⧆,J[|wו1뮧\Q،rF$pUz7{[ez9 juΜCr`(ZX~m7>~#f ]aNRI@Go" UF͡1 oFPprN$id[]9q4[f|>ʁ-5:Ҏӣ4 y[,vAkrGq}n{SDV*nJ  ȉU#ЕRtƭYAZaCl(6.18슋*jƃ yLE̮5x蓜uՙT%쇎_w+Ы${]/nfa,^$Yn}QTF(9|03gԎx'ӣ;ՓgQTiVT\hot$W@ )_d7< Yj۪4GnwP|nãm(Dmh඙9q,qjU;߱*:&*Unn MV=Z zuu; 0´: GR3PIW޷hצ--\,yNE* CLA(.Ey UE/)ܤs@D+Ik'" QIo ~1Rю/l-~YEX'qǎ-d'Q:3+o'0?E))bDr)TV.w`/DDPT)##_ h'oO?ÎF䡥.[[j;@sG׻qAlK'M3Xŭ;a'rs^] w -(⑱$(6MiN}ki4I<˓,I*26_7Cה|WAR BHɄݻtn#_,w_|Y5ZlSIO|RЍqLs0 l&!' P8i4pL]|vBh3<] ~ FYA.{H۪%'7y;͖\Y+X5r,J}Pn r2 7 b]e uԎ}WaAH =Tse%ozw_thZkvuwxWvw8. m l.Y?uxY3ND|mkl.v: J#-mnN>\mZvU_2'>ϞN҂dI( U# n":ǁGM6GKJ(ETєMFvP1b)_ryV/^`}uEF |aFJOޟ( EX并;1Ӓfi& 0 l}ēQo*J-않$j0qM6( #˳uAU>c_ks'+' ֥4]Pq``e(۔sB,@X aE`:m>-vHJ|8<嗡TK yR6HS"]Ugl,@a<3Pa93Q}"HT7%f\>{ʸx'17X]03?*n<{(Y'*pX7}XE.3;H7ó}AnRgVMl<e ?twLF0՛.D0?;/w`4IOAyhJ(GD t~`KNCն n dm @YA& Z>*TqVvk\ @d8e<='i9rD]1ԁ{>B6ypoiK}~Z)H{?m/ ="#F>4 %|O m-o}W\\|Q@m)9FP9~}ʳqj+-?R3|~͈2R\gvofC8}|stz(}wW~ +%v-<&~/ѱv6˵:~^;D im3g呋dDGDm8VOfߔe8VeD3)e hy&Q9%Yc V bڠf` ҃S5 c{tsp.)c~jht(I"c_'su/}5S;vYJ!+䭄Ջm+%NuV.˗[乽Hn@E ρ51:y[SLy(p;9r8āҩ\o>*Jj+WdQp|GjC:e ntU Te qقverϹY\M:N`=Vζ~&\;1x,+[ɚVj[xm~˪i{O}s\OkxN 8 ]6 fA΍=^w#\-r}c}kWe9no7;/Y]q㪣0bd.G93[Ept, ba*QFF]YR+4SC(rc9.c\F\Sq ^}+?puBF>}'/Ŀ%Jkm?p[,I(~e&'ɋV=~g(2Fy{]0OS+gs+#35)a64ƨSlyȽUѸM1~6RcS#rg\͆,m3%(rןs_ǙoWJ9Âgac@ 2t*JJh7܁ov<q^ܹ\+]gG. 3N N /\v$/yfMF #.9E"}7۵~s~ozi񢔂Ēavvp٫{ vh\[g~܅Ձoa_8#gV$Pp !]:'9yE )ي o~蓟1RRY/4kC[|˹OH/~8Z9٤T`{*Ȣ+Ȑr$*Ex1mx0l\pn8 )-3Wr_ e:\z΋ j8bSۏ{}Q:\iw:ր0-# [ve}h!5ӡa\w ;9}J/Jja(47)0p`j|p#-'e>dx|olmH+^kr$4eml۹Z+d]P,9~Q`+ypJ"?}_$vxUSfxas; v]Ԣ!J!{&E.1p큛8pZ-sYNluyc,̃R"> 5>\CzL@ڬywI ll 5~J,@lot FgG hmDv7N((H#UΘ߲^5DP5k5!`)Y)M"ћz.G_Tv[. cT[I |`c %wϬRvLi|*왇9weNlJ0Ηn kRݢàڳ[61,:S ri9OLL@9¡ @A"K\ӝJ$ԹU,NBk/Av2a)Ղ NɅ5z6'NS|ٲO8W8wg|C bgpYZ,B.qrj9{Na˚Hie~}ҡGOd0t8YR)12669|KxkD@Yktಹ!~k/HrɺAkEI?ؔI N<% ?#=i/=rK9g @3(q`A37ZŇhV#cC)1|iyk2[~7R)kR8rNSPEF!NIsO-ҎiCuFp~'5,ML퍍c1}fy Z-Yת#CMϐk (l1EPj#D(OAWFJ(u{2 H 8ŗ,gq7SY^߅#=M v {7=^x>[ z{w^Ήby?Bk8'\-Ye"ͱ3BS܁/i׿YVK٨An^zI^Ʒ5NN`O%./]BV_J%>' q“1'1Jv{?ڙ/qyMyusr\2)v׺e.mv2.tg?_[Od k64?̳M `r ~WZymZF'yV'7Xk' Wc.oWZtS?WR ?'A՚Q $_%Kqg2nAw8ù9B4K;*~Aiÿ,{NkIQt;,I/Т`[h)q}KQ8~&36PMc=~t\% #]wR9ں8fAfW&+٠p3 7ƀߓb惗XN/w*6mA _ se‰*kZۂk۱e8 嶐 l[[,#ʬ :Br=~{W:@TL6'ЁݯO]ع%o{7W.ejmH w^| @01>E~JiZ/<'  j|+07R9{L29ǰs|a"'9I^x~V5֥_̕>6X~qQf36 Ɉ(>xf[T|Nl{yO[?oi1sgC5u9ûIIC1c(N lA7E9*.liM '?91&!FRūM^<B61֗jng6)J^g*#C#N[(0d#s$)k]ܷݗ+;Ӥ&w^c<xNxkt%"'.m(K1dN˷SBy_)!c>t0r\ *_HIyWKq9S(_eRC"\$69r:>LܳrL)(QWHDcjJQ, ajcdQ=UngN*OZO E2lj q[YV qIz {nOsQ4Jq1< W〗uRyN>٬0xl띌W]3KR '[tz)~F?YJ3trS(%*1jkҲuyrZV 0J)-'D&$#njlЄVFfZXVتsK&; aը پCֿ 1 t:Pe5uz;|+ǝ>q*l4P¸'f]TCcW8;_?ycѧjNfo:&蘢jʔJEQݔ&ɔn.̽~4E>F43՜HvxE\ S2I93"- ߷'!4w,T|J%Uq3m@{Tg;k8')Q5;rnWpwh,`;!-cR̎>p-eI,Ѭ8z^#2ږtSV^bgowR +ze;YL{f܂蘀WۦY.(,{l4ŀ=[fck<`gtd̺nm~naT읠\i/?]ng6UfGr|ZlY 6{D \A9 ˭BP _'UXQLhQ`@&ƫfQ2}5;T11jbyVJj 0J#?LtKvixjϬrq u7?k3KLQp/󥮋lY<,U+5}_S&kԗۗ;C5w2T\8( [y~?|S_XGhH'1|KaQŷA8'ь* HpqÌ j#0far?Ov\•!3dVu-f/^S_s3=܍HH2#AWS79,Rm 34SYJg8Mg)|r '.'H[6ZK*R|%^I9ȘfQr|V$} }BFp6v[ErK%nEl;pC7~=6^iwML;{생σs~0a,RiSs'+⇥ܴ{ (tKIm++m~'$햝KulqA9lɊBJPNXsF]ss& "'Yru+cW`yN-WS48sU}}@oU#xNɢm: o82殻[^:Z ):}4ѡ&il/B#[^JkFo)G9},_^ .ŃY6^e&ূ0z3m)QGaBzI)5|!n4{J5r[#0G)ķ~>WΥ|pwrV(W.R^ (8|]Pؾd^PT.>/dImi 1|O4pgϸ4~F}Pfu0D7Iɰ)$`IJE$jfvcv/3s{}n})_JƦ:pi9&5db) h)Fr i0rsCR(tZXgȆ]F$T";\|i׭a^yn+-1Z;u^thRdZg}~d'M$Jɨnl 6"+&r>Eɨ< V3ą+/]a]9qSI8p5bD\ (򦺴sk60*iU%d^n9FR6K3V sw̎M޷|-[?sGP.Kal1ŷN#'fgʥm@z,.]R~W 5X^]GRy=)^yQ89Wi59 U.pW'EHnԒ+I. #_ KJǨmkH/wҘ ُ *BՖȄ]]oc X\vnldDeuZT乤9MTW)|.YnRsSg<$l|9a|MZF9Y0,I@ir$%8rkn-X5@.-x8w Nk'm֢fo_$v%1Mw#WQuUiw79cFLl[ t[eL.. 62` ͪF, :0Ld˛nUC?Hu~"K›q;av쿎7ů7X]Z 6f ˉzuFiliꐩ1և_x'%%9#MJQ9cb=h(~@!^}X37"rӏ<;^qtJ@~w?sYxLRBJKti;cBL JX dKQy_W+*5V/91ĖmTP fkjX][AspzfiC]^+0| 0ω Ag~7(W* iuy20J@/۷*\@>#@$Io ~t|Kkss(_w(40zI͵Fk"mvU!!ajNfS44NP=n+5e;1=-r5Ɓmht l+;[Skpe(eIm^>lǁ<$]rMrVlOz\gmrJfIm{.|MX9ҍyF6ݶ~t)Zm|\Íai.Xc~@S!q 4<\X13p_ځy}am<~ust> К^jJ2]~[.W+"\ 5٧M&e~ϱvm{'x79Q750;j?R(RʻkGήU¨c6x)^wݜiV"%r8W0a~þ[F{&ko6,G {~/'*T-(=4M`FF%kUAD2O1# 0p0uO2oio84 }[\qdþ%7@-'3ԁ엫,w%@ϑG011Yi]|<&vKes+7l\aiBU&1i~k|x`zhu)/k5~>M3w.cu{_NѤ^'%T}򻾋{+ҀrcL/dy;싗Tkl. ڋ5r9x3&c<΃X[gtfmQ[&}zق5GX^^ɝr]}HIF9]vz[ùP/ˬ}<.ٺ\ w T3q.-Jr3P|KQk+˾hsk/Y^#'N:$өáQea3-(D,U'eT|aS2,SL;w/&V..T<ߓ%Y?9(ƍ1Cʈ$اҎP^ K +9YÔga8@pV֏])ZwP;d8gݿܑʖ+3oVJSzH?'Z0< 5"ƀQ9ܠ墇MJuEuƁb{d)>)+A?vn/,UA/zɡ)3TϾPͶͳZ_uc-{5G{ JK9!`SfߣAjA3))bJGW/_ (  E0VMjF+Q}1ۓڵ فR 3-}{)'rLaz:~He"˽~R(lX j MDӚ xEUkmjCCj=x(,GzFNH"K;LA)pj/Ai |G+:w3Ef(T[ya?ӎ]|d;@׊RRuk-"]>g=ꕐ'Izmb⊼_pʨikdYTgjh(W=B9n jc0.=+=3K?2駟[qԫu>Wk5%/3^rvEݓuq6/ӷ.ww: L:kؕ)3g g y9jPV373ZtP <~&8,V\T~V[ p)U-jųx@@d^ @seO5mj~o6JkE1E`-}0(3LRU  , +mxd!jQZw3~? Ԫ5gx&ڷatRD^g'ᩉIf9RPx:+(TYC]an|;7/xcw}4R6X?*ՆA( ԁNa0\r_!SY Y偰4-9W\K`,SY/"4$}+grY79M3QV/˕prn+xQq`ڥr_Joo6#-G@[I5Nr1}Yƍ.P%yG MZud*leg1ۮ!#ii_8^2CX8d{^mN;O7bk җ (| $K6'xK^VƸfX]i@R2Dȭƥ ^`1PKyZJdCd?2C@ktxB2~#ٸJS+?uRB]E #FGYt0 xc%>xvHꢶ$/IH.wY\qѭJ6ӕ-wc j@&-d-g q [,Iz'R ŏY7}6#+T2g CF,/ 'PZ&ƥrF!J~*gR(@7( $CSz}' ʩ[7eIa-vzG淿O9:EeEOcT\)jB94SU@km@!ss^;ȇeU5㓼)pȳDڴY.# e5kZseONYn #n2j0,dkvsh6Y`=:)&d̡5VjWqV&)z7Ƀ`4aj(LNթ]0fw2lM&ij/A9S<TBlq+yK^OT.Ca$[Hvd)/%\ )1G~/=xgPPU)b@97۾aF vΝ<>(3Rn#jv##~$KY6 -:rp%-?3GgJ]FdU^wsi{ޗF00$ tfī_nՆ}CQY(4yS,#V,y!e00J:/iܾ Y$œ&N[ f#jHQPBMP&gyǘ?up1y[nV"ZݴQmG|NxQJa6gd3F,֞o)׃ڍ40W U0|Euݎbzr%g  Z)[Ya8uPlVK 3k|fz)o>lO+} I p9qoPRR yɈvA,GHS 0fUF81(6^' [ ˱!FʁSisT _v;ß}IC$YJexTLC`OXcLJZsL),**Ss;ys~GΡ0ԫeK')5K^tMVJ9eouc E8岟BmVھFv >sr@iZ II:.)#h\٣ӉƈqܝF,I͇`RY,x*|4ndL +`J\K] q/9sN}m?R 1p8CnKm~F 8^ƇOQłty+~ u3BIe nFCZZnc@E%j?S)En5P 2km%- _>wa u0cppќL <H^_<džw\ q t)Hx=kjk]^>zK@^+zyYN H8dlڼ}XッfӉYҳ~inIWM6\ValBZ6͡vFQaT@J⧣;.] #֋ʌ#LJ5H?d0 A-Ex\Xqm^G@+)|c󱟮28&-, F@ |0rkVyd^L7͂1yRIKLb'Au2&`ցMݔG%QKN%@pQ„manEkLw$ #\Њq uwfg} y~q?M)16~{~ǎKMPhOsi$!ĝ1Б*Yvݔw?.>яOXS4rdu}xogTܳn[}$qB / g:Wr+k}Gby>\;`]}3GI7-eݱXC-m, 0goUuͅBSo:{BC'47U%%,/-'tpJ64 ~%gh&bҴY k @D]ӻg6v\n}Jgkxq.^b4/bVsq)&Dn`D֭kMūan(ȋFO_;Oߢ<7d8)9uB;iH"ehF&zD4ϥYCXs&lں0pXDHGk[{StHֻhb-tyVZò& uߤͼןݪɨ..>!?O6j1(QR)NPQ^V?\ӯqoQt:f? >P>筤5+!’e]ij;ix~akq4UL5dj(P̻ƸDA,%Yf>@ۚk IR]jXbڬ?pXXkWeI]N|v/SoBMYz?X^=YY,Xr10 Xu:2Zp\3bl! % iW^2=eaiiUk"dz gH!#ĵ\-Qt&Yc=!ywZ}h3dNSW21}=KQ- & il:՛*& ZyRw0Wl_fc|bWXhtF< ADhC(!8ndNmmxbM]pNSbdUe2ӁHu9uJ$)J@X])019N[J9<X!4zHO@-gTN_MNbO 0-0&IIs$f'NٚMͳw_HVGк-WMX u`VDXϸHRg`RHDD5k֋sŠȼXa`5s*$%e͝s6 9˲Zmz.vc/Q(~K%EXp9VQaBظA>3FPP ?+!4?Ә855tm<(S\[HN8pY-"`y%qBAMFNL]]_z eN n"  0j.)i4VL:cXqǤibU6(.ZbU2*D6,uPy$eD/ԅkϒjyh2m?'~ UDjַ,2͚@⏋4צT[6G"Ku$w_8UBk6V V)>K +6Evf?Nj@7|@NJpNc)V*Qw ZU=vo=qhn`JQ/{ŹkVsΦswG$zLKtDž&0!wm,㳤M4ũ5q&L`)̕~WM_sqp^mY*Iuu,6`,2UΜg4!  )Q<1FQJKB@ 4þR1FXL5[tY'fOLvƘwVb!t >^NȒO8 f>u9#6z*@,V΂ds9̪9c:'\[ҜNC>h>s yLkp~DU}4AfakEP:`el_! X89*NeqbXi(=T"дSݷd|+N(@x>Fmk.A'~1NݧfPVE3v{$I(dFwmaG̹dTY9{h>tGЧFi|NMb$蹝_ i`b$M qVgt\;N6$/!nM`ufv_zɈd)7Γv0&Y hH>`[$H]l3Zr0YtQc7_ɤhE%&c iZ.˜8 O%-Z?vȂ dUvs4:{U>h.r'Z#13@pabi<_ongcR Eq@I^E>DF)բ)8G&f;Y: DAA7!+N7ڼ M0[J6eV=NL4ɿkZgԼ\:=Ѧhh5ScaMK)GT&PNϨgYE[FK3ܸyc0b|pp<,FUY]ɋ,n6|WP_|[W|Pys}8BBUMnu'MRW'8aZ@_Qg >M([&4\D4(J KZIu+4Ev6m : Z^:x:G5 |%{(A.2?W>)!j%)wB1KC|}-Gj=V@blhuѢRO$~VYKj%Hu^.,;X?M\dUA x\'];z^2kJE[Uo`ܛmqYQQ Q=Yy8 |_'(eG}VZ qNTHRf'WVCCU.չ5J^.WUΦ^oi?O}#^iK7ޜ+bluϩet:8ъS[7pؼ~#ƮW-WH}\d)`qV!KR褍Z| a$xXEbg8hZY 0XZR)s!"`yj>gٔp({saW/ݸ~[7o׮\y^\!]D"q%`ߧo[_rvv#/(&߬&#4ZBD~Qn=bv<]5I 0A@P$nYsQt͚nYhsm*f+_AfZH0/u ?N˫4vxf|NiٴZk9yZk҉s_%q ʈ8qǝ,ELWZݷ(sK`g׮«O|Q?b3N#jq7a}h-w3{Z R+^ӷLԧ#=UF\|A/~Ek RPKxA (K%4U~/fc@fqTY|PؤTi,!{\,_MsXLSL3&4|ťm;k -&8{,dl5DbR+(w AT'yɋKXXU'#gYdhoqJxQ /ى,p-32|iy 'BheIzs +skK/xO}܈m~[ds#>u :`wVMy xϿssE~Cw?*BD 8;#/F$,Hc9ڹ-jE7C"1WU몾 f#@z\H{)Ŵ$Cdw\ʗ>ܢ;X'1PqڋuiM.,Q r,B6CJsAPp$PdE^&c[M X;)5 oL&?DK[k|IwAʊiw㾇@DمIY;i5"  uH;!dN5.FJ @KbP xuqG<0+Qr&=dgB{VYeQ[rwL]G+qߟ^}Q2;B.h"u8Ir̅}|u[^z^>~; Mt,aQIQ\R'wI=Uw9Y6V±%խ1'w:ߢ2vuw9UQyk2pBkڼEST?w/W㈳ |H+`ãn@8? z w|z+|ө5gϙ^o=M/8 NlM < iD~0NXzsQ7BqsDXh}3V:z'3ԖZvMGj@tQIz-p7h5/6( $ ͚ɄYP$J_:!\ rYzl^w.2V;W%f[BD,#qX0=5,h".$;FlzH, Z !׵PSTbjz]/nUU[Kϗ~<{vC6,u[Z½V|&Bƪ!iŁjQD`Ww]='4A6A(|4tb^ 1;89{=Zga(iA? +AؒDSWw-5U͇#(9K$ק\ _ajt6o=E"xt6M?3[dB5v$=M5K&M K,Ru"$j'^zzRٽH4v γ 3dZ*&G$a֞zNlcUT)͜טS_9A`8:DSO򗾠զ7XҲ.˴bԝwy<ݥeDvϿÇ>L !(BQ{~~GU~/]ʆP4,"(N:Oq~ĚFH/mGIQ޹w޹jȅv[)@?,Tʠ>>:hzzq,=1Mh6tW-}A܀Ѥ΄1„1LU|Xi.@sX8j.;:jbVx|#.P~Ń_Pյ <"u7g'%5)S\e8nH3\kT 4QbKgPJ|~{UI&juE?6鵬WT0.Y)<./i B#Z}V6w1'M|{;NNTѡ[e\+M)\-ղ( ^S*ssͳHb~s@XR/iҨb(64{WFϼy"siZc-uhcmևNssnjxuT/+#_>{I^ՆMGέk-ן/}C"ngw k3|>%m3hkDGeʼnNo93ϽDlVәd>}uqSJn`+cq9]'U8&!3ata+]F֘b5WWB0;b]L50^]5\- !x9u8F7jqy>NWɠ+$.cs<ԋŰ`ha˜H~@D5 QpVJ-3bU@n,?.jXgTd %'FVZ>II$z%v4ycMڭdC괋%xZ]QBkfIbJ `lZ zVZS뢅EgQD*YE?!rQN1+Ȧ3Ӕ zbyu2xO{ pot8l6;'siHr|Q0:Ww-R9*Z 3Ӊ #{ڐVIik-zD;\VN ,N8$ePmVV;|U+M:)iԚW}sO3-#ڜD4VV^#C@/֝Pfel s8ԅyN|zkq ;[yo0 26N'iwib EH<5^[FyQZ15NjĪ e{8i&/_Lrn"1T~ >\i E64W:.z5޽F'ƒA5Sagb=f @WŔ=bjEB=;^rK鹢$IP=Rs8 BGrrs?K_I}Mfe^Blx@KAd68%^oVQzqȄ忦"GyT0E&^v5buYP߬Է2gd:Eǰ؄^V^`N&W螹PWg51‰%$1PqM@x—y͢Jb-D n-*4e 5SMaxk@@*}tig+MpH+R>y~ Fz焝1a3"1+:˨i u!I>ZkqI?NwGP`4Dh^'Se\ m=&7-I=Py8p}d ++_>_t.yoe?^{_UK+k$C9 WjdypU|/VUuGR-wZM= xl&2> Vn5 Z 6^-4+(etɫK%[W_cwo͓lnt⒲6:˗ojqb:;\]]ztHZfW{-\ԼPyCx]ͺ_Z;cj- m 7F{Fc0," o]:8<~eW~c)qcs|$dE"2aS*n[{O,Sb #8KEiدDŽd&LV{K$=|",noBd HEv6@P@I&M=3`"֚ ͢l޾[B{?fŘv9{=.֌ѐ0bE||.Z7>I%USON.Xj2h3KJ"_J0\sy)es %dkJЀnB{a)F#gN[[1;1o6DWRvA<-]~`2$IwpNDB8GM?vXgSϭéZm~/+mNQO>_?{E]>YXN- t#d1(3Tj_dD8 .nT+Cd۹GӂOm?t 'vYCľ /x$NB@*f px!~;jщK^t`l6cxO^~֜ߖfM\YĂ\Q̺jXZe\7PG'ʏFH[HM̿n1F-ПwswӬ2d?I9܁ՓnJӴ^Xc 墭Bf0Yt_(U# L;Ēfq(QuN]DPy.Z+nWYm"E3ٚh yӜ:n6<rfd<q0J@,,|}BDB,&8ޏ,Q7?pZIAs ǟ⫯ᷝQOXB0P:j<#(&xlzHYz~_:{o=N]* Q Z4Ӕjx.2>QKM`It&.?DK%&7wFSR2ZiY):4h}Pw|/{Z;u"|Ls>/2P;? a.k2+ݪ֋$-B~gV{9O_1XԆR8uiCk"DKY ^3bM lԕ >6'Z`Vxہ'5vK̪!$K<.F0Ւg5\5&GeQFGgъgf/2XJN j͑ fJP?qSoZ9 WȼA,w*4d8# =lɮMqŬɼO2h,RH dޙvИi3VFyXߦ겸=,KtKL+> iE8ЪǼ}:x0י@ZLT%hpr*`{}u9uzDmk(Yelz}T>LS9[VVPp|M̔wM*O+uYq(AD]Z{5.#IK dl!%e|ti x'im-xI/|O^u@_~ϾIJ͂aؔK|m.oaqѲGl:m鵠:B*4' yUqmoPn3%h ^;k3w~5 J)o*ؚj铓^tx]ۉn:@b0xH; ~6:s:n͇>CCnJL{TۉQE2L7+Zu"ǔ+ 6O﫶 ekҺ]*nmoZfB"r] |G4&rFEem T׭(pN؈NA΢ -A̺)KSNr$uā=^UgYw|r kQ*gHOb`V1ϊ~W$o$RE5;քow QXZ=MHP)Ue +[Jq%e!2=ϰL'knZSpG*0iT|7`>pɉKkT:=y7| eum~GM !5Y~:v[IϗCvc/WMn_1ҢNUnU@-ї_iVJ's ʜke:Q=VrìP7r;i ?}2pZȗ_ݕ3k=*:TEQUe|i[<?v |;j' f5%2~R6,Q9X|E*+VOK0nep-γm ^][nLtmZ7$IkuT^ UUMFI)XUʲSKwS!Hd!Zւ*I%[.kTeq͜1VZi(he¢K94Ft^*!֭QP?rvwUQ\e׿3PUE뽄v:?]ᮟdVU8?!x`VZ_Z̓='\;M4r\#t !c>q|ZڍxCs~VeWA Qm ogd7/|.S^Ԛhܞ+L(7ִ,OIt o;s &Vo,GڕnH`vcHVwUS-ְ`nS'iڶޡ(KW:cC^sG U+_R 66N0>:ĭťiM5F"Pˡ&c*6{o21ѤM,MTqhI!`b^1g4euPQsFezs(bcZWilB+Vϻ'ro!0SrQ+[B^̂{kPqskH`*W3^[DԢ6Cj`Pj<|/2߿:>8.੕C?>BֻgM;?%&$Y "ʥ dAngcxY3p*;G96W& (gji&e)/Eo*smk8Cer.xnǬz.v-Ѥ|T^R7 |m+{|3~@hNNRUU ?()dP\ZOJO>mX25w8h=45?g>DX qD3]`149Nq'E!Aߺl.q6z&]ƀyj7 #^@ f?Yn`Ǎ.;K\Y^= jF6+~JeuB>*g:]Cm6O垷 _p9d1vCX&ތ:oڴl4aY8܂Mp´fIn}T}T%1;(B+hfyp|VW/oBxqey9q'kk_6Ϝ ^8BU~㞓aga "hIED$YW_+zex>8lȚdZ}.wܹU>?0^| x9H|4o'\ v6mhI5ʞ%$)F;1c-%I"W^ywo!3/ כhDך7gD;)o`zA%ӣ2q\|<敗--DQ3l&ǐ,GHR![Uёa쵪L=}h`XUT/~ŅJ"ϩ|d:ߨ>Rf@^FW+%chh*,I|?<[d/}w'~|\)+?|ϗO~x3g<: _ש)Wf3&9mHHֺH5.QRzNKcz%5w5iYjP‘mG8,  &kcIXe*#DM]&lAej8ρ )h$>34`s Юϛ{P-5U.}fn{x~IM~6I#Bt9+p-p^W81>mO}NɋQNgW c!e%tZm(5iəWș[eݥ^iիjNQY2@&3wY[`:dK:oSl͏g,/S~?85No <3/^}q~u.?۪+"=3jXc2%lg(ʬ$>3(&Ǭ_KKXi*I*#1!=ntդjk ]=oR@>F|(z?SqQk& KcBIM,-xkoL]:z 6nf&sUkvέxv[q$7UVW* ׇw=U vw_F@ I v?5{Ĺ/cr6<|~UTcuahxuw˻UMެ0J|w+pv>S"Dehk_U8-|'"/<Ċv_mYmCuV:Kn:2+ ګIN`Ȳ6g_& {.{ՐxkOW%H rI]*y>RE^U Rյvu1} bרM_^]ZV7ů~hwyғ'99XDX |/ΘG(}53.r.Y\$ #ASK43AA!-Xic10 `rg4kj%ʪSR4ID,O{Av_//QhSw4NJLZMNMcAIUqMzG\*_er)­kמ-IS`?~=rjp_ _ծ\ OʡD# 2Vl$*VD@@7vZozjeѨ/N 5ݧb`V͊JawB_\mN౻|K+l|K ^|q?8/_&oM۰֟[Z]tD52Mn_Ysuɧc᡾̗~<}y/ճϱʹ3a4 yڳbtt8&, nGU.D]?e49h NܡzID(@哩ebE-'1SU%:i,(#I+eim]gJd.c压}cwkNt4F3pwZDܓӂ)$k*و\fHjx'Y'1Ŏ"etiY#>Zcv-]gn;SVE`Ɍ5Klٵ3xmk6ضm۶o٭6n0sOEFFMEŭVDZ /?uysV._gӿ+︯sCgN:C,Eײew]j|~:vOlcǟγkS`. "Vx?%8T.lD34aB/B B+j(n6 gIJ' 9M 'l$Y;i_ 8Mgj>S vb! ϭE{/4#EDݑm 363*G a~okN焧0R N@*Ra,,04$nAy'nEGަ@ecG4MՊضjQ)%VWV҃-G+reh^C& W`G{.h= A&c;~M߲$?k  lW ?Chv~_T*<$+%52 ol+: 62w"ә5Jw qvuc|xFG_4&3lPeBwp7bbxFx||ċpnΞ q11VbO aZ//l/.ҞchD9ÝǮz5f߀n s.n&[CJ"\$J-Cm5̯ŬM7|- w4rЕ^3@7AmwQa0>;G-Q,adc$ah7 J*#BxA5$2mlj=:_ɵ6ژ՚/e8q  `TklO0󎑭[BTX;Zy?6P cb!MXl6°+[ش\6Bj bKVxf+fQZ}U@ #6͗FF,Q\Xm@)Vn/atjk Q^eUw$Q]+:qwq~C}DEC. sQ ,9.Q) EYn+(: Ms{#xiXF2hcɒTXXCed_٭ |ã#L+Uϓ4KTB(F!B* Y74BTQgMw,E*Ag'2t ҚΎ}ʴ:sII`Nھ.­_U_`Ӛ®,V|Mӛi пا~G0jҡ$I8Cγ(n7qa(__d XATjqmKSK>TC b*#E>@ S1x|`n)6-üur kƩFçmYNl 1vũ.I@DmE;0=TKfP Nc@dOksb>w Q;]+f~\#tP7^{N<Rhɜߚ1&u @D Hi%?,I3kKcy؈R^*ժNضKIuҋ~T&0lDXH}R$GQdZQCdI*HFK)l]Ɔdc~^|$Zàpmry\5kڠX,"2DqZl@*U&8Dyӱz :wo6;5xPlCݪQъÆ.Md]n65#d 6Jf+!tȥު4󣿊/I^f6Lg.C>iu;zjN&q/4,F'BXx'cOLb0xޝ~YT \x|9ï`ic0vKL~WGOO#_aiK|#;L  A1YNVK$KA(~Au\o]3@J$3a+&3+yt#Eˡgex3jV :;LwaW>_J}!n KU<t*C<լol+7:1QKD iߦ$St.1åR%$ P'蓹1%%e*!*ŽLKLwKr+kՠP4HDsY"cx` "PEW|`"&9rH(HF #ABl q=lĵEȓZH|)(־,ET 'c(* f]a4$Mb!JXE!$E"j+ 6hEMpߗ}xQmoX5:VOk3M10Vxjc:>ZA_Pk}k,Mq3t#_hb00k-S!*;܁9* EJ8_ǟ_7@ n|Fכ/q%p38s@W*B[ܓ!YJ[b\]j}vji;lLsc,6dfwFװV؀'>jȶ7qފˠ6~&jN`)FǕzPoB)$a!c6$lJͺ*%-w|o&,iF1HTzO)$}6 CHi%}.9A&=KBFHpy,KlPrݖFcm9 aJHϷQǩ>v5KJ)p@sj*5nd§>Oxn "^=7inۑ )m ^{˓p%N_!S`Z^`X-tiIá?=O{93umh_^7$`m.sΣo˵PM$p\K:ƨJ5UтU@IDNʑeBL xF֔iZF"T=lm(JUn%"ft;}[9A]v#Q{RLLܒ4ZU4",DI$\؍u!m-FyMåib*""Ib CS%gMBҖ =>nXG$4d D76%QgT*[߲d{F2 U1\B?M,--bi$ؖx;c3Sn w|[7֟Lo=7sޢ<ج~2h=0{#'64$~TObOVTTю/K渱V5:KX3ցuMqfx?db&:Җ.;/^_,413$b# .6|Y@`sphk2e6#)ah3W^3[PNSOYy~i?G#yc0S1<(oEo.0B 2Q9<ə5vS Ka0?!Dc71S?DTp3&Yf4s'4$!>a_IRnq΍p!R٬ M>o&3\G:)=7u>0'zRg1YJL$Fh;mCtX(S\S%CI$LB)%II ̬HP%}_KeSo$'e~qwlt\0$iyG:5)/lʳ,f)9XjE>=< LR^u-p\T[3cӵv]bvmٕJ&ymci#wev4 2Wc-w8:st0诹rZq6 ZKࣇ!|V[Fgd'waVO HGSƤ`O8c //|+0 趣jqT~Q_.i2N9W>޷2H0n7k,\0`ԑΝNm'qG@r3\Tޭp=9"Vʇ{}A?WcsS,EjFͰa4B^vUʕ @Aas].Qn[ ;y G>'Z{O(Z5kԃÜ%KBAgCkxӾ{jM'ne 6\0 s$x/Hҭjш޺&QLV `=j2>C^yl~0=Ej6(*j ];eZT`Ax Ik~8B NY'CHkM!`k8[/~7y'F^2'v7Ƕ')=,^g^H8^c!?&)m!L K JHXH=2(,Eo&%_GqfbvP(^p!J7 Eni[\nk=OF\ZZqZytj<70PvPD p7}\xg X0BxW>FGjP8໼rϾ paۿQ GI4AX( 28j5[ؤ+i]`Vzՠ/ /EۉF}zSk&K~(N>8NuRq+iT]r$Ϻ/ ^^014B (`ߧ?z kL)? /)8N<8,w$yȶ,Rմf|J} P"ĮP(1BkO^}B9 茆F;W&Q%,yVK*(DDkI¶s{YIPX}~?-ȬL '\pS-8s&qj@/ahJ'Ag?a )v{\Ĵ-:l`塷*ѵSB"RP*9;X!@ nCy|ʐ]LF'g0>ZI °^k"6pOhYDte_SYH3$F3VIA*TGGxn߳g>Wx )0#r8wLfV\U$3,>:{z191k}?/p"R*y\?.ŦvFq s#*:ʞݶ w& -J[  ^BǑE-F~԰^灴ATolZ]I/02={VG oDVKﳎ(+z_} ]2b2 ?q&FJJ.ιI yEfJ?~W>?1$o{B^ٽDtyJ\*i_}_ӓ>bt8i7q-U,R^WJj)KӜ %R뤧'L:!!\QD8 0bc`$g@;QR@TRX6"<凡QߐưjzbFA6br/,UIadSss*|Y<~|x#G1>: ~ <ƌt u&=rX-Mx+q s- `@8 0 }7yYi}Bqtvvd}a1P%F ^X,$QĕᑒNLhm 0B0B`‘@#yL$1> & 07?RQ^.3 Gʩ+8q` ` *Gګ^k^_rϖtt>^^[ϝf ՘IENDB`PrismLauncher-11.0.3/launcher/resources/backgrounds/kitteh.png0000644000175100017510000016300515224505336024103 0ustar runnerrunnerPNG  IHDR2IDATx#Hgfl۶m۶mgl۶m۶m+{:ULb!{dL eBQ9cQd~ :u1_'O5I$_늼P1#"""2XgJ4hஇǓ~=W\ $?Y#""444Vz7mT7tP9s<\z_^_vk2d?}7qWqƺt<{L^PV̐~%9""(rZZ:t%KȣGu'N,0[}6aW6li.]ի>-,I=f$CDDD_U@0msrr+WӧO};&:u_)DDQa4-/V9pڵk!jIo߾=;Q,DDDEP49zgggի?8 R)ɓ'>ey@)Jh$<ܸqC>ĉ#YdYgaad|]$F=&@eÆ 5S8p4jI„ g7!"" 1Q8Zg̘[nھ}888Hҥk4 EBh)ڛ(Qk*Tټyqy?~,xG͆%O|fCM> BsN{{S哪UJPP\xQ3ԪU+)TОQsLQ~4mERHcK͚5oO}7oWŋ7!VvvvӦM+YfUi|r1vX3?7 """"{MAG̞";cryZN؍&DDD`*^h!\%A(G`\:k UTQ{M~!/hy1cZ]̚5KըQԒ> ׵(x.N=N,DMRpauAJ dx6N܅2 ""G٠h,Z{G=hP5!-DDDI tQuBi oH}|o77'k꧖?D%x`Kɒ%u^^^jYfAbŊ+ݵkoQǴ8 #DDD+E5; ׯ{g%Ea0)[;l0 PE0 Υ{yd缳IJw 6"VZ͜9uiiw!If!!qdszR_fE]A~ݫW/c i֬1?DŽ~GL8rLGv^z r=*~Uܞ%9o{x4h`Ç7c۶mh"c(o뮻 #JrJ>F<֢E ~$ F۠LltlEY @6nARlc5È# 7oloG_`!Pڶmk~A@nvS~>?_n&<]Gy=4m?EqxŊfsOۙ3gNcZjkO4թSJ*egulgp<߿ #2>3\}pѣyTIW9r@D3߻BAE@@@@@(w.֛m/>XbȀb>O?ٯx`^o>s_D-11PDhٲeƲ^u{m˖-?@t #tko̕+\h8K'83w5we5p褓NZ삲f9H"Y Âr"]#צEF79| (J8ūC@@@@ 2; y~(QAoXkf+`vb#UVTRʗ/o8_sCB~}YBBeRk !]xᅆKqי1mmGnsEL0^~7\$Gc5H2:CMj/UP͡OQSQz=Bܣrd?NQ߆{KI )ھl9JESb1%\*EC9PaЋNqQݼdӟP?a`Ԥ\EV_Y^H[JvDyK)נAzsMl (I%nD @)n֬YM|Ĵi&={6C7>je >2z:q.*wX9ʩ`*s#,,kd\(ZQG1G$~ve= ;pe9q7'ezH; cǎ2Z9M!!\!!^z}A/|fX)6mt9nz:0Dh@wi|An JBHdRxet( %:K}> A ? bKT ]JpSB1G OƍAD89 =AҀ%)%"2=zP>RHg+*NUU6P7 U p BB@َ<MeXVdPl.ŏѷ5j$7RJm۝V)&e銀2{}vK dAD̯%_ΰRʩYއ)6h86! pG1UIϷ92D dB1;|Yw@ 0?bz.P1~t~@@@@\eC]nJ.s?F. Qbx}?FA6eeۼv[W%ABS@@@@,k^>1M ( AfRakTX!x3Q$nYo!j"5!ԔwܺPef堒^zK򘁂xPC.4=/\0لץUb lyOgD'ZgXeɬ=(7vp@%;\B< W mڴt@==2[xY+Db?H,SNּUs+y%;VBvYH ϵ.8ּ CZnҥd||Hdr'():BBD˖-@(m7"#L_ז/_n~Joo6dDbB>ίّD%Y=/V XzAOڳ߼lg/[/oM7܁"ԈXN6Dt𹡫< v}H>-j.9Yq>wv>7)>t`R/7zH)u&  ē,jd7o^YRq]ՕٳwGct7o :b㾴O&t߱l,MRd-/'.(pH(E_x‰y7~8أ:joN)Z***)ҁ5Ve픱詐M0t';o᠍&HcmlvNj-_/ln+ bx: wts(X`H 8_|̝8[ҞKk'%iU=r-oŕ+W^UBRj{.i}}3X+ʥ) fuq~[N?+nc2!ϵkZv{{j]WȂ Ѣ}k+];bv^!;#>r g[)'"5d)Zn~fp~r"y?O'!CG&N޿z6l'&i P;_TdgF~oxq3CfX@d||KC5vM9ZMeWVeKYJq fE(1/"Ez=Ţ";"<o뽓j֘@xIU̳THvꥸ?n2G|N(J0{l JvfΈ$Sby+~~ ;3)'yEX*UUVYQLm8 ݗ@w)ɝ;wwe'E>HqkTnWwmZcu '~1ߕ.Irat뭷F.䒗R.㜄6;M{Wһ<N{m=fY ۯ6ؾ!tbwçe6 3JekU2n| Bn?Crw>AKJF8=TůsIjgg@Id̔U!"+5Mƻ=l,^ }䖏y(Y2Yħ,lTs k@@@2U1IhbbJdk]KOpZry+S2c0_6B'2fs^M:{U]TR+_ a-[be<唞Th"YNDz&gw8ͿdCd@CF v+haE$"*~QSy-`Jd,^}gY2h J}y.vwK7K9mR̓w*s% 5߶Fؐh# " ܠ!~[{w Kj5juq=f{ꩧ 9>EM4٦?zm!a۳@Ê.3bĈtbMuٹNe8`rxIL>63D<wj8TzS\.Z_d9H3ۘ4i խo!( 9HDSiï VooM0 <Ma xqkғO>xjH<wHN5d ,E=j#l{!a3|dĒr5_!9f=IZMqɐ+^'y3DbHh5$$Ut@w™ JDp|ξ\&xV+LCɒ !Tt(C6U^nAjJp(ycX=8*Kx.;7`ԇ%ѝ;u+[J(Jy3|d֜9s31yd! ;UTE@@rx`PsifӸt}c]+U| oY \!H2(AV9+wEH!e۟/҉~_<8Kyr|ȑc_6&P6BƦyɀTZ* }qFIѺ%cGK|ʂTfݫ,i֬Yt̜94WٷfZ^UI%`Uln<y?ڳڡ7p#?dP2>xhbPMy=Enѯ~OVU.2%z3/_Z_PI` E,@DG*P y `_ЊOA ־RG%o dKi۰ W.@x_5Uzi&l(ٓ:<(9P@@.] 4űOCj4jܫi{PvYK*^[ŐjwIqV:7U93e>G>dYeʉN!s#Lc"!cuv6ײ|1 FwT%_AJn+W"fʟ=WEIȞTZ,%Mdߨf2\riKQ۶|˖dֱ rmfѮxuCE>d[ʛgvu5%X0dž\[9sU"2;rW-QfE]dӧOϐR/~^cgEN/)7ӯ9P ˬF=Kzx.b[2Z.NWS n (f, j+"1Q%.Lp~# e޳5yQ\J^Pʊ]PVLQz\C#SV/]8|a(aذa(p#籒AHcI27q>'ˇ(hGƥ#fjiR KސRJcɨvoi7=r,$XW_i{Ԉw8 1 JKBs%kCfp4dY^{QvtܖrPRKL_i H*q |9ܴM Ɵ,XpáJOh S*ĩ7g n[x74GUY88Gʄ]Db#oH_hHhI\yE7oVG<.]z: 6L緢'lD|/p+b^\TpZwa4Qr#l2nސr !hv/|DGUG935 qǛ.v)lrq8Xy>Y'Ig5dDڐ3dT9/lǐwޕhUWY3e͔YSWc%JJEPPϏTDh܉9 ~ngi[3ΰ:/ [׶VyrQvTN}z3/P1ȡqŮM5e0f}|!z׌]?2 `G3C:t@)G^ DA ,,/7fub>(GYЛ4m.ЊHFy'+eF` A@-<+V;x>DЂ 5Q5R q[,:Cʡ`9-.l(ܸ#mSn*1U: Qb)2ld:QFȂ[<eBzUy3 'C[c!Fy%2/udCIdz\q1Rys2]2(B@7*#稼.0eEB%>ۊ+)=_o8JsN焟g(y\v. [*9F4ET֩EH%d /TY>!̥j>F͖nZ%Gg'T\=#z-;!\ D!FMV1IR'3ܞ_+*zY`X=*!NV]F ( E673+u!ʛ5< ȼ~}ȅcYg`H& +>Ug$@FB4~Y=ndֿ3YA}N1&`bDޟ #(a7_#-Gc*J0Y|q}Ѳ6ݎЬ>z㒑@O%9+7PY]~*.m5CrLM\c/LE5/CDR3T#H|]$ffh#;7.їOC@yF? <4ǎKy2pBĵR5adJ- $V=7!J~ ~yl5e37hkܦ͛}q;r(7Eh^Q1DtAL}J(vsA k&.\j.U yZCy;ASzd|DBi:*S.cD)!uJ;*KٕRYϤXCBA@28ᾪM% 슛,tnɴ(Al{>lb0KL` 2ÍNG31=~`70!Wh+,Bfl B|rF 7Mk/%!!&?$D B}פt1Q(@KDcEM  ̵0C4E1TL&j(:n_3 hqK\x"6t$G O҇ g[p0/Mk ~ =J|MB8!Ȇ  lOzo4,=D3$|/@aCF$SyCKGBca;JDz ~9Sd@R /5&< ҥK믞H'a7]5R%B _I(szY{\xCQTQlv?2#;8)4h(' P@Jfl &፶xLR$Ո <~JktH ǖi( {dxL}1$B@<Y:AáG !)tz`THCI ^i=.}(T%{^} H`Ag.-<^>LyOy{lC%){Kۣ(BƁڋb2 35.0E \W#r !! \4〨hp,JH /+@Ł_^+O]zBqrS\/ ЕE@W&a9޶ֲ灋!;ޜ'*C!ݱdFڀ[|ꪽ -»1< *4m9)aË*f>W]fsQ㇞xdJ)e<2 !d(ܿL9Jszox)"%ȩJaf ^aP8NR%'6My?;ee j@@X^1 HˉhZ @"`Lg_? 2H^ dL{_l>9Vd)&"ʄ9(!R"RI7W $vXV}L +x*qrN3m%i̺B (xG15{r'좇Aً5~iB^dM.dS4%hA]@95j.#.WĭddZ!B zJ N+V@N ur x#>#Cd;ybY5񂫞}֡ep_Hئ@$һu_ d; A(̩@.bxG ǁYѸ` 7%&ߤ'[Q録f %>sh25RE(kBK(-Yԍ<&2ow7'7JqA,2(Rz\tND?E)2a6(r{ nA@Vτv*v~nx(AZ )2S<@[! JDd]ܨ3H@l&'$fTq_z:[o7\j^G$昺RM2}>}pq4F10L>~.E|mH(>MŒ}Q@@@yĝߗPQ,r[A M168loTeNg@0@FC--8ĂJZ0$X 89*G#(gn)ܖuշ^(ƍOGJ=I=՟^+2ȩ}!2!JsiS|׭[Pkei@@{'Պu_{]A3"(YLt7pp}`@Nd?42 b%*ܪ qE̞V90QO8gJb-({r1P vώ RtgZ ?(H/epԩS6ȬyUJ{¢!=:E881%N>KyI7SZzZc8Sʡ70ܪ)mC?pd&cv(D7҇!#%C7 NeGMlI'dbj2caQ14e]@@@@&ں+M#A.t1r튉[H4OUd{P@.h25Oƞ %:;%9.\Rw5cuX.ҲJ#e8'-ʂi dF`ShkG<+鶼:=sB5gb ã "N + x7 5#… CB:uaOVܹs)!K*O =뽡1V>k]Af+@8V|@)'<)A>"Ȋ>R@\ɵYG9i|SֻOecm dI*S>IC'NB[WC?37j(2XRP6qEW( Edg^j^r)}!~Mulw4s@qk 5]E bXCB|\IYstdQ%1r뗐d@kF51d̦ܬ/[ꪈx&H^8]VB)y$#+G(׆5`~\P#k}wy3D.vh 8Y\xenj{˂ (^g"6U\(%a$f'MNHvҀr)(Fi/SmA@N򀄞 G<ڲ d3*@IqVU038B]l0Vǹ/JUȷ NAY `(|"ˡd Aj0f,/PBW(y7's[<[jbgD[bI%Î&SKߋQ@(Ƙ ' ʨQnR]Yd+\"" p}(!+:|[NV+br&!BbqCVcӻo3\,,D^U(mUxޓ$ Np9B/콣trl]vaBN|-5 k׮~i! ^/ȉ==Yx2FqxYG*n1|SRŐ6֪ lğ{kAD !Xs{emڴI{ޏ{E.QܥxBޥy.hb՜p3[.dA@B p(XE#L&A7Y3 mɧ4'*9ZY6)].D) !2"(Y,&x.$Y9(svWr>JtdE xQX@ b$R_MF-17U1gcaÆ)2ZYP@@s"~& !U^2ϦB:Hͤ*@>9Y{ߎ}:le%K,( կ)>=Zn}m9y1$z"s(˲m߾}u!1O> S۩ئ7o޼v@2OɊ(a͓U_=="cHh>B)wXs?\ȂeOQ+q)K1f#%zCFt\QS۶mm۶mFj۶*uߛgAU۷8Đz;5:hD \ P:|tjZh}qFKA.q(M;o3ҡCGR MMѧσt!>}aVZ(!:~)JpZ:h<g[b1gqN,|PP'rRd!{%XoHtx 3U:txaD9S: ~B)zƃ:6ߵPÉ9Z6C.pmxR9hJuDpeb>! 2|ri\ w7ua+UZ]U{rcdB:" aT,.Z= 3R7y2}NFoQ |2 YX눏j-a"B;l< aO[SAWX#8h !0  wRҡxh䎲ϛ($TMR|પ<5~ZtJM]_ UuVuFg #l&$  "cmع+ RQU_8ʒ%qCdoP'qk(625;Ψ%3 ,{t i4G`_+z TȑU(Q<3́ -\J+J?oǖN:%JS=bǎ-e8Q"P _MuN9n>iSx mױw2s@8͞n(!C0a¨"#~B=fQ @E:<x !Rҡe8)WŌ2º1( JQjc^*@{-<28r2+"cq\3*f̘=*I$*UTWGRKV1+ QU?ָW^rvB B7qiWfrUJruuRի [|_ÇϯT& <0=4 tޔ]$eXPq%oyC_Y%,,&3OFZ5 AnWѣRl(Ym\vlxsWdB,cd=βSؾ}ҡ7q1 [*UʨhXǏ3*ț(< |{d>Z-7L.;T)7T?yiL8"007yY*/HB 7?)iC\rFs޽A˗ʫ!L?4P pz eP'ʚQjJ5nغe˖J/(` ; wl $@ek2!h|uǿvVO-@:`89jx$y|…/{9V8gFDžlĉ1ĥʔn;:*Ν"DO׮UlQ;njQeAk׮ɓٗMR֣H <7s|yp֐O嶇cp9xxA$E=j̙H"*Yd כqx BL> U|JuPiM *#G,8^owI r#7Jgb*Xp%I mH__/"ducbɈ6 *WXQ[7"T> 4i a- rR85+pr#y 5%8iw7d(_V$tдi 3 OkG`1D=?ZiP5D9Z x=_a.ד|+.]$_ }؊P:DhDӦVؾ]purRvUFqK.Uil0a*\*P3{#];O W8,J 68NeL )ד 1`ܐ9_wV:V(q]H.PH[!GQbc;uƽ5RVJ4j!qtaMϴiӊ[Dq༒W 8 Pņ{pg2L!\QƣHΑ~#ӧeSm ^Pvp3͔7&0|r$' _#.N8]S:Wjϱkj*m*@J*`*`*`&*@2OKh4bJ;PJ& \HZbф* # zkUW4>_@\ pIQĴЬY3qDsʝ'cFaa@WdQCEiJlvj;nֆ Mniˠd;{<1~̞- )\oԎCWԌkZh4E+lr`oRRXo(EcKE>!ug@1ӲEOX}@F$"T\K.xY!g<̝;[_4A'N ry'pu9 $<%"SRuѣG;qgW3S3F{>r ˍZJU[ʼn:XmGat>@bH؃I'd @ wwngj~Ul&)>P1!@UEqHBd쨂g訂e蠂m)AAHZL“zp* ߄zj_uѐjL~]Jv:Dăp{5ː IW52FTiNEDȍp1ާT$e"-6茹w_ [8b;y gHV1kqٖLQrgWuAps0g\%8 {+u*c4aQ4Qs)Gw1G7>ƾ mYD 1LD>, WA#ojச8)o:c~k2 :/gRf)%MQqV$" -ZݸqK;g*~]i-C? a+WTiJVrAʆdLLk ژ!G `p2;NFkPl:|tyN_/:X-k4Yh|UDx "dfC c2S'~Swp ɂf:[1UJq[Ls%N0+n,YVpJԥGiҤJ%D#D!Os Dx<A2P%>6ݶ515!{Lq*Bdk B$ C"g̐!67 <0JAvi`"CNh0q?FJh,ْ df2vيcLd5 >|X988HOd#3lo181C9Hᾒ!@ Vy3">dZ9'e%7Lx)p\GF;<ff";lQaV̌-fNהsWS+߽0}NAi #՚N(FJIc ~Ř!bDe BҘ5a:^"JB'_ )2I`B:@ !=e9~r:>s9ȳ|jQG$^0mb"gd^Fk kҹ . O?nՐ}*h:VA=}6tTɜm/h|#%!:&(SD8ǵ ;뎹9'9ͭ)Ֆ3:~f6?ʏB|ޤ񃞷CAbl#B@^ƞqYBr`VWާl+*\H%鷐kڤ?rh:tFV!.gj5AU0E wᬛwK:  _5@[{*X AF,ܗyuBHDXi5 g+dz"-7;H8TD̙*t|/,(r?Y)At  dȅ瘊$+n9P OxsJ TnM.K-WT,!ia:Ihq*YsF䮰z6(Hŏor[RSs¤_|cb8s`QT !پ0,L# l$ļy?y"/F/Z }'*ZLR iƔ꾱H g)9*/!0^ML* a9̍(*ԗVNrVk C1Hy򾓏 g9* iq /Ull- W O )BH!σL{m4ZX]8XZTdzn4 w߅{,<tL{XhգoX@dŽ@Q`@`qDi(xƽ8 K8*,( H pD!Hy:,s2l|-6BmnAh7$tB@B|&{VLBK~B{IpR]zj7lҼh@ޚ<[8T'>!$3AcCT?"!i@2%rqs8lvR)qX0 : :v>o6̢0=@gvrke+ 7 UүDnN>ǝBi1宻: D/87V*ZkUuQ S6kkqiv-"uCBa eg5ýM-BCоV b>g1C1H2\tMٸ+FDL! }q@;s -?ֶq ȩ5VޚÇOֲݧOpmCc3TQmwj=v\6K.&K ͜i4"}?C;!H7g>1p,&$E#y5i ˡ <8 F7JDX<=(8ȵ!q-Ĺ$cXWUs."mSbSBB2 !(Qt(!ڤ F;k rx"h'ȿ:,ķzk8E<"QKk U e9^d ˵q,5 9*b`gFpu6fq;[-nFˉr`8lHK=%, YBR^طwSK~Ȉhn }ܮ!8?P'nvHUό FY["aÆt {^pƩ N,k?4/F)%YHcP@N]cSn{pS:x?N>7dUs n䄥"D^s5= >5 a Z^T)utԘz4Z Yxc4 jUPsW\ D:qzAB % Qm{'U(** iRGccubR,,ص!Ba(vY@"|hEHpGyy jT $F/msfglg{0!"ndס' H<x; C>s_,515V5>~7"TPEXJ5~ [õ*?uRF-uw:͂KHa,lXq"aL xǸ'd8eB(.#!^Gʆڲ<,a` EtQy Br|oʃH/cUZhMf3:\(j|QN p[7 [ _[N:n= +LxG=Z aiYב n/QT{V uTս{H#:p7 YbP+)^=;I*Cx Cr( 07s|r 9 l<\=ghxA|.Ѝ`7W";:|GaaSQ2hWXT`]? %"UHHn4fjඌZavJK-P@yf!4Zppw nwɻcwtC b-B,q,_jd-۪% gZh`aA\lZIx]8?A@4~0D H.k\顾Q+EwM:8Th'RkZOҝF7,!~LQ5D~[no{)7t{d~宻B٠AS ؊АF/ *s-,Ҩ~CN]?-|X+<7M + M[d$5m8r:)ͿY!LrWCYq" yN̉g@1aE}R9q&t>7@l6@Mk;$ƊBGma)a۷ꡇ*=zt6m,ԝ$♒M92P / (JklUW]5SN_M0)y@f< qᘃn|# شY y&dpEB PF,ؙN9!o8\an9PbEރ2 E>/G@xwmТY~3ew6v{ $*mi) ]!!  Q|?-J9݅Blܿk񕕕aJ[h~ 뭷^Xb%-^aamhE.j1CsCEEEH#9/ )$Br=2XPY4ð`P.|ezX 7{ $9P:u³A/+k ԗeGRkONADq?$PC^SERfFB^݁usLOݻNC>JYh#c}&#~WP%8$@x;vH86:tX21J 9SZ^8PhF=Ûo9Ӛꥑ~+,Y!+wqS!&@P .weΉ;r drχ y>t#]?'̖R<jlop A__E K?/CfP'צּknxA(0劺-JcI$=P/#Bwix"JCmmr5nvR0ec4Jf*v,b g ]GϪk(Dڌ/Ԓۇ`Q,پ"!'D9 2!YUFAAv0 Cwo6Wlz3,|(Wj4s` =GL܃ l7 g=.!7fL\cߛPf2c(4 D`$[ ^&x)Q}*# rUeD{Bv^{6ۄ:+].4EEVd^WHڱ(r25)|Ȋ,6".!"la_^yQi\RNaw(..i15cBi9rOb/'E1IL?<7zp&Z /1W+&J3nN82㳢Jlt0f|9O;7SĦ惺$Bѭ絡BB䝛w'Z;5 hcNaS=**hSq#B^#myllف[TL8.o uV^yu]7`>蠃B.]y!)IRl 4e0 $kgfC-%JܕٵYx =ȁm!9ǹNr<Ԡ !s#nw) ÜODMH`AiTrB2Bx\zj ~ANhg}vݻw2/9&U0H~FE_L3GIB W.6BK886vƗTG_#y)kA^̋9fT DT*9J#9v2fmƉ RG+}M7'4 NnYl}Ưyį{J,ԌTE #7D)A3ȭZvW* 4|&_nQOqҪ*"Dqq7Ң-0)@qy]5EDT/2+J# rJ'wWFb6`EToHpW L[r0Ă b$2T硸x))1/>SYpaXmD1PGxN_BfS*]QIa4Ҙ CC5rѕjW\q?jM޽{wwF&T ,h6.9W#(!(ۄSLNج!5JCN#ѝmH)0ƄG/& R_gcǰa壅4ҘDzf UڿDHMkV+!4 lD UCŔ/'bMCn3  5H5R\h= /q0O ȱQG ^gPm։hՏ7qO!4Zަ-u DHP22B;Sܹw@NQ^.K)5Qwn/-[n7s$$}tAMy)`<4Vp1H@Đ%!+GM`"|-?AcJM]"psV@xDS[yD]&D."./=>M)o˃60Էnݺckl3i\(y7jLb@ܩ)C:̨p?D" YI!c0IHKQ_J5-I{}~PsowQY5"dFۇ(bTժU۷/QGncquۤ$#>i&udFhf*`$¿Sm@P&kFxt"('N&F-ϔe טJ^ys4Z':Zuu@Ν#'wAщY'$b%d-bL gRi I'= !b]9Nejo M&XB,yZ3\7N)Pui5iB@C8pru]]ыbSR\V3ǭ?YtDhy2vX9V]#:.jbLj VJpmiCO){( FYHbkoQQQdfM&f⟨s,suHa$u3AVVooj7M4RLY$Rz!aτMX~rYJy膚waL9]Et,ExAq]ZD:M[[=3s&mFMyY +I4H_")f"'Z#knbƅj|})|d/~+ÕQAhAh϶ D$vE BDJ*6k`NtmuPP^iӦvZwkÛ|f(,+{SN9b66 *Aw7HɁ=]q<_hx>҄y]D6f+ؔDHi% :XUXlʗl Gֶt|v!U&;_g}.{pqs2S1"€sIv3:)={  ֪̊'llukM]"0c aeK˖-B/Z=<6U^9Nz峕۶m;J*7%=[ Fx?Y*B?OjA+i4`Q-ԧ8 ewd)6's I"1B=X:ĄZ964{xdFٽ*G֘IjEEthUW]5n5dg x% #5*@gԉ ߢX%ڨVK6ۓdm%^wV\ pRA.,)M^e:LʔmGcug3k믿_UU%M?Cu4)8j0'ԑ7@}Ƣ6Y$MFDl2A+wFRK;P,/؞/nV[NDDE.n{.6ZPݨaF2ɥ]Τ@ą ڲ9cc46śS<뒷]Z7BCHlHY\RС"Kh^9WHzx3.!h(brNw_r%eӦM&  EmjtbADBz ֺ";=.!T<^Vl LFQ^ZOAAɵmV/8^t%[n|0 bܔϻX0Gz q팧#"-6W/$v`5bC)O8\#J`jQMJzZaF)D"t ÇW"B۸|mn{xlbڰ0ɲjŪ-BT"e6;L:u$Ǐ::7DLR_4&!G@(d)MщR&߿u:~=uzlVu5 # `%M!g0~em%VXuGHyJ GE|a}ANJ;|Am}<Y7 &FʚTȆ`뵖kݺ^ 1;f}GUȹXvpKk:l"BcYEqYګi~ 2c>{4~&q-$ O8Lʓ?( eqD% sstYt54xs\A4Qh`{E/@<Ӳq !qBA5k(CThkweoֆ9 +.K%*< QG!Dd50Nm{xxSrZHtJ Sj]{wZМ Mqȶ aEV>tVႩF n%oe|_8Q`:@r[+ p"DhTNAﱗr?G&*ǙlescH'Uhb]3Q٭[fX?$ӵj6#peeeC9B$A8~0i%9*H(*B|cݲfH '&&LAmL~{N ٖח̹{EE;= M愊v D;8HtTS%ipD&Bj+b~>!"i?eR. Z,Ew/F! Tj{+=<HQ;QJTXF*gll]u!YB\AC=9 a+h\nqTReoJͿ(cAZygDMCӈl[RãʝWYf09 (bt-HAA: KJ9N8!&H Qr9FVW|x6Ɣn2Oy}nWPg!dByy{ԄsO0J ho:7狼%X |I{󃯽N>Y:);Rq\U RoaFL҅\olݘrUh\VkmQks҅ # AxW=<|h%?#B."K[ ' og9O{Q qDI"H C`ҕu@_ 5|tbJ+5ADt4%0#݉EA$-Q2j^wBĉ%*tUsO>mmX)K0L>KKK%G6OV{svMHF…{.G2NY4D$.PQBD:$ܡkr1\#drpxW_.[X( ƶjՊ kJ$zGX5) [*rD?|N7,@ʉmǍ.N99扺˸/QDC <6C|tX2TĆ‚"ڰ.K<|]hʼPÌcA]AE8bHWRpe_E!=Gt;9>Ǡ4#6i8)$DΧ><s9/LGx!Le v r}w`2V>HSLSaVT/m۶4(p҅_2I1W %%!sz `"}96rD>DDDJ'i :Q#)>OYnKƽ`4D}YQSN9E0e> \=M1Sx89 pLCiԄ Sr5v ;YIs5qεF*R|sFMhv:IQn *SWYZY"]tQ^x!>DBbu..9 }k}SO=U}i& rE!Y&A\!@nn:ρ0ӹT|WI-a$*R:tZXm<6+|P>}dΜ9Ү];m!Br-'L$L%WQ ϸV!c;Bh~@ԈJxp乢 ^Qn"=| ӳf͒P,BGo;ǺSzxxD͔T NAtWʼyƍD Z$=o>`tnZ`'=ܬf;Q7 UhJt5J:ť 'Bvr?HT޽p?DG8R9Z/;V[!B,smE,Nt SNruJ{K>E1TClڶ{I14@u}&7?<_rjwx=Y̱N[4 ;s)7pDyFh9mӱGR9YkVjBguή￟.rnVYN,m2v%qΡk6$jQVyԲV>Q91ZXJdD-#EG.mz F/{&MW^}!4Ljzq AJ&*K8 )..Һxbyg ׇ17CVq"HqD얎 +=<<"ªv,1c-`$= g+H]uUD<&<.2v@7tp戩`C,@i7~0,B6Op3!!uis4[€8A"5*QNԳ"uѮ!:ÇT1~x9X jC<m;L9H<ҋʨI=o xNѓg!6R l.2~BNtg` -XW5gPGXRS ROUtY1X1-3fݜ!lv*"vy'] Џ>h\ԅ "od0GEEiXNZEC3(Kx$u\s<z'9%;+3:شJ"]`\PǥЌ8mVXzH5+;)1z;FoDEpV:B1:Y1P". _}'wN3ΐ=zX#$ PrX7۳1K~N ð݁4Hccaww*"&(vw`Xtw.]{f9sl>3ݻkB>j:$bZbC4Gh:FЍ9SK)Kɑn: eB ?#Zׯ2N%.|бڞ[e6 !L(h(h۶:Rr󓾍 Bj[S3 [)a|%Pb{׹}kL#N6P\JD hd Bhj9fT܊п[裏&.\* ݜA ϴIQli&21|?NBBAI0.\zĤZp/+)aEL plbyxYiQi'Dn!,%QAwmc i8o$Řr9e9|kxwȭ !iYqqqhF 4̴2que&\{+^ݫM |zPQM\osY@68ץ${hÏ BC)L_f BNwU `ZW9jBTMil_JL;0BDAu!NA4tH1VE$jN!bx7 KIQMe?.JaRy:ᄈ n:-0}jI3- fzwxEUn| $& f2ëlҍ= Ǎ7ve TkCC|mA( mBV5٘|L#߲GOB9 %)X,Dz*KҮ=_caw*2efϞn+:u_oIQo ccцqSB\?Q {u Ѫ姟~3J|*- F^n."W81cC 7hTb)F᪋]::?}*qW !ƄQXkNC"Y-Ývډ%qSFJ;UV"''kƥ5?$fv7Ah? \'u.uS1;bE(Gƚ9!{>KBp{gq !vqGzԨQ5.>]@V}mZP8cK.q3rҘHqh 4&R n-B@*0 ;!` q9OIQCl b䢢"'D;v,Mw?֫W5lؐD5pdz惐 =`Z,PcXFF"9 Dcq&M&Հ5 iqT ¾gr͛7wCuBDv Bֈ75M?Xu iyT"ͅtJϞ=# BdeeC9LB~ٙSnRo;Oi/)!&̙_;Uqi/v qMqT3|`=(4ŵ7 !j g~Zhe%2B X:t@\d:j۶muV4\0;!j >12DsOǰ!둅GrYBW]|_ MLo.!ĖQ]rSX:!b: ќrjsIԩv;$$L3|=&!D-kվ4<*kb]bhbj޼y ؑ>/8Ж=kB2$"qFZ A 'C6 5dT\&!D-H0;.XU[DuȝwyQݿo߾Ց>UOvfO4 !j!^ NCr~#7b ѽ{wY"~ZfCmrLžf6't3qAh뭷v': =bghj~7VKL'kk.&!D-mNo[͊ B,|M79,psl+yxҔ`nhۧ?IQKWqh7/= jN!lݺuŋc4^q}&!D-,7H3د_?'D)88TW?L}}3ǴIQ9w%kfp?5'p !R< Uu c*+S?ymC°s o?n6a8!ӎiKNN ƌWv/MMBZ֦|A7;.1¶U& dx thgعJҀ?vkhw?` !j93MDL űchNpZӦM #&;;9>-AhiC_$1/D~_u! -wٱ4 4x`>P |p'"SI\tM7puB{*SVɋT@7]SWuCYa]w11c 'D.]-p5jTנ:(N@^iU? !ۛ0 "tI_tBy]zm 8U&}YZWk8IQXzhѪM_sO"0k:tWNE9oӋMB:FC?(. -[t'NtBs98$ B,F|衇\e1n8wA L}]'_څC,2{ם@_]r5TlPϞ= ,ߐ$\bQ^J{>Vmz\d;oembK_^mMB:gN\v.E9!Sҳ⢂\+AXc|ߚ=t_O"n5 v WmWAp o*l89'pXu4vmnvrkq1iBq[3._~8:M!,g^٠ 04Fz&!D k?&b0kuӦMs"=pC qw 5k};\p-]8!RM_zqAhm)>A! Kq͸;/couևt5Ŧ~@"Fz;FrqN>hf.UζaÆ5lzMo!D qky)g96/T{€TJ#&MnWMR'٪UyWbh&.|Hi@Uc3nBy)b,<諯D*zj)裏fniOwy%V) !Z褻+|ׯ x"9i)'60"rZF}׮qƥ'c)~p5-dٟ}fw}U}Җ[dzŴIsCWdëtGQ4>ci?ܭ\҉ԁ9C: -|=Pw-8RuV5.dRS ";A@upBL} Si/[)vQ;̒ a<2MS9hZKi&NV8- \2gq? }Sj"ikHJËZ[iꥮ8!҃Lr:ZtI0C‰bddd8|0e׻k޼ѣ l2wixRكJOLQ]q>E|ic" 8447:5uۉܹsM7v}2_R{c_~= \+<?Gq턻MLߘOTjRi!d?',t< 鎰v9~A;|iF[Yn2[z9]veJRiNξ \T@Dg?D݅ .4( a+?|8N3eN4+ӧVkۺto_7GR~AĴ۴Ill4 Ar?S]pefmF$i8N.?#'ë8nq|Mڽk LMw-H94 DXp#uo] lv9RK.+hl#6t>~f<ృI\l٧GV肪DNNڵ+8N14;mszLQ 0^SNqb)Hna1()mW]uAH `&_b52ƙ^G!LO+L|W!"YwD!v`¢?+T6r nC!"%{2ֻ>SN -5!A!61u52ͫlå6lR't[p[[ &VNYɂPSSBIJZ- B0d>;;ۉ[ ?6IH-zvygOJσYPiK? Im|!vo?סCn>: 4܂ 70): NXP )I6YnSS7l$-GJG/^tB.88畹Ƈ Ol`:$Ixbn M-FMjq*aQIp74oэd'\޽%\B0`Y]Z^eHr"Enz&__BQPtxM0Au*)S{]y啜DYM3 ".y$43>;k>3FTv8!4oq{V[mVT\ Z BǛ\z $;ꆶ&u" Zu*ٟn 2{]3~n뮻_UA1.L[h5 !DeZ`ivs[\Ȑ$O42quVP鷟~ PNtq‰;w.N:Y@SU3c 撢Q<|$4iKEzP`SPBS8Z;x]Aq3%D*Gg u}uڵs}]u=qL.Xg\aÆ_4!HJtʔ77>,oxM)i>>m $?[Fk'w0oumw}Q/apP1u" 뚮{ gXM 7vXի7|ÍmdsC!RIg qseJB6FzrZ"}kv!氃fTw|7lp0 1DѕƄv=52 !D,NA ;!@Z ٩ǃH+n'Ov"\8dD $Ә0ۮqqBi)VY} '[|K5&L@v7.HK{.!:?<=nBlfcAne 8 ä䊄 . JX@](ޕZЎ@NANBQMLYвr)o\OE f81F~khW!pYlL!i_ƽ!d "o6cěBLà /$sG}45!{RI;ȚU":gC]M5&!IP ]6ڪL صm}oU#n7._4 !D޲80J٭;vҲP UW]YSkg{;LBI@* !Ԙ`Ø1cRyk)Bmt L{6A(}#Okm&!`w33ΧU?hIEFM5 8<Ӡq}~/auLXdKڳlJA!=17o^6pͦ Y:.gE~8E[pe+MBɅDPAq.<tQLjU;|Oq4lD&˃Ko$IH!.5ش̎-.U5j%f["Ԍ:uꤵ߁ o=AXV8jꗬ&$W쨚q,2dKa%F](n癙 _Sqڤ=_/ޕC!" G{2믿\[8)9nQ^XK8lAhiScB`PA0 + Ok8)ȨmUTݦ\)&!(-}7%7ƍs.×tŦ\?Kg~GwꩧҀP TOCo#!7> .J;tX}a_d騣r~Kgشk>P8ΦMBQUatC9ъ;m4,[ddJq:+vz\s ]q @{3=BKLCMKA17ӡC7ut)~Axկ_. ]1kPwd@$4w2T Bic[‚=.%G']Z 4WiRsBh7.2a8U} ܄={vڵfp XMBŦ7Ԭ h A/5:8 .]` S=Oҭĩz`zn$HS2Pcr*E;]NBY)YYӻ{ iށ 쒒\ V[~&!D}TbkfgLN5_-cAп/ S&!DQ״(s3'EN'O;,W7vޝ+;+6 !҈SLD!ڳIt۞t\_M!>#-[W/-Nr EU!-1 !҄L_"q"|=c=F ;wGZ{'T̋`RA2gB>'D&֭[7&+W;jji]vzggIVuB~m]"i)ULjtXmGkSڈ%FS^`RAxi62miԴH0b B6m:&NR9,c7U3g@aϹID~ MGMϛ^1x}JJ*qFP QVll ;1 k%H4([Ȳl.=̙ٝ}-޻ާp;Ɓh8ذ5ð_ҧ߮]zz1;C>wC5!n1&ӝu2`0^%zaxbBaҰ ( ;.d#n%(p#Y؉T'q@l  _M@իW8nݢk!O69Ñ"(4A8 7aF%H&l5LH\HB"d `%#. <`'6oL=xgeX/ Cxz;€0t4GG;C]t/6f̘gWq+ MG |XUFىlydLy.6"j1`1b\tęh 6""w# |9tPBł ޽{"Ʀ |PX+yOi}ĝNci7 # [ XQ=u$./;~*,L 0#]HbROo?n(((P ~qb.~LL/XeHF:\:FA*!؉sDTCwLGex:Jmʐngyl؈ٳsz*j޼F흐w4 (t e0*tt9؅d +(iVW%v$b f} Ӹ#Q (_Nu ФQWl޼0`s,eVBΝ~)vzÇn}؄Dl1WmHBĻ 6lDE<$o(e˖96|/~؍dXNrQANUNTw[ȬݐU#R qcsh"siy.my9; \;$8qbT˻{Xʰ*RJJDy^ 5Mc/pX뱱1B@ .FϹ(wN*}etuEEE> ;3N@gN.ʣ."SlaC uXIxlm:+z7 SW6>"  1>WN8l~2qsD齊6&MPw(_%:k*PU(T,jdՠw\0N>v1b-R۱賔\q RsNd )X9Fc(nƩ6h.OZXgMI2eC׉*8TY(SE馦Yp,P;H`K>6G.Br;gFrmHG1lj c6&KKx4tA[4 @,g)mw{1Oɮ A:JhRw(fŋ#kj*Z$Tk\ۍXpd$q 1paYRIXXYo좆qc:hh8^)sT~l[TRE6fM 5ZTSNQKnrH}LB%؆Mߘs6q^, ۶$"$ˍv>ŻxwV܄pG%!NOiAtwpzi4FDb(v~*k + X]p% 30a#0`%v5(Bq'HF$!x_m5t\6xń TgSN#+[oetЎDT (d `hkJ~lH]2ŐHie _uUW]$%%٬u GmnT{>nپ}kT& dc{=6cr|$]3Q 1KW ܘk×lrPlFdնGw'Ok>@O߻k6> њf xK4$a2B 0#0so0a# W _Y1@$B]8G ,ػ..ZP&XJooOo^uhQ4i=^ 4x6m8{vM?"8мԘ%QQZbk10#Ka2!c0d( B~& si%B~7xcc(01PR3zM1=Vɝ{jOc|z5ZY_DZ~I;_GZ*E6\lV$c!chFƑ/c1U͊L©hA:독9U|K.f۶ms{,_ >ͯ%TIHʃ .6c# (n$3_0 6 p|X5]4x$mEwt':of%:F 7Սov#SOU$T6TlG2a)R(&@C>X V#Y5ե Tč݅wCp%8-rk~ٚSEZtr6l0RD.krlˇ؂ d86FVg|Fi7i"c*lva:N:xȌ:JR[ۇ}饗8N;"m1[$&lc'ȅFCc\`!M$|,Z$b)9S Xg`^F؉NȪbQպX4eVIkq^{!>|x52_U8A%:ۆ- Iflv0FHQ]d`<:F8 1K\ߊ,UtIW_}UTs:tIh75HBi^e>!̱ l1N@? h|Xtd71YUytA΍7ި*zWtRx>/#j7믫jt&!ߊJP1? SIoƫ5D38W` Fb<" p!Qw6NDG7꣦)rssV`1wkJvڷoII (By vF.Hi%I(ƈbc\J\^B4bƢ%l9A5@c8 ,܄g1fc13'Iհ ªXIm}FԫvӧOW9s設j1]>Z#) ywIȫF2rp|{2 [x#`&84D Gtɸ0 b1RpȜcU/ϘF>|'Ƃ;3qQ{jjjĔ_MV#5 M(fȄ [D?B QqhV Ne`C(^VE@o_M9s|kF3{,v"!uSy .tQ#"-؎H4Ccf7ocU4BSE\G&&w23 MD̘1C1tܹ7otH"=1& .BJ..C:C!{B,C {GN8}LRlBc"t[C0 6otSu'ШxJaC6&Q-Wf! p|"695¾艫21F1*b1 sjGs-ص.Vwt'AWoaCعFAl*aUS\l<}+kc&8^Xa/Ay%"t=MH*eVycB:TpGBBqU2L (JV=aPa [VU"a#x u.(:HLQ;1篿݉c9}w=YS9Awuyq"G&pa1klػhcJqXH8kUX"hOټyc2fIWZ' h' ;+JwU ̆B\&Fl6,9p)`2V hXH&kH,##?#KhWպ`X:&L1 B]{pUW1cA;28 H~zIF]ݐa3lJ4@;'1ec\F ;:(-5>ӎgӦM~sw:sӳgOCuU0;;m۶u|!w}qV\B">")۠ ep20'Æ'#p>X2[3=c}Ύ;ށ(qFM[Yd3uT_VT'v#?Q%!$'xB;:kZ38]vc'sR5a:N(=SF{QBH:2#:ޭC0OO<6 ј%H6d!(M^NE m晫3p@}Zxr-OQ*Tά[;!U;uO6M.r@uħza}|$g ;(u4C0Y{&v| 6l42Iw1k bvDMk2UOD;}WrSzge˖y6RRR3{-~~'!#ty 7Ծyi…T$"jWPM!ꤰi؀a##M]p §Kp,Dj*)=F~-nDF6JAKB~ yv$V*;u߸03nB"֣;MH6WŸ 6lDL舳1XdER"OSec=~n;Z' Iz#&QvBzT̃qy8$*!iž(T .a#2`,8H%!N *VƏG`S򩓋zU[gP%O=Ta7VJ- ./iU8'!6o6b.83 ț*A-sƎj2Ov~~F"vVEv gV * N~3%Dq\n( U `9(Faoذ-(V0hj^j9=ϗ_~9j۹sJvto駟1(7 g[rrr裏X E9Yȩu2̂(S4̨/p6lt@GZus}9h"g׮]!魦jѣG+)ƓO>ILLt_]oK+T3`FGPQ"THGA4IvFrh.Uy=I*D a^. uAo{tJȏ6n}uɸ O* [i2}teꮵ܍"pN8L}atwB{bH9c@ذQZ $ WwM]Ga{>ǁZ{$nݺ9'Nt|:N5rs6ls0$zOo:3gz.#%H <1TB|ay#o;0'!i;6lOb"_K_e<|T;_>a/auJZ:F4' Y؍ ,aDCEoIXzy]su:}{1F[y=TjZxr,Ke%u$hF,(XlELArH-t7]t"+6oެ mI ر{|ǘWGu$BA~vb%L &b~.y?ہYL<9qa{Jq 5ba ΑUI8Pbd'T76lԫ>Mo`U'c5T} ]NCڵQI: אj_JP$$| &\Y tA >Վݾ;{`&MRg6&M==}WP*~ҝ۴icPT17aԆ'g =^T:rS5OVk(D$€_~9EbUɨJ\[nmPT1qP}xa)N($}Ԧ{ݝ\y2|K QyIH;{.;DRQDl \2CaFf1X"_ hTsQ{]{v=+"--y衇c9&蝱k;ւ4 {):Fhm 3lZM Mv䣻iʗ5g˖-h =}gN:<2.8C"{=]P9=vap Fb=@y]w9rKHHEGn{ 6LXJaL@'!B5Q6rtWV6 1gc/ذa˰#0o(:( 1b'///*ݼШK\`TEGR7P84Gz;EJO0dž -132#Ҟ={k{v?nN&j("I'|r`#l@.# `Æ #>?ỉaܸqS ]!< Q~6`P a9[ذao,<%jgLt8m4% 1P$!ucZ= U̘1CI(bJR1cMmذRQk1XO5^[pQ4T H/9j cyR f/-\ U,^Xc5|ZAzٰawAñC^wuf?8)*ȂjL@; dǣޫW/26lب$H'l]H+!C?s-駟j0>k'T$$:u<ݶm[Ȓ8DDT ذa|dj}qN8[n='d胈LLzʰ5;P9j;X9$}qAJm {lذV:BJ5Aէzx.˯zG/G9?{Ew])-ymܸQf<loH*@vMTZj!OSq#\8O@6lh~n~EI *EsxԁZTɕbf4~ 6#Fc_nV {vOj TDN@tY7-}&!Fduݐ"Nv$W|+jpԆ {㑄s8(g%u6e:(-(--TQX8n v^ V2xUBݭTٲ]S T}׷o_I[zSTK0$$!nH!VUru<Ǹ9w?6lh>t_ C˔0U ꖭ^o0dOJހDO%Zi7ª q,Rp@gɒ%J@oBIh(铹ݒNWo?-tz [ǂJLz0:%D%0~ V(\s233`߇7P)6b<.AK@6lh~XcRPɯ"YF^:ї٩F:i?&tX#49u |9? GU&'';D l%bçu;dž p.:S29=ڊ+ oe~wNJ4Z駟 >ܓؾ{㎫BPBiޏ C)XIBXp$ uq8#cה)S4ZB & x4sH 54'NZ:BӝS_Z8u$gO-fH_P.CXza9~`Æ JZpu6-* 7ovH~)9i9t|].ֺ3l"M@uRG;Pȑ#A#pa1.>jÆNU(T*7pTW>;SaaTRtw:(-62 L;! Upoz$W혋^aF+c>HQ*ޤ*(*hפjUԔܴM>u!A7:qbxv?6l#qX wmDRrnhĈ{ァ?HDJN:eO%"hPҍcך CscÆ sݵ]U!׭[7.窫ۊZn񲲲P;/\4\VaFPE؄\fQd!R2NGC60 ):SP%dkp5D h3&* 6qa)7P^ۅ& mk=zZ뻽&9Pmذ@A%H&#)I&JB*QtPhI@"/ٰa1ZpB `9ґQaOh>H=.K6mr: n.C[ذa##MHqXFwTDXB݈zv 7xPΉ6<6lhh i6A>챉'x*{c_{R"ս 5qRRR &˹R"<3ck 6B \qHGI$$Z* <LGB⩧4}5*ӈnrjuX׈}ꚮz ޵kW%HI@0 g6%UdĻ$dtMJΝJ VJz4*;QDwN]GÆ }G@rlt\[lذ'X,v9+H˲9gH@ꌮvJ k$؉. z#E]Q^he6,G}T3U߰?;$fb-Uٸ3lذ]c2Ó|,s']oLB }vN:JY^~:u=S4&,c 6;ll5}0&vmsqj*b%=4Gq.*ofRҥ|(\I( "Æ 1Twݶ3Y_>r; vS:SѠAiH}x]賓{饗T8%P'<,H6lPwEcE^"رvCf *9iv*>;-8OeؿGz4$UvH~gGiWݕ{]RP1zV+6l4$GΤB4קDN5lNku_w<>w5v0>ǁ59}ԭ[e_|vUJnH@e*ڰa#c NJl^|ŻM i]V'I\eeewlHݑƏﳉ\#PaF E'.챢en nw3FqL:#u9"))޽p;iiiwXBߧ Jqxlٰa)cNJ;"7#I_eԎg׮]~bәg;|;͛\wuJB`Æ;2챪%(y}MI4[8+7AVUզ#_pB%G>gI&8N`m1cQ 6t?U>F*Pdcc-Vba darPv?*>#프|Qߞx gǎ*xwT{]6l؈h0ˑ=vWpa3;~$|axO @5f`3wBJ0꠭%2CGmЮ];qwyh"wޫd\Xlذ/NXmDH4yDR ?+x*ľh7`-P=Q%ڶmVxP[(~qUmrT*AL@0}b3 vTXQZPR&؊Xq=.8ˉ&jv󑎒Uҕ1ơX3OO{ЧOCJ:>靐?|-]gjU؎ a#S`Æk#1RQ{p#.lz2Nn\-B\'"kCdT[6%_:wJMot (A6R6lب'&hθ-fc Ұ9(Dq !(, X10 _a4anBot!hfh߁vŋX* i1 aHt+ H~ 2OEx a( @\KpNq8]q(>hVhDq]z2.7s qu5nwJ6lhhhh]6M ,bVk\;cV1b1ċ8bÆzk'8  ؛.oSlX9 Y$;!6l"jVk7 WX7Jx L»=^!HqaÆ Fb " p# X/zt6KmذaFG#t5x ,b-*PRf+R0` 36 HlذaF `b=6!B&r7$9.lG6!+3>p܏q:b_Ek/Mb8̆ 6bie쏮8J@ W*\'Y؈؎5 Xziϭ0B!"Jae F!DƏ n|2~]8&^=U?%PlB,^ >[ܲ&݊ÅDjeM[N2B!r9]J͍@lQWO(M=&GP"x(w[fy-*:Bd1TNMV;ލ#'a4m ߻ir|LuF\nBd5X p'pQefd[a(cN{g!هSADj8w= Ym^,n^xt% 6pJlB=x:Ā-@VW0r+TT #"+A v<-mu^8k#.3B!=L:pgX)~].R~kBE ef~@g9LRVa^ ?͋BB|fnƀ-mseK5vyᲐ0qbBy& ' w~#.Qb[aUi˝hTA^!2 Rg5ycnKol=%waLI|3B!2WxZoleQ%}<5ؐHƨĮOe/bB;r[$E,atÅZk^wr#"#Wm0F!V-M"W3UF!`q3 *b20~n(Z&_?exB0oLI`6#= aL6ʯ!w:[>I¤ ]aD9N|7F!` z4&î0aL+ X!1uqE~(@/gɂĻC-YBIV]q-3B!ҧ~d3PMs OrBc)1!HEְ* T/iޟ+k/4B!tQPɶIv&O1|#"gmŝJ*ZdN N:|޳!H{UTƵi>ǀ'IxG<[w}!HE$JfKh<{k:`_SoBSs)ο*wrERѺ}n 6zBs[16nU Y*B2{~Dkw׽ilq !}6o 1t+I\?#Lݪ!H҆N4a]%2S{{!AzP2H  3 ߝڑ!5]VsWo $ ~ BԠJ|א=#nL~ґ05:IMu:#b~x`*0 ?#L|ߚ).!;XP5:P +yUƷ߹5B!yTc?.{]@^xZ)]ogV!ɩ튬ߙЅH\Ba gØ^#bNwV*8M ز %O˚Ƶ*B9ru+Ec3B$~4*& $>{BB|RpP߈e1^B8.c\iBs~Yr/tEw$^>H0B!mN<!/I-bvB ulq5B?rwH[˚.7B!IU|M~HEx.JT?J=B8.eea/ܧr^絮[sW%5CsSbB$p_Eޯ 7~U;W!sd5MdU K=Z޼U}^fBLCmd-Jd$vIi;u#W#Yό '*8X\@0i3B!;LI~r=cB=`>J\b@re`4Uܵ媵!.5cαugqV-o|o|6^pO{:.(W:t}Z rw輖3%CeT& (ͳ%>dr:c]!9r8\~.IǽpW ʞ8R`#*wX k. ϼ=o~cv>{i&;p/! f8QYJх6"?R="m$CɃ(OZq9rݚty@;#N{GpJ%X(W&0^Um 6YO\XȿB,. MwbO9Xx,5GD"a܍xSpة x9ѧv׍ffu cى]@$?w5%TqPK y$;\3ʥ_Tvϕwe3GrG @"f/ PTP׹m#m:t;\ޞ_=k8S~v##~C{@@4?|˦C(9£2)Gas`#VtU.Ndތ ȣB ebreydGl L3 .qk?$.XUi}IdH\VСS$z$▋xWdiGL @&s(cSo= YD"a/0]:QYu@EOl:t蜁^}AcyC%\(DP"F(pWo1%r8^ kxyn`<{ ;1e26D+OQ$iȞDvqL%3Ѣ'6:ttpC"!a.Y<<#Xd)`FP"HH eوjFD5 QE={S XĚn)o=-dGɣҖujk]XKmHd/ddxj=$#NvvFVb>_ e&dabx8zڽu]v͈yNdǺcRe%ꓜCbf'# GF!G1]Q,HX]%-e(gDq;zd I<:Hq Ipiq4 n,,q[-d% B1ڲ] RRi^xI(S$FA5JNpd~$QqFM@Gÿ108t:D"4Z$fhj' ءlTyzVp xA(JŊjHuh)iوy;LZJ"jkt={ HDg"gp:~ nA8צC kB^mk-GVb$W0̀X̙!Y "F0 (LX<3%*BP9kHFaIO]ifp[5#DCe+p`v#Mts%$` [ܐ(LGO(oaSxLTfbd"x.5$k\'UlC"'DmdR蔐p:νN. I>58 ~.WС(g}  Ye%fykS82y"u#+ۀy'J * QWVr`CA!/zpG\-sŽ뗇*eᨘnrOHc3ѡC@J"vα[4ݙ_9,f<|^$Ng1%PlzU,.T̋x+a}!]}䁨GҡCG$r f"1d {%J\81+ x F`S]2! 0fϰH(cA9q䗔 L^%Jp{,T)/: ~ցDpAc:t QDVrY oLRxGcJ,%`@b!|fGTֱyrL4#]J%Ι"> I0\@\/6ԻX)dygGgӡC$rxHa$8+Q2͍VS\7]]v˫k 69- N|Qel(G&+P\\ Lnۆ2QoDܟγ rD?*Y S ;E3%#-I!'Q>2{}VyP IY B"XK:@)h% QY\[uСeq8C~$x|QA͐Bsb7 "ytw\*YtKIW\8]>|i%$:#4죷}yV1.:U(b*U@\v/Q֎ie T$:t01\'4#q a(d" \(\I!Ah;0 Pb+R[s_ٝf7Q %t]I)QhhF[D1ϦCNU9W a&S%, rKibܶ2ry)ԦI.VH7Gq/ qS=eAx A =ˋ2V\,$uY#\OL(gSm( ::D*h*٩^/I.+ۦDurg ߷СłgF7 apa:tKDK*0-A!ԝi=!r qdAlѭ*ZđڳEQ fKV52QDq HqPaXC֍kkA{]PCOs68Uzs`v5c|fTN=$ff dfUTg)y 2(/^ƔxRi2dz_& Ȓ!I((w@]ӀEU4 y$a܏Ks1kȂa ~D8 HTcRǦz{0æCvOD,G}l7mǯ]se˜j ff`BBx=ea3ffiG"EϨ\nq%hw\4UjD2N9rl:th3yoIçwɑ5c]xLQ%2IISÌGTN %{#-PhpS(0zu:& 1]¨(G$9)2;cӡCG"0~ .;Sϫ !͓2|-( DDx;MnG*c5S#r$B~.С5nd~uj1JVL(mH&V%."@PbbvP2 ͢<-Z0ۤofF)M-&19%k e.)F)5ce,|>y@X?PߴСSWa`HSyH %iLsy}-&`$hzᄖZׁ5cs]CRA_/=9`HC84F$w7oW-^ 'fCMa2LPy]‚@mƢvRiINa2f4(U*(x~Ѱ dր[,7-l_jMxeU26:t4I ?[$ߵ"h )JޡAHGnU!Z ͛IJd -eBxwjD.Yd_\QP"Gy)v8d1J<2*UFd"MgpMy`Hk#dH4(y0ŽZ;L @Z?WfƼR9&Gf xX2,Ă璌R&,_~12)%)2Mۣ Mb:$V˩NzP{$ӌLNH4TܪT<: $S{eǣIn_I/ƥW,u1e6:t+sNiM>zz7<3ɨ@\$ #>:r=ތn5.tؚQdO'E2 ˼2I 5zn 0r <gYq|mW! J ¦C:3\$8-xMƃ9uw){7BpQwYqDZFZ"uw8%U6lw("̂=2))!#7BQyyȃk ?r9"<jӡCG5#VǣzFŭ`Jf%d)+HW ϸ8V$܏Exha֗PmS2i)CQ.PP )Dqr)P~>8="7a^o0(@ښ!n\.\H82O..ga7[rSS$͗&1-v Lo ;)P(S(pܔ>Ǭ2)9v}I]$Km:tR @ڋw.7.7)sVJJbo͍evn*BAɃ 5b%4%Qʎ8p7>~Gm:tD@ڋ*],f$d?閟Z'j״)DKY8YM)IظGCaj:9"?m6:@ GSv`L_"uxSWՎkos<$>!C\h8 c,u up cf&ȨC R X#׮F?#K~EQwxV^85K鶘,&%Sf$yppAO FkLse%Xw@ڊw@涵r`T)ϗvUu\)$"@ˑ$II(o%0s4cz8;I1z$DFti-z7S6o=]vl_1Q rTϣP"AM@}lcc\L5fs^mTdfM/$ rl:ttkmvovϓYχd~7:z"^_ޏHGIyr+:iLʐDUIMn";75SdJ$'?+l/DtO*fB›IвܝEy.qi/lfFʼne>|aӡLcm$>a~l+M天3GmOe<H?s}D,ճ4y@$7DaӡߕlN6f@%H| 1Zh$ER )ݮ_:ohCl'ttI|< e|",AODl@H;yb|I/M8r *v$ qˌ#C¤(f ŐE%](xdɎ>KSX/,a(Nqe)22]$3d ?O[?gf CiKG=/pHz#.yQ 6δZ.Q%*T "!ޚ#,ʣ 1eʬLcT $R"S i)`d,_"'镲xt -BB-A@ ̎!9c؟[9j*˱Z&i6:H!rCȖe ,'(j!U.iJ1R -$[Eyp.y1\ݿŷX,dx(a1L藙UٲdlZ!V6LëɦYeɸ,&]TTm.}nJnP۽P>ku%{a~'- @sШ@}sv*.0'L $-fRח@~݁ zJ%ɿTi1EbfP> @E%̔zK%?*u&X~NU$ɳ 䰞}2|{094^ٟ#gDIByuG,v1p/Ht(rLE1ݢ/#8i4:,,DhQeANY=9_91_><;Z6ƴ\z8?}WkE_jއ''Yc= OԽ,JB{ Pԥ/6je ͵ֻ K䓋s dE]:KeGt,!~h-TysoX(\^8/nOϏk,D#S"C*9wl^3kPYQ?!CۀH8ɚ5 r}w|~yܹRZoؽ@o6}~~ki~d<ܛ0=h@uI|@|`q0u&Ry - 1h4Sp SY7yWW^_Jo*ɓ]{{JsQY-v\|䃣%вweݓt90Lά /uC" `a+?V5QH[*q2H'9<X#-7VZokSZ=S l  =`LD`0NCii@1>c\_j/)"BS(=-E?; 7aM"D`HQ@g^zJE9J}'7dˍAV4BJ4։seKk :sX %#fɂBgUd`)W:]&D@=&~*UIrj}|qeN (eCC9Zt'!(4.CPs\`uT 1+/Bԃe4Pdz/ԥ'pdػ01ڪd4*  y݅AK[a+[=y/w/C'> R*;u;mPY͑]Ґ2S"${ ,djqQUa'szay)=`` :Xh}Ђۀ @?4RbJ?3u ^ LU'Ɂ5R!S8FbU)A'"ksK F&Ci%%'!+f˾%vR;SIg+!b N u RTfއBdǩhdHZ$ܿ +L'yR6B!!V?d|B]]4$!POB@Ik.nޘLz Z ݶh)["AS0O|r jjokAd(kW @;)FҔ#Kex9" .Ԧ̗ώ|Yzہb>^/CR@VNrTjU:^R}$?|&"DhLt?T~f!"!(9Lc$#%Ȫ?E$]]*D=W-dn!w)-Xwu$\)Ɏ)wY&(R*IA2r I~:۫t*yٍбdϞ"[]&>kɇ僃YҘ:mE韉E-"3:RBF9&KJֻV "0-M "BFt #>=GLf[ (|t>fKp-SG#S01FGk3ʎBǸP@/ah.}v'gUr`S/!F'xۍw;6b7B,F' b)[I 6`a-#`c-)ϴ ~S,:t*R(aJl#{. ɒ7wJz%ڐN# U!jO7+ <2S\b * ,@D;+dIܿt pZ (I+Ș%HPɒky`?6SC [m$&RV@0o܇erihz @]Q&ЊceuQdcoA8QЮhوN-eU=;#9?YS#9xRS{wz| x FFiAKS='jCYc#CbD "Jme҈6*v3ƠL;G8.!+0PSB TaSǾRiߦa/~ {<{8Oat]ږϫ˒ #YJq:?&TpPmi/2a/,SYJ+P1^)tbXOKnkNda 'i%!jYwvVG S2u5t&E&Y3Ѧ :ԞZ NbN˗I*N2l$ "#&-C/AB bI_Vr Ѥ[h%`GQ3x``^3?oO|N'H $ۛb)GV6gʼl? s,̔s`/|zy9*3eYlsgHA#50r ^%1N] | xߖ3E:;Eg+-ԟ%-և  bB}di%rPf##mD:YU)"(ν{F# >Rse8݆&BHADH7yG_.ui@#7vgShq`il+xMiMe c ʼn2½_iϝN07@v΋O99G0Jǰ҃ &L^;\#jp&GGːN$#t8lw1{guɪ"/ddۡ>ZCg-PMAA(TRn{gg$?Ї<]`ce}mC4( 3Ra}_[ B|D0QH0 HX?Nv'-T"-,- ppt憊HVB Nh+ybHp%յൣ9z 0]pjِBz0L +bz[ ;>@ rҒYҐkȈ4YLSE:mЮv"xHdaqteqθ ^HdIAD_TtYIlDdFH{'%Nd@h҃V:z)lAG>KFHe8t}-ʚ eA}h@SwM}aS]_յ0Q Vɶvт96b|SIsxYyPwB$u}ADě j{龬Ei %),iBCaćnҶQT.2f#X{?F ́k 1FbWxu_Rף^sݬ6cP%s@dpГj]T:\V-A%gtOAsCj'*3 '@`yb,x<p&Bu&k"}p  Y[0r64d(B˄Oݑ]i Fǚd@)sLIB:/"IЀ/%K]re'F wIGWRO(FkscS; So#Lcn/$bO(dMF&9S%o4j.)vV>۹ Y>9 \>$:\\.6p_Y:QEV: $77'HX:P@¾Y\"/ϺNO]&ػEoaBYQA97X9<CE/3SMQlJt@Jl%-FLeɦHOoyL`clI-emPȫS짗u*K+$(NVHgf 6fX+ņJZCd'hD閨;w2CN23,&E[CאD}DŽ >T\,GVȊR썔౜j4BԥGgy#ݷ6J$p1&vʄaӾ0apZJ&ZQ,. EW>@%=1 / 'M/ȃSJt#tl~( gPxE BCDbӹK:dпc dyg mdpsfXk,gra)A]Z"9L!{*M 2t >~o~]ݯ(=Z8YRtGmo~g.u/dȆxNhȔ[Tfwޢx̟aȄu 몣uD#ۚL.]݄oK4䳼5ЪVu%nprQ?qb rVNOc:!:d4C JΨh֬Rk^P|\|Q FSu6wԉ2K]&’`w[Skj\8?q;BI f7_=LLGLBƬ-y6q\Fyp=z:u?HF4UW<ֽ٭{CyC !eCѠ\GI~oh L%bnDžу: ,]lF2cƴ˟}mǨMhE3^45,ԥ."ݵ93VۆJ0cSi;ksSE%ZT6(h[övL.O4bԝuaJ}z4(^ԑ4 'ӱO@ϝcˍ||~VTN_Gߗ~%Ot$mGCw0SL4L1d?EtSekcAmKnxi{Sp {޽QsyG9ۛǸ}?v. ϵ(~:H{Ӵ-:[axߊt X.IS"-g7UsOgGěK]&HXN^^\bKu(GYaIw+*̦BVI#Ф<9&ɹ7}?GF$a 6@-']2l $2:k~-?7G3Eci鑓$0Bs:˟[ D EiX[h+L.5i6֌LQ|G{w:?}8F{ژTtϛ;Sr¦Sp_1oo[KXpco"x􄊖QQfZgY[GVMvWg߭s bL`Z҂tkbc p?e3+P'>|pnс %ٴ:*#k&[¹R1WehjfR$(ɂoA?QaVuk;ڿ2œoIM+ƽJ>eMe$Ƿ^S]>ې(ay趉s6ʼn-JI "+`B#`ZQ*иs5d;jf?c jL$i %_~z|[_{,Fݯq,syiR0gJ4VkN·ۖo6nTQA䇰Wy4LD;t: *1B2=1?,ZQSEExZǻF@EJ)ތe-]4LD.7\TkΕ`cdg};6LW@r~{ 3^,FY@jn֎kaҺ4[bgHZ2M9k*(y_=SWԥ.#tQ9$[GkpOI!(n/|J@ZQ Ldhg!qkjKskZn5 ha#H/@?;?D.&ǁf6(| UkD:WIXΗ%QmWy~?@Gu0rޘ 4悀Vwi#a|O=ٍ,& oi|]e-t?C'wZ%QQ@+Uw| lufC6P5W)Q?kXOԯT(R@)Iv4)l^v g'X4 buVZ}td[=t RE"b ,r\T;QLpR;ASK%)X끢0b7?|m?L7f;Mqɨ@̼Ü~Dx- (Pc& ]]rjY&8J4 &j29–q^Qşke  ۔!T r؇ 5-z #5 G&=V¥>͗*v/LACX)uN)ju,Mh-+#F)6t~- ExfU%*ʵmy|t FL0@= NZe>/ǀcr|)rT%tdn4pJfy!╫*qi&rѽPrNʇ@l7>xH1 30k'T:ތS#hrK˵epͰK]F10LضC8X:4(oz]]X,R}r}^"0Pd)Lݿ,+0;=Xv2u`=&bruw%牼uY.X9;UF!bƫfymMuB>ݹAGJkU+)+&+curϻ^QUJAR0Jfk"1v r6 n_f_" X0N4rKc\ki+@ƑBKߊ0G1 QG[U qbZckMj8fuC@ *tN Y ~P_o_eIx^I>\ZU)P->M+2@&շf=ԠCT0ĵqToQt $\F+Z/=|VE+n1v&햆ۡRqm,ҝETO`6b6 i `RDr$T.">IJ UD E"HM%9^zL$ &,H)NU 9KWA&k|[ -LZsN"5qFd\:1,jSuE$ڗI1~W}^k;<,WDK'RIWe)'lXMfKRqbuK?EӨ{'h% 蝳 |kSiߋ\mZ#ŗ]1cpst,+._ /웏FZOJ+`^G>kV`4|,Q!H)-D# (9D#Ӯߊ HTW2= [BJww#UWX atp <5Hɟl*- 0*!Z3'#*tF)$Y{uXQifhߎ;&b6BӰi͹ 7<v 0]2ս%)1{c{)EႾa_B@8E줠M0Dؙ/B4[CD/Ӊ s{v)F!2 WL2i=.Z@E|xTڝ AٿS&|D0= 8$o2UBa":(rL\nҁʹ1sTd,\'Dq߇qH =le)l&dx+Df XV7Lm@s*ʪB`@L!H,W3zFˋTZx#̆""]ZD s q'ōS@TC ΂ӌLpʻ 1j?KAbA-_lDwi7}p~I:2 1E РxT};d҄æ3]8ȄIW1Iuʼ ]c;ސVa: FP{@~! "+sO*@˙$|M"iJ(vM}y7 {)к0Ez)rQx H2x|%߷yvP*bTBuɂHD@ aK/Wv_zhdSa$H{JH% be2"yX 7dDU)l„ xNaL vڋqb4#'g+=ADG vren dARA r$e (/_(\ @)?DczTQ* ^7 >"=?%FuZ=Kg~ x|]H"D͢^&g U! _<PDn^MY'Xa ?ܡH3,<JMWMw(-A u98d#ȄIY%3 (֐+<ou!;-$݁2Y,Ʀ4H6TUe%<`31V!Jc>&( +a8ӿ $˞( Mso8L ZtFH[!H"J-zel"*n~uGlD;s%{Tl 飻$A WS^*@J]lh-POM'I$ M8F{|0;(MiC gnV:Ds#2苍D'AdcrQ̍ntm9X n$WuЍRf0Eb.fs, AC-t,d2hD1@ʊQxOGU<(h7S'K#wͅM ROZ KP4q^vw1qʈnp*7E( F&(NBnF^ 4ܶq#l22"P wQGY/#jpNM~DD_6Q+="P`c{`{@TAITMᵁH`9~p#`T(r'd0*F$w襗 D@Iwaz=$Btћ`/Jc  xBwanT"Qi.ID&7lJ,v]9He9ۡ rP02 J[n$+z53I #rAa,F"cE/L"Pj"&i:ǃy qAc!DJ~l{P 3tCƧl:z#eD[3DBm~M* D&v)x6X_:wiqh$Qt `Wu鮾+[M<.T*Q40ǣz B;GIl,Vpw pyRi.hH @"%E/L ?R/VLm5ٞb?.Gd]|v).A5=^DžHV5?bB9GIaԧاK!&T]6bH^&'220eCFwfrUdWQ${6 ě@2S()eԆ(sϽsnbu7#X+nOit]+}MkK/@l*i.Ur.IR9㤗з `,W>vq`hGԼ XNt~ h~o;7Ihې XѶ6IIھGWLl~BXm.(Qs͙ZZ2| @?.CʏsZBS[^U_Q2H"7%DIaMmTeTk"ۻ W=yFذ޻5W\Dn&I8& ɤJ#3#"yE"74yǤD"".yD赂 L*E`먰֤Y˄GH$hX&`^6Q5.xT\ >[gBؔ2&Ѕը>z /<&X@&{ґy dA0U׶wG /r0B^\·ڌDR|E/$Cp߽#)fC z9(I;AQǢX 1uILe12(LʲA߉ۛ0*Ta ] mzH{",ϐ\pi"D^zI" `i^h(Vl v>^V%ZՃ ~\HxI4Ў:XVDu|A3H$|i{Hc{& ]Z=E 'פ1l3^^zI$X $lW䱫 }gwv#Je襱GƛL*&NZ0͢4dV3b냈T\Ձ\%F5WCM6TL@*$nr& A;'!æA&LyYiL M.#0lRX&MHn#6_/AdBe9$#j&ݤP!#M=jL8 ! 4@X "P8PVD2 oK/7*  ߑpf8)B4..LF-hSb;bfs{#"u%JpMO ־4;c4};m~Nx4!_ؙyxh15ݥ1뭈bz*Z[ߖA߂JswqY@Kò!6*DS t xl=Xe|=YR17©vpd~7<crb^#dL̹]&TR n4܁Lt {BAY\{0lK/7ǁN@ QHM1#mUiB5uAXS8"UrTrtS`/ߐHm@VBѐhʕXMYHݵFKXًrdt4 N{ED`xf<<e?=l?,CcjoG%4w*GȃJSR^T_%a^A!JitK#n HE,cGs &8qSq9Mtf,Z{B; t(pѻeLo5ӝi"Vq$Qxo 2$B>X, rHX:>` '߄Q ft #iǿcDc5(GiĢ\>07ёR7K8/,YM")DBȢO'B ީԤ欚*F)є'_YȒ`7? Hm((u[X>L<;!Y Uт'Ԭjé38sƱ Ba~bOᵓeܷ9}篢2lzVְCY($iP2@Nb8zVakC`j }#$ pxޢ^.ɿCDF ɲac#K.xC.o (¼] b{yiEoUaR G% Ł^ Ԗ_fϊ.vwU8YSng by0֜s$ <|dD E/xAVMm6g܉ ~+fdOk͑ITD|R*"?%]wO/B-M$)0bQypL]߮ʛVJ~rD"5a<;ݵdɧ]? iJ.`ev vߏ$ >0|,o-} _ jw<>`1j,pZ7. 襗 D@&gZ[ F=P~ RġU o^ܿe̒ > D[r pj;:k1 6шqwYI"Mc˼weŔo{he5fE"q<!BGᖪ9$r_:14]~OkY "X>"9$SOlB/MYǟ$~SI2q :9{ࠆ,ݥc=VTLH0}_-TiqH}qWDvOţ]cK/'&EnWڽCٶ=n7uQ$Q-L% qQdW)AVn| D_vצ0"i;Qk1;rH4hx|r;j=J@?A4$2@Z,!>%%bDH$Ma_ TyGF$aowmKGڔ$i@D9^z9 j"0 $ޢ.ʁY4щ ljt(ѻfc gDr). MG(iA]U YFj@j3E"_3^Mb6%u{oZCaGB  ]l[Hmu~| )Ou5ybgӡPNAE' !Xˁ- .x oEA*9;|غ g!ʣ\ح8m3$cvG%DR|PRAKDRso"Z-z5|g#Gܗ^D(t)—ʮ7yŨCWp%ޑ FaZNTJ%"isB&#NJcԌr}ӵרEF8HKQ;)2^V(o?*]Jq饉$wDꯍT,~ߞ4PƱfQʩ/cA#$u b';Hh1_<Έ枴\`*\)%:OdhQ^z=A" jof#*25 Ylgj8,Nw |bʋ߮U{ N +~/qȞB""i+j{\wԧ}5B̳"fK'7qMT_'E`yodle- 2KL]yGQ;f@p҈9vrtm3;/Zs~|ۦﱷ0=Y$ʋaڡtphU K9=,Ѭr1#jNF#׼ ^z=1Z:D"QYo08q z,\ed헯G7NCu~ ɧVbpF]Mw7C!8bɎ݈cj\9PApՈ| Fe/\~DM? Z5;vD",UǕ1+ {Kvw륉d`'\@| }13ty*$rL[n$q<v $o5*o;^ŭ]RCJ7. V ܗXufLLg\H;`&v7gUZk;RZ3o!0nDNĉk>|D/M$S" `fbzH$N|e8H"8JvʝFJJX/@4Q/ UmD8#X@# y#RxI2ZN#" ,z配ijVBcO W;gjTxDd9Aau1df:aZmyA(Hpw1徰Cq^vH/~\駱HDa?jǗ6z[PO.G3"xQmTZ8Rmx>Ps^S_DثA#%$#A!b/⼑cbwQdW(ΑY`+I{l3>KUY«*LNlJq,yϐ}tP(gqb! _SLT"I'Wˍc+>(]DTg;Z$@DrX՘sHgb6Ҝa HH~ h7HH*OE%9Ij:uy]X78SY>u#A2עv D 촧oϋ h`IH{aV(ZEx rkP`(7m١5|jU6"bTM3l+X P_yC4csw-ȩ_7;籧&-3IGnF&F*oJ}IGXXt}+KT8ofwY *yj  #u;rh;Xć;E"fwn8pJ(NF)"#=.xDR0$*ɼ* "$)J}&*qZB_g\ |J5 +|E!jSo|tn| >dEV41TIB5g9m8sK$n"lC?ćH0$B\ E1siZr9b I60ZA* HOq4S&G_<|'sN$?jǖu;dڱ= ˥0a+6@ߊDT٣2yhu9G--A&? /0'T 'VQ # U.X7DV@$J~ULRfg1 mFDTyt5p  ^<~S[o֞0y|Kk⥯/DY謁we! FUFE">IB{ GOi+3*ISf8+[k*,4H2*Hކ&D%Lm1Qʈ$Z,=:j"5=f^ ckʂyֻa^:2US#ƕ<'wЄMgw͔3m^0L8@*Dq\qDY}i$1=l`U( oD sJl|"ϊB4\IK&nzC/ 6)4d"(\`7js`\zqiE <d`GFOm T/D]pEXd^7P}Hn=FE>OBAtǿˎո E92 DQފ#pn{sp NL K\JO]j+RڴvLrvQo|HON%/߲6Bas5/~;Ø@$LiF7N!pîŠG ‘`]tZ@@£5?$dD+P6&ܐBlbGɄ%;u< +쏈Vx4i|ਭH8s%yہJ"C-غtzKE$-Ӂ4՘m(ƥc[{VMsXܶ=}edHj* 0=;LGH׷ _%n􎰃P1kٵ(P̓<" a*KyDyR@Hg;ΝuGVPaW8 C$L@ ge\NB0*%xu<}Tg .E pSqiVBw} DOU"f.內~nU[V:"mL-Yi*e&( fؖ7jZ'nhǰ܏)-ey> F)(jps%T $˿PQ 3@adV$hU'rf|vք#ٜ?$qnxW䰻Zs!Ra\hd7xIXHg1A4U?pdԁě`ŅzV!ˆcsCZt]z]7QnzK5l@p2LuZ8'H9&p$nJG& 5;qoKD%\R"W[Z@ !{;E]be(Dy zt,[* #"%ֿʉQz7tDHO&?d#P[݄ c%FWU8|H~hq>#1)Rւ'$⳶ܜ^cWE؞#:Q[y U$@5' s(xTX G4E:%Q=$A \;ϫC.HP ›7r)ZaE%O_\^S|yB9CBEBt?/yqhg/a(9 L]K&;xu.F:F3 n`qSXHGnjcN;?;*Po1@ JiI*\ IDi.  T3:bp 7hU""^&b퉃 _{ V)xL:+]ٹ cg}גc\{wJiӏJMj"y*eb1T;%uU,=pAQP#1o;$pAV{鳵m;Lmxbkxz!F#(%xHy*RfBJ*UTԎPFL%M$ 6w{UUPTeiÞ |[wc*MW'TŸDT{>v"#^\l˽G}0B翋\7_^"7Da{N{9pf)tw|7Pl~u=QkoA.$xH(i6ML2"-X?Ո _*/h(VҎHقz'Vt3D`$:rNm)U֜_-ɟ1_h57ƥйodTzHM|C:;i"Zps?Uڐd HCTFJVOk.#3\@.I:>H M$Lݖ;W'~^Hyj'J깍[Q]P<93i]"s֞B:W) 'I2E[2ۆ˯7#HVBk:uHhAߔ+=2T c鯲DA/ߔoARX ֜IGMj#<&Ujl40ׄ+ b0zH>YR| Wlj$DOkpL7-KdW#BfҜuB sNO*LA'ycq)lK?aVޒ;g?~2r˩ɏ\_$+f D7ns$D@LP#EW+Ɠ^M#qYDE!= `djl;2qS~gCv{aeK2"XG4Q S@c$B<5e6:Q1V.Yf# w[c)A=NhfF !J1j1T^h .qcdasߋ|}@LH8fF!O$D"q԰N쨍$1LxhHt!c5Zr77 Ya)lyEÞa*bun(PDC0k]=!?}ьO#d"يFCcD^q^("8b׋’ba^[OZ_y(pHxWA#17Q,VgW8Ŕ##؛PfzD١Iʪj876^}tbW$(0^gpW)*w=?Sh*hUY;uSQbwٓrJ W=,MHY+i^vw;BlQ[/5?c"nmG,7wVhȃD(D}UޞlX] $i=ycl{ y?Bqt kj4.DF svKT@*5rrjo !Qot.?/ PV""s#9zx}9/u=.kp?ɏU U h.E׷ Ep*:<.'m"D!4\ϛ[srZ:n9aɆG{‘i.fD+ HP ҰBκyNVN{CLC +~Z3IBkg<}=$h+'Ns5JOm]y#1nux'~aOLkuOuzjL6R AQ$nB eכښ]"w_TR$BW{*M& )JrOxר/'[}fPXR˶XN&`6 ba聧Hng2 5KcYδÆtB Y`ӐI 35P+;kIeKNČ~29s`?7Qv^3~Dk@; fa-.Jm[* F5t{cysj0dL=h|6T7;\wl6cFԌvG:bxD6P+ďI?_j f4nb0a؍35]UoqfDˆy[ xv*.l.IF =g\ID&YȄ\k&@j2쩻?l16<$yHV}&fϒp΋v6$hvjj׸7޷ުwجݘIbvTrG9sxd7]S&hd\+dcbNa! ϙK$ֲWh/6pR@Hs)wQ=IF"MRFW@ivpH~ h&,hR|EeLωQuɼjTIFmՁ;?M]ED K1*. |: 'H@dɋ+JEJ$^4>6j&wS7YȤ9wW]!o--_Ӧ5Cަ="\'V0Iqմeߊ("iH`+F$n 2"ޜTd7"Zny>C$FזLHvt{J/|O.o5Vm Bi#S[nJd/DĠ.(7ZWQ[jIt\"8 )Ho] \iLnL2_^ DfIȬ7% ?B"A#6-@"aZ+3xs-9E%WF{gk|HdFLB̧][=iMK[|vT#RiiY5wh5kxb{ 5[mD@.E%F!ߝKRC[** Rs1zj<5ʴCV|ˆ쫪KTmAj`J&ޅ$ր?#SF%$t ;4`wRc |MDglS,  _}MW-IBQ;g3"ztޑg"jH Q!M>+|H `w)ᵟG3K/#G~?"ds9qY,4Am{b %y<4H[ވFDJtL|6V0|H^_oA/F爤{,z.,2^?${e{1G$2I\!)WĂ\Yb7e%"E&JK#Ŋ&5u]봌!њ(q"!|&agQ +Ǚ%^H4w-zzp)g3.n}-qq=ƇJɍmRpm]݌i@.Wl[2MPVM{ieWRZ+ Q7ڕuԑA8L}N.2HƄQ f}ⅴ X _[%I;:<bhqR&n'V$jh%i/T}}NC+"8ni>\Qb'?DKZx\TҔ7nIpdV-5 `^ X~sg7$$@.pX] !<sG^{gII(vdsXuJ#.L|vHr9%-ٮ$L&{VX|E/& )CR 鮍mE #:Y*'6Oll8M6 "wmr.wXZx,tr`Мb$Rmbj.H\ՠNe΢ߐo8@(LAϺrp((o3R2-忕G]%iF(C1 FGը֯QM U_*K R (>a2~*ak>UKmY:%l%יJ,Ά8DyV=M->?:Φ8K#шI@)63PJQV64^ _2>5_^4>pq]4*F5(W>HV lA!ǛazٯD;p W(e&"ݵAn^)QHwXn nX$pc(qv_|[\ -o_&i(3 iHg{4yY{2= U2A0P.-灈u:uǚic[8m۶m۶m۶k국ڶ*hy{oJ7npG;`EEBѠ @SH$l:r8 [a% ~G5xkw#(&])4*y6Ju;)a@~3z~cG7Mpe{+#_3`]GǪJ3TXПd%<%Bыo^_[7d2LG#0v[ς,C鸰ƛsmYwbP%2y1ނ>f ?~ 8FֱHxwX̿w{([|f N.8M&[{`ͺF3g }õ~h97PbȬ6\J޳CN979XDRuraoO,0~[I֥hz}d0(Y0YK.N[4LRC69Pc9Xm;d"FYqPlXުDd2P\Y2T &fyHTqH+D.IU""Ć.Ych4 J(wC5IY]>X'mINKP {`+E.,PEP+ G a_`{q;.ء#6[bGZNH?oHEw@PY 2%<] Nh.m=%P,,"cx!9p)T7JĄ(`iG"x #Bզ5yO,8< tqοs !TSN$htF R)4Ra1$U^d8/s< Hᨼr=_apFHCSo%l_N B$_fҰ3@*DY@2'$I \<C wƙՓ%ŖB#"*A{5]PTg"U 2UZ" 麌@jDs"ho\;hrDzʶ+ka* 5{U7qeW3n S$VR6E# dn ]OU{ZID$gT3A Ao69OW[\`jWv!"h9R$Ju& [Ǘ"h4[1h ԟM$=r_uZ踴J]Hb+U>\rx88}-G5 ID{uF$Cc)5RiʓiNUf`sD6$wYkK>LCA[5]\ T+Ri _*$D~"U0痥/Kņm pkn-@$W^>FQ>Xq @6T4qA {F= \S- W*:}t%hMw[9#~w'C2#X8>FҌކ;Jg!U y(vCVnHh~A[ Bj","IwTsAxmkb B"c s)HfI7çh4c %3jPc VDl{Q+xNom2$2qD`8<8YD낓 D<ҧh4㍵QhOUs^8{RTYo 8I!aBڣG/? W>FPNߔa[ln7oK4IGqxDpp\ 8aֺK5btש`svq+H"qI /(")V"Oh#I$c2"ԁ)5]'Ąk40 #^OVn2>.w@hmŵmmֶmmٖn&faWuJLTъpOd/ETNY CI}K⹃_K)ن,'Zbq|Vk" #IJŎKb r~.-LqCܓZp't^#ĵg cqWϧc_ )5Zp5&w^ꕢ qVu=rjFT6`"'0F9 }S gq^{Z8 ~B*"X>lX־t9]0, WtiGVKEbF< e~$/IT9 c?ֻѨdvGX$~~Rou( >PDDY`>-q}DUwD^q?+\ v}8pmgmOuI3H{%'B)8]oIE/JMƩz: ?w]ʠ}LWYÝ]VDD(RIZB\kAܔ me}Z;96s""@n(6{o;=sXT桰׫]&("i6>ōIT-A'TLT]]CYԿ{*"i#\<]:~izj-βByRDDd\X |MKTݓu4=ϼSi. QE%H""Y 0s4Mb~Ζv1]1ϿMBg$,'aW3y""g q肝2Iu,>TDD8n.,'LyEAEDDKgHۗCO0>,. Q"*""I8̘vrSW}-W(""Zwm4wxCwd k}ꊈV AnlDw\>X;r]$?D=S*9[XiT}9|. }nhu֕scH`6~$sGh8kt:b4Ns%nhOAT#\& ;+""S&=_"".,l~X|6E@EDDcpJhL$,Dx[JEDD 6}ƺIX4S;u8w~ ڊ۰}3k:7R>k"ʫoF!MlFhr7.| :oV|2}G_QX¯ME/wA!]Y \ClqЅ›)L0~`V`eEDDSzJ}3`)""Ë.D8c~fv.)""[0SKr:/ \b%n[)""37s4sŃy$ M_ZDDęύ( tUGIENDB`PrismLauncher-11.0.3/launcher/resources/backgrounds/kitteh-bday.png0000644000175100017510000020026315224505336025016 0ustar runnerrunnerPNG  IHDRupOfzIDATxT\3\H%! NnzRKxu7#u/M!I5'2<*O\6js%Ĺ{͔)FnHAb?>puH鳒viʨHtn 蒼U>*' '8@9KJ頴{NU>̜n++:)V.wnIť%TG9AHJswm|d"fZ}uWn{CY{"x]*@^TՎϴZ} qAVC e\@_\/]{y?B-HQ )媌Q+oVD%VsՑt ur"%\80ReÇ߸~mV[qU)8mw[.~V@j/Jmr>r(5{T XpG#ﶕO=eDIJ`A删2ejhKwޒfUD޹[ɩ*))9HZB淯zWf=[}[;M}uC=p0zI=wx>cxfmpV=mCC ߔSd',;瀏_gCW'm?S9'c'U8#G^uA7 a @[JݖublMX593Us FUB]K1b0|5;BNmіM=jQeӇ2Y\'OQ 6邑;qā;m'*]}Wu(bC沙]*LwngRQT, +b g##|xW yYƫm ݎk#FH0ֻgJ/h >|}zӾr^>5)Ws >R;Nm5⨈n04@([cCeyI>*]c[5XL1[qI 丐LTn̉"viwpwm0S^Cy%O㖖k߄)Y,7UB>熖P":!J!7kZmsVti~d=gݚ _pΨP%\p|}T NqѠlC@.~kz6.jT)k@snUBatTߛ7`Av~e?;(PNDPܖf O1u9sxlǮGաm%d~Hlf[nnoX8@>E5U=}P 0t?L6M9^MQ))&lEah|ֽOt)!s[j-˭q FJ9(i2A8AC6ˏ7)%{AWVPOߋl:-"KȚWi((()ʃ|fΫU3$>Z@*Wĥ5xX?|L(̬=ПƦ5=IE.-Η_jZ(:4KaF*%#eVQ,ztaX&_(W`+"W (Z s&*}!ù(G d+ _Y8q 6LDH˦2X6!CCN Um9@ S4O4ԶJ|%[qvK(t}ԷVB^ *d=tZH1wp@ׄyj5HIu٭|%9l0S ʇnUUP+JsYl E_y4pJE+.#F?}I[D%\kD|XO+[vgإi=%AOȬZMUP@ofM=+z8w* *9K*uOd"&M]SDՔp%Ĺjhz><J*$Ms}DV LۣY q JxDVoɜdM=-%rwWY ߶rŠ([B\<^>:;E%tzCrðL~? ֮H.VVuuuK[7%m28k7ήF޴_=d%\3 eah".MnN$'Hݤ</߾kt,)ǫ"~]OYo OCFyqpY}lj]*Iy5dl鋈W>ZQ ~8+5#-G'S&[˩iJ"zwl k%C q \6sTxe_EL2#>F7_&O^EĥEiWFofe3:jq\DS:UD?>2SxkD"(!UD|N{ AN{˧~L;*c9tD;mV{ `+mj/͵ӭ<:E;ZdBUBONї n *@XW?܍YPB \67 Ǻyp'n~%uqUY??yu1ܫ6$joÇ|[m9~%0kvYPBj7(}1mtphA[T=.],{4E x'G>8NWxU7d@ Rrܺ$>z\S@Y-|h%*0")| fgO ѳy>mjn5N?UB#~tz?ܧd?{PBV!5PBOf (!u7j `(1)MAV@ ]34ݙAkJhe?gMXQ;@?FEr.M;߭[ sY JYQ {%wBÝdǃYEyteA(n%&eNM0@?zPO⸀zZD||t9A =" C&?$ֺ(m-m k?x%T>v<&luUe4q N-d~ * Rg 2{ YPB\7 ˴OHt?[^˨,sqBv9AwdA?lrO-MeH*3ϟsdknR?W8j0yrr|~h^F@?Pĕ㷯:T<-!uoIͶܣ%[q]*!?e%tWNOOYK'oLۑ4zEϙ)_' WWB}Wni:/eJ"ee-i@Q@]oũٟMIpddw} U.d6lecƌy322!sz"@ *\2y0qBXW=?[j;e͕q>=4EvYs9gܹ?쳛^q3g6,,i!ĉO*!85'tvT˶ 8lrh>{qڵk\KOMvttwycٲeգFSW@KW: %8k^Wϫ"' qx-ZVAAA{QQRBꪫ$v`KyuzpN&333_pa',_~e$M6Iڢk􃦄)A ^7[:YUU%y%mɛn)!DPg9/:YSDORPfSSOoliiTTTkF[Jq| 3QqQ^8q/‘ޒ%]3jBP Jhe~u*%< N;c˖-WAOɕ386! P@ uu St dMM<;w?_&''m6/]PB]d|mXuYwUG;+իW7?n2f @ 99ڡ6܏Tꤳ>f9mڴj%T~E;%US<(V>jyVCKy!mj*_J{m&[3C{"((B!t\B-YRo]hKVt']% /P]Bl !ƥ*">U^^sy$iȐ!4)SBd%0^?}tJIo֭+lv؆`8[Bc?Fj֬)!~HMM5RrQ"4M"4zG!>i4ӵ~)ӻi-Կ˛"F>=sһ()Lj/6mqIf~-Px=54444rS\!PBL2~CE1XQ;"!ax?-[<~54444rS Qh(<),8?S4?.C0l&36Ʉe<^0oa)Q,8JP`(N'mZ軕*U$  0sU rTt_8с|zRndnN ~_\E{Nr{`]PCCCCHU"8o`>`B %fc nBgQ~iUuS!Kq蛜g47mqcF΋JőeI`.ފ(DuJ 5 RP^cZ#U",Β!5`XIhPY<ݝdݼ|f"!Ϗ/1]%-۟I'u !д@(4e$:=reN[hذ… R5*01,Zxa\Tv#߯W'6`m&q{KBj2K&V!EȖ Lhy7BJ+ Eezh I qxGAsFHcj+| I|-d׮]F*EcǎF4-V;c)pSsZcs>ȋȮ^'ވ z&>>"FT2w i- /ܽhI (YN\ Khҥڀ{Rv\Ů+I8_~E &۷G{T[*!* jhhhhEJ|,?2~Lى` TP?q* +%I5gJ'ܾtw']]gyK2:1+`>XsAc7*-pE!DjE~^bT)g55yXK-|ˏoP !&&Pwb3Hy-%ՊgjxyYQ 5\obswT3{`B` n^.Ip$K֔"J_C3~2zH\gqIHHaw :jD`5PCCCCS6/lI,7.hɏ}8U!Ttήcҏӂ0t ќ0+&"OD*EG麒:l8t4j,$g6[//lLfzno#>p?i僆:]V.63" :y8jmL峠rnSiZ^(/6WL|M&.ZcMz!*y@:yu!+r͛7'lsСCFRreL2!-NȣnXgA Q8T(,ۓ鈻y|H8ZMg:W!=\K2 S6$cv]LZ~@^<([X&8}t-͈gΜtIRRԮ][3jDiss*`8L1 +'r[,/A^%;Q `o`-@k/5;c8{ LV/QPǪ}dT-ޑ5IvB<)[|`P鬌qbWunWe-`QDD(p 7M̈́UԳ|Sl6D=s^z2dctĉ{ꩧGf||Y ( :Maښ}hɽQ<]6X^Ȑ^e9 3Sh$v|͒cܹJyOghN8ߙ۷I&S-5g-Y5444*(zS]7WX25R0̨9pgn(YV-:0HOnSp1N[D"sSlNǨ7>|}f'Q f|7 Ȝ&֚ iب)2Sjgp$ޓvtj31aXa9}gX͟ D6nxͬbPh-X{ k###E2p@VX!˖-w}cc HJcI```Yw=Mn lSva/44xp+>|]0g{V/SU@cO JS[s]gx>FCN}Uw ̯\ݧҥ nxs1]rh(Ŗ&)hhhh[tk}h^BCAjȨQΝ;? 7n0Z߰a(^ض3z4"m.9f0(D5  )W4)i_m sl7.nc^gAAnfe=F*nرc%&&??D RҥK)Cڠ Sccc%<< `wش _~"tqFbɠY ,pͥxͨNL ڻK\ʌ|N3E}{(]!5p:QUH=eBE2d2|HRSS 1רQ# %!DwFbMPCCCFPl.Dcq H1 ƻVZIϞ=;yO .ѓ$8"@Tj0P؁D4fmL' fǽtN) ;$2 DF$&M>#՛ ]a@V v43ݵ6  BE10 {kgm5ttl:,_NǶD9F>NaݧJFcǎz a-`qƆ!?D\:zY=z)(T"L@Fldj {/+[JVD/}RdpSL\`dd-P͝},poVT4"~gZ!Bt9bՃ!Bwwe@ A5p:|~hb4Y‚5ڷ,u+6)AOS5JEG9 *)02X:qlӍ M > LAzx,tk1Ȭ#44SJx _ tm߾?Jq7;B<$Lup;а'}hok!0Ne^ 8m2 Sj-Kx-L@SMGӎriE S'+~OzmV3JʨF e2 *Zq?3On4q aHr2k`Qj>siU"0ق()B*TEMs5аs{/8^`_.ҺBJOq47+-7ϓO| a,?s21s2!x\&`JΜfaV4#)E~"Dd癧&ؾG,[yXN%v }]CUAACCC#/\~0$Xbw m\oWO0S<+WȎ{y1 gIfSF 5 jzȧS?SH'5 |Edq]=ni2OAY2hԌ.'el" t7•GCcy1ѢacP*9ڧcJ." //>( J7:.3x ! "?"Z!"c8zVV:@,?5-6tWIUcRNNNrMSU􈑖!l9#K"êuG8ihhhF1cp-b_L@7Y|$\Z&B9bEfF& Zo$`-&f\J,#ɺǚf)| ۲sNֆ,]nj&.\t'*I'˒*WU  dd6d^{5ܹ3gaѯʤ.lK $0A>tzciV PIuߍ)&}eMg^TYDZNYA%mx4_1bh.CJow!aB8b;88b?,LQŋw(T]ĮR^TGOg^MJ`L=6b?r]rfA͔5444l5t!„UT(Ҡ(D8e=8A8F[t\T#iԠ ͟>,]m0/Ǽ<_m۶` J0V^-͓c2 a2eXCɄղs+U[Id2$aL^6S+SVΖKAi@M-DnvL p14t Q%f( Y| TfJ` 1w#|ڃKTrFuDFB?`Z623AFQdг p-C]tU&rgP넶,L./zMfoZ$ w-S%GސX7_ZloDC4 U@B(v X_:]y!5SpiGE96.W'`XE]CC,Lz}H[i2:[<_3:1:8ݓZ{>0 \Q6k˞=74&m$AtG^Z%~!K>~P&0QxRJ=E hBDғfMGNVg:89 |G nB m,0[RXX+xE U=X7ICC E۶-?įELJ.MHY~}65۲g'CA ۺQ`63c[mq -i޶8 #F[1 L'ŒW*YdQѢE"hھ?EZ;U7E!Ο?.,lTeGDz\mQ)*EqZQ_sOtr1>JA=Pпr ₥~F=Ҥg GrZ1F]1oN>mʮ-nnnncu|tTb\Y_c=JZt/t7fBù544Jqx 6άYN-Vx_=<(:kF.iٿ-E;ؗ"Q3c./* `C.B*E} ~{7Rbt H7E:VD@5xmWpbFgRI6Rj 9'~vn o?biРxJ-˘DiսQh [ݻyz O?}C+)`{eM_g4-qtÇopcd֜444 ] ңG2걈NRu4V|Biޭ yaŻjyRZi%1Kn 8SKbb"W=o9d΁;@<2>$.~/9Pǎ \+jC.4^o+1өS1b\LQK9~qqg&:<9MD|)Х4o.aPl|C$D}ħӋFc]ga48:: .wn,Z# Ap6I ;9ٲe!WUuف@~7?'f`5]j7SNe\^TusOVReJJ{9520B0PT$1l̫RbT9_իq&|΁7Gc9ź+V= F]pp |6βutu^;t?zljEgS-8DE|D/Wj;L m"ϛ#ʖ-k1f:J )4hpn!L,'kL81[K3F/9aÆi e:Z~n +p=v:&tšb(] h 'O(;w0 G[~$y8rwZǬ_p|Iќ(iz`S?<6lЀ]Yň$;qě%*L֮]{rxDJb~͚5nТԎ 0F83hxӷ8AsܪTPB;wlshiȗ/#![>&ifL'E̴*6لTG1m|ܹ2bbe:Zf90u`rB̨>Da{k>%B߀?"=95(,Hcc(沱)tFomHȬٔ>VjFב`)BQ 5"Kj:C5jhhLTJbvM\snS%meښy}= !24U$#ʂ/ʤfp~[@ PFI!tz!y5` q9.Dwsۢ,,7KUO_̈KخEhH۶-ꐖ"t0%EZI\h(Sw8ǏcxM|猆`VzyV544jQ6 ؼqO|mf+ Ԃ8C< bhncFB$_(P঺7lZ .Aj5Xod$GvP7ǺD&'mX(ѣy?'Oq xCxz#훀~$X S#}<_0?rTZ ihC-S}ʠ0CuEHhtDQsю&B ՜^=eX=}$NmDA\2"Vmڞ;ULQ`0D DFЧšVB%'lk|D%VN"ޅnܸ8?)* .+ymFg0F~qKp;5pSh,9.k |ыpjDˡ3Egt+upǍ >=y\ F*0&%wh!(^ZE4?]NWw-~i29yQ(4X6>i͓LOѻ?l&N!owqGcc")pgb o8V@ 4!f:m>W w=<+iͺ0-zTF3P"W COU mkL&UTܼ YH~HhpH5|IjՅU,? m aܝMtD))Dx|S/1EfǓo~ _;v`r饗6XOzzN] 舼kS *dܭBMvrunڿ)X :N" 鵧=is٦3B~t,r@ԅa#ˢYժUSY]+ZwB 3W^v6hG M@.ĔWRm}S?;=:nkF& S?HH}谬ϥ1'E` (=4vjUGqX*>O5 6`CyR6 Zlk7fD/j8UNW@ł˸F1ЉXhʔ)N h%Nd}QGy2- MMLW 7Cc֏:"|B6<=C_0բ0` z ϧgk[nd=BpDdW?POYfP?A G>4Az_|/|-Y7oͽ}sŘ-ZFH(&ię9VXr.bwDC3δyvaͩa8Qs LZ 8؎C< ~˲BۣyTDn5$Pm8I 8u, 5[εIe[^C I8ޤ!G{NN H}%&]FyH-ēRG=HP +8>yum6ϐ>:Hb6ƅEUK%PZ!S5P_NfC3&%M@zp9aCZ>HXo6ItʜltX'rhNNk&ZY .,oJISmf@5`DK+Iz xE2 c;v=E[( hhhwQ! lL'9,I/l"`!sZ?3^Z!ت_mekRhwtZ*1ۢp 7U5NCTE64W8]mhH N$"&1SMIPJz[+`7'&^`Z7`pD^upB(޼1`:H(z"W5zq0/=u뭛'ǟ(\ݨVSzgRN8"EZjCQ\f(>7)#iR#GԙEE \K3j HO$iZ:5'f2ئp ?OԨEe==HS!P\{?/-ieeם╉ R>э!U0`cm-ä\#BP7_S $^z8fmV;ժUm'>ޫdLD"ώ~HX,8ы(L#(<XR 0"( *gK&N_l GKJ Wlnl[tN o2aw2ˈԁk=E=!c QˢdԀ^˞5YwoFHצ5޿օTɅZ6i<ժU[?(Z=CܝL;]. 1S(_|[\|<ƳwD7&բ!ew,\{׆ci-4=iҤR^CIaЕڔZѬ :1ˠ1'.:0L5K 4MDy{FRj"h[vн%/KSX熩H}KFVˊ (o8}X䲱14*З$ʰy9>I?b]9+/ҒlUW!C,Ua+REp" <5f16|ҍ}P#JDtG7(V2ĉQFֈ)YOsO~u #9 m%=)4/kXZ˴Mk0mT~z & [)$L:1W"m5tQtQE_eƌ@EF$jBgS0I$M$)$ћkEVkE! B5[6SSOuL X}G=9 @(!rRm6{nj3+^Wڒo ,=Cs |-*RnPE>=l#Rp mJbFDR;g` : Eַ%5 FHWT`!:N* -xWY|$yܗH Ucw:HT|]Rq k>ӟtnr[2Jjժ-:?-͌!r@ M_pp;쒮IN8<ҁ_V.]k!]l_' zw a@iOwVNX,L4_m7^5.I5h;†\ _z3P+szjYmUK{K]XZ//?Xfα u8*D4:E"@ohAF4!mB6@HJC)J5rKu\ۨ T܀嚸( `Yz- k#lZ&E߹ <ԟzʲ4k {E;bNw5XcR;N]×VKP{F0h)?!#򟖁܆_Zj+ h`ݕ! ?1Fi_mg`!c(? tE5@H\F뿊xU*PaHi r5 ~~r Iaͥh~y||D}4"wZ#1NC֎[ P}c8iU2quڀ9Cd]KzVQKaJ)/dsQO^FLi{4C4Vm+iҒ"{S_{bmC {z@l-kEpΥa]criRWDOFjx?oSWST_8 U"O ,0ȀO&HqZ4̦ PdNI|L8R1;C"66EzJRgr"zfEP_RdN ]0ZZXhKJ 9`7&wQ7U!KM5dQY׵KK"t'R[=?ժU '7w; x"Ro d:| zZ @0u*|PߊV$E$r![5ѐ&ԇ74ZY/z:n&$wV6#9QWiإaJYw'?6c@aX=:zCB\!f|9VimFjv^ƸR 8Mߡ}R-(MVQg@ш~bhItF?"(%GCkl!dCMc9"2@aݙ~t 6C"}~8p̵XeȜHHjNvmzF)3_7MժչAZ*rs|h||bюYQ9~EhN+@4.'l\7 $Q:Z-z97VtGZ䑴h )z倒zǹw(ؓ1D$K#\zUW6@"kkLTm]-XGgNL\U6z~Poo/sG%%`hUï0nz&ذߥrԆ( (T-E"(!ǯlDvM-C5֪ÈHEjs-i"4':{vUd|ovRoDƄa4 ;ܩyժUrj| T_{VQ Av2'Bs8pH=sȸ35>^(RH<ՃJZj^Co>'~akOF^iy<>|LL*3?X4jbENhD 1{Ǝ`3:Pl&@H}HеIM}n2M+vM7Iáb[}a]k@ժu) VRSP$$HQB )|ZRGUtmSm tԈI;[OC&m,Fh'e, nZj];riXP@hNJV .t ҂lG(bmZjW'].cn}613:ºSOOt BjMb)E m#⻧OVM RCL& C}Q)&Â=\&cr{)2yR'uл_E9jC X$-]ׅ䃚9CpM'qz\5/MKCAYoQPjgkW" K#/?Ό?X@HsyfY1 F4T.S6jt}2ށF[JV)3BK_Z.lYz.,dJf_|"sQ/1-1S;UN\Qw$ʑ2|GPQZؓQЁPwMzKZj]d1΋b&ŧH #hٱeCKͨZ0PGTHu[AszZO-αժU"[}Ŀ'*ZD X]ڑB@aaޣf&iBHgoFAժUܺ1%1'a%e̍;K}h]dRNMZ 2I*ԅHӌzш(RƓ>^Z1uwPhw|:#nyvSS b*)#ED RqznF>VkTZo](W|3guдq΢Bd2Hϩ^}njAhX~sV\qEmWV</B =ԃ&;}fM@M8qOszkzoO]FAժ=J-hbir[{.׾ZצͯkbRr00zOTB11ժU{NGn^|ФITZC2=UAAdDW_s~ӟ2H4~hU{dA:@R7 ɮڈ2>\Q lG@W#jժ=Sq %{cTڐ{nݓF!w~/ūU5AOk|SlF5 #NK˻RrzL|uQjKaOO/>9Өj&Rzy5~_4ؓ1 *60ZGV[ -{?:i_5ժ<|FpvTN|'yُ{;~?+_yYjZj__>wQ˦Z|AD@3O4/Φ_? :c/{WDm6S۶m[J)SHm۶F6؝mnlݨM}3sg~Ta\ˆB>p0C88&*UdH,]AАQX@xŇ/dƙzS:oRh7rrmk"F&E20,$~V1)&KKۉhſ݌9'r7gӮ Q(XA*6⣷Z}B,5ƯϤmx]BipWYAzhs1|+P_DnZ2@kVmqhusGC)(.h宖C׸XXnҭdʲ2b^>k]ݾS  6(8rM `H~k1;_A9UgvrO>DCS@Cږ{Pnd]rL[q"dy;4i.#&UpDԨQ=cƌ+V,`@'O.)RǟՁ˛` (-")  ܦ/ZhJ6lԩS[ӧO D AJU:>cqe ˥Gg:~WEtZ Kp dA!3vMᠪNp%<@s`#@d ׌MVpμȑCǓ$杵dN_ ɛ-f\R TDlE -bܾ}n8e]ٲe%cƌjQO "-!@sq~50j]p2jwiՕ0|`7a„&M4-[6ru)k]]])XYx[\aIEXCċkpЄ(L8 MBw27o^V3|0ʹ`B t*fΚ1dʝd]Rb3#Mvup|>` nBH|.+++y5ϝsHxOЛcnˀ)@ZDEx<Syb]sj9s|L?&?*=LH/࠲ťxt4-mF؊xO>:`Bx*b {UHڎX'GolEVxJæiTcTz|F{@jՒy打JXc w \&B3`A{;(">@|||RݻiU?WL qZ%0 D ,(.xj] )nkf%E4ThWatP/O`P񆞍n7eA45JN\veƇ "5p=Ƅ q̪ -# OW zeMfݻO۷hE`AoZ*MB`@xe2T3%( 2 '2!Zsd9IJ\eH` >>^7N6YQ RJԐ Yx1LiV\zhfD9_՞ ι1`r wϬY*tJQBAСC9|ʛK\{vD.e wH(@T<*](O XA_ȯV%4,4{;`GM͍je( X8ÝR~fNXbRR%Y8p\(]Ƣk-[|wQ YߠA K}Bnc8zDQ^}iP*ox֋رCf&MHr8rWܼS{ $˥ 6\ۣ"LGr` /`uC@`}^xc?۷oW ԖU& cƈDqџ<)n8زkǸc߅}Y bH$ /z'#HfTܣ;ڶm8ЂrpKS+ O%  KIe:~cs?m6 A6D1xMa>ڃLJ9:gx2gÆ8?E0%66bCBw|:rDN͝+ !R'Oϟ+wfN eY3D\yYVn3pت6̈BE m'OdʕRF ɕ+eg"䡺-σEh.̟"f3Gddcc*mӑ,(wE,QJ9D/t\M7ndd05a}rܳG6oV2Ӧ)7mڔ- [p{)T<}"mn{ ^Oƅsر ,ǡe{̙3;6l5Y] z@J*s><\yQq.f .oLÂN0` !*Yn.\XqMPHfڭ3E<>ɐ" 41|`Fϟ/2+ Jp/#p k@P,/H ;_q^dXՖ7ZD hٲe1QH/j},eB08E"ܬe!Tͻ:-|kM?t_Qt(>FB={E9vLV);nJ*T ,իsUYʯT 4 cCT:^p9rYF:sh[ Sd]dT ,@ p:{s ;dRR2x>Npqiwp$ @G_`AbLp#5t^+.84 ĉvQ؃CP!(D,ύA<xbw=ܹ̌CԼ22EԿ_{5(BsT^FGG\ ]F- 䣓\m>'k (YI%j5wDNY1R6IObÜu,9*"  Np& N]8/@#$Br#e8>rPдЫW/}c;r+ "O $RL8EѕGc9n RaW·'\ip6=KpGm`( w U:a_?qrGv姲b%1]-6 3-Zh[fÞ%| JSH*hyNq?jH- Rc⌬5&rEհ6#1- Yޗ/_ޛ޽ ܼy6l§-ŅYҿIN]dL-a28 uÇxGc̅=~6dyf {8NNj" x .0 gax(7d-–xoei>xrQR!@) K4%NѡPUd,)`_8W > ߑLs+Zi@a(= [Q!L&eys#ʌeFIR)$0!͆S(F=||30 DӅHSѢ\pTGRsGw__K+2e lbF(QDf͚J.޽{ˁIڸ)>'FAz)Jd^ 1 >fJKxzmbnx`9("I` pbvGis"tyV9|mqHp'PxB2fCȄc [lQ! <V u(ɂ Y`*Kq Z Npq px=n˖-(IOGÑvnR _> 3 xr^UPهr 73p(&.(LU"g9Cu}-6 Q1+G!jq1 ܞpq Jak^e^`9bg(L fG̔R`E/^d aJU%[c.nG(:-ECU5l68::/RJܵkkzKl=W⚫d+{I~(@3B!o FH%F~-__!-8@Q5DoVQ0 ?N"<8c:ƒYudAfȂvuFӧ`ѠAź`췄 ! ]s0E^9D1bNc=E ts(#ySxXfNAw۹Ϲb&`셙 %xs 3"U0;Ǥ .!"c!LU-$yi ظŠߚf/j<3E98#de!g ΀g(CGĉ5@ql΁lg ߈|ႜ0pB8 ]rE8>L؛-WBջ&F HDDpfDJxlsL>W5x?97|M^&ÏK]3 '˶]ـe$c,̇ NSLl(&8n0JɈ-#gaI0lQvŰk]<ǐjDCkݺ5{EJe4uaO&DaV b]Bo{ vx@o/*F(f3ZYƣB̏h 8z 2 Pu5 9Jbrm~}A%7$OƯW *=#tZ~P5pw9g\Dѿ]qBq1ۙS?Qs)-!HgT cfXmڴuaVp 8N<sI^z@ѡPĊ Zj}a,7N\ ݉ٳ@]!b1"4fkW{ԭ;%骯vsmI\bE~|/6uPűąy\:GNc;DI"9 ! S _XC+@#)8Ht,XƖ  t @a =rrk˝koO?Ix2[[.bBi#c^]v2DgsBeq ? *<SӁ T '(\y1ȽIy(D 8/D6dh /Ua+-WPqb_uBxJOŏZI:ԵȳmaQKoD ~;#\,s?]-PC)Ol>şa5SdQЙOa7 r#i)DuYD(ѥg' DZyO/Hݿ_pZgdދ\ny@B("i(T yBjB>c֯`!tsE<*ُ-AxL:V ,36DDWI "<) 1$>,g :-DUlM{?(;=IB q_5ר F2v7$i1 K  o,NȔ/-!0~9TnAdNGeՀ#9q|])'Ksj8 (l]XEbzХJ݌r=Ȕ ᆾH-PͥoZ=;vt]^}U&y`Ymť#$]) ;^WH?X} p a9@l}! Kb; 8c$<'Bwx Aܧ#"ƹcd*)|1J+n}LѼ$MCBB'!x|@M9sHӤu-dx9:0u{,y&:gg8XīI9`!WiÂ4 'WDQ̡t S/ g=q>v(ūBAJ0ZVG?Ts][Ձƥ# 7#HOy{-xDqb駟>Hz 6ֹ!t(fl)k?Ś CZ,X6w4$y?,|1`Xpnypm .,a@'ba[Tɡ],D}r,`D@vn  =dFD% ՞qC"枇zǍUR2VMׄZ,XFԩSR:t7$tQ`lwoUl EJ?X!ZlD׿Hi;gJo b`[#Y7iË$8 B ǶDž=x"~c!Ct[sI#ېGDYQkCB*@kCzQ 1&%XXs =NWe\vEAFKU!!H;m=O^r>RCFD&VJHO31 2C,cC)r<,;8|!LPBڷ57W/ǡL ,KwVN;Q[[Zg*XKAP@Nǩ BW:.=pIpY9?{ n6䄤"b8j8yCP8uG~{"o䣶Wy6Wk|jWL`W^)7hϸ_{t^T0ZL-k{ NJ[Qۂ:0x GxdD?mwawp,,Bn"267=֟{)5yx,,~=A:['a3ɤۄ !`>jHs&^bI1XgINǗ`Yd$ BKx#(Ն ;t Hhߔv2\gABVhxnۧh'ѣGK`jUx7~/86C( Ou;,X#Ĭ4}mR \ a;@H䍐s&v&;$f8„w#;w^p !)\?̼pcEdbéF165X_ۼ՞7@MN 1?PtF7IP]m(G4/Xd6mVMQ*@8-Z}BFf)c=:u~!R٘<ހfsy qd,քƢ7ǃkGB Xcr\P_pJr./Cb E/ǣl!8?q]7B5bI ]"w μJ%犾Ԇ|x|6- 93=S9&y 80PjcN>]~ 8zE M\-hE)=5ǤD+,Xѐ_ )$B=ehadƻ[Saq6I4G%2W AH@ 5kx_`hPj qo,88$\O=y$_x5Vy]8 ERyH~rixۍCd<'Z[ @K8L.b#IGۺxs8y*(gaA^~Fd-׊cPܨ4Q6e]k-T}h.JG3FGH`uELX]ŸB<.r%lIp@")z tWD Q82ls G }2X 6'DhՐRM;'E %!7gwG Arwզݛo nچí<먰BnK^j TK b O˳Hq ~2x'QBAD (Ső`<61AJ,FL,xA,֐Dki0Pۜ{ Ej4kڡzO='!#H$#~&8zq,dV)]^S=lMQbWJTERɨt]vt4 ]j xwzz U%g<4خۄ'YCj= ! pu t e &wtJeƐ}zM| )o{6`yAqI'yFj%-[Jǎ]i]&b1%Xx"c[Ҁx07ABxCx*k :vȋv;%A' ȱѹ ~9,~#!ڬ# j#Vx"Xm:J.ʇ-h#t)HϞ=Ci^0Wb Ws ($7d @Q}Ppw=0obY5ozTSʁfoXCDY>8XuFRW̅]TPWiӆMDlҕӸT3Bt&P8Ό hzzj3XB$2ƽ}nt୲ၷzo wP!XFVsSD(dTv-?DlZ܀mBM^ 0:2 x#ȳJnD %j:7AA`?3Ō-;SsDgFSMK!\(S# \&Soz6>l݀G¾k̓l "BRl{`VpMuOD:uvءf:sћ xj(*c.O^˴V=mTUD[/CX\0!Alj Q4wQD *Y-P`c`XIŊlRIɱU*:o߾3B.slbd N~ $:9?ӈdz6i]m-/?0E>V<YyLxlbńN:7EW* J{l#hP~O҇EΕ0 I3)<3dLX+a$g_~EZjRlBs`?QHӊ 1E$5ǼZ[4}/_a4v*R %$`BuB6Ǜ%TB6;X?!A;TRv Gi 9' mx>#<\ *F``2޶Td3tf*< э޽R #'" 3! ; Dn l:O l:BD8c=2NΘ/NVF- U[6INqa化XhC1$~?s!ЮQq5)r.Y6>{ ƦHy$\^v%"`o0C&h2[Y$k;DYxCU'Y3 .\HW|F ]M,v6hbjEYL/=c飏>Z]$kg-c.{ƅp5吿:"rtԆ\ꍗDG]YlPC6x^\ԕsR؏UJxФA!D<ǺD~gap%$d(7et;X[hq*}eʕw0!Ӱ xAtھ$ı"F#<Sy$`ڋz\{+Cy }~HAh:׺ UW4Qvys=Wz-!lZAQ)Ry0 rQ [!Gh*5dAkܜ|$!C9/0epD]z|-u5cc U AZC,S<X4WT2zelY}!)rȱ?ºN\k "Vybh2y?:A55ߗ?I}lժUrUWɟs+MPO"Xms ~T HIiK뮻Yg%4e\tZsxO[B8m^@ Nr?|Ado뤸FG[[o5U0ݢ/h`6WN );zұcG2eTVVJd?ҍ ՂI a 9D $OFw0V6K qG 'ޏYAz`Yߋ^xE"x㍅'|P0f)..`j"Qe4U AOBQ׵m)-hp^zɁQGTfJ~(X-_ɹR|E r1H۶mwޢJ5r6#(caG h Gh0>7$m;XE`MFIbMIW D"0xF}A?|Z#OYcܛz骶#$ۙ3r~h׮ dMQJ ^ ɺQ Kbu#6l#|oC,XrG !85M=K{@$ UΚ5K.oJ%o{bE`M~t #*0]ͫTSRB;b1ʟK)yWIg0fM eo)v_,96x`9c:@p_+P $ M3t+𔔔URPHvo:_Z1'A0L~FeʈhaMZ;#mt֭s(8t5uhgŃ֩; CbJ6mw}<@H)lua\z[ȱQQD P5@\ɣ{x| 8pj+^^A|4[oC!hvO<S>V#EymȊ`4TG!'N ڴ\knR@+57}7uQl(qdG)=gGk7p̙3% oCviY`Dt"X`f-(>U,TxGi2#$I\zϊ0AC#'ɥ,qB W>虖# A#"X'"D RAșmo"F7rVr#!~)sDAcUȇϧ`֢GB(Dٳg f iDGqE`RvTlRh&Lm%#O,X:fVsV7B ڨ|'DT6UaHyB Xę1Ds  !XBc> *7TT$馛.cI4 MFг"ZlC*,X=sG-(ڛk2 AMw- .l=xN G> 1y!'2S]3m;UŻe+v#ŀ Lʊu䄨Okk mdUܬxvCv[0_QFe}gy@#DȎP6X̭ ?>k 6GA I ^74Vnbe06Ŭe1\:P`x5X?wbHyD|)c9^1X8,]TȆ|q@0D ;%bmΫk齃!9%`t"*9(-է,,XЕX9Gx㍬1\MzitRHY ?I=u9CDkSvzDwb P1D]ETab5J/'t8T( mHXN "ׯ_VzoYXrMcag_Six>xDxJ'4 OÝ'.0dl7D~YIR/"/3pE` =z@ZAr"h/#Gl R*lA'BBV\jl 9r!> 9BÃF4 GoBs#ՆE'EmZXY"W^yeR+>xBbu)Y;*`'=^e!/_|;Cl9"댊&};Xȭ5 ~eYPOTo*6W|-o$ kx  pWTkAcƌ%KH۶mCb 6@2gHvl􌋝 ꭐ ApZɵ J>TԬnKQEIRFQ,Do]b*`%h*+ ֨AԮ];Yl0y)1ŀ/jmݖV1X>E !G7ktF̀jy#:W|@VWCYG~$eÇuP,XvbJ _vf=d_~0EZ!,SǎZ]X\*7wy8/7^J5椶/9&D[r}'?o{k'3.dWm-yfz[oU2yJApMt,Xb׊k=W_}:spHМ_CtAO?Y-VhȯȬ]a'd-meĹ Hi] (j>]E5HZͽsV>6Rd-ww P|b+Y_ "MPmXx"Cs]sK^˰:(ydʕ.yowzʱ( QvaK6äKGs"[ِѾm՛/Î預/iº5Ԯ_FB7*6j`> O1ʄpGBM,Xq)i8ΑC\  34&:|_Z3EIFDZP;3^׹b0Z(|rxMLG ~txUwMaa˶t 6&+<JhUo~b+E`jWLIdue{-{`3f@Bq>RCtG)ju"V B[+SJm90aӯ{mFc.{}I#r55JtseH6Cn* Iʮj!)Vc;E`jwX6O>lcrc "<"*v[XcQxȌ\""?C[/\܀y=_rX3ֆ :6`K- _2m/t- i,XB[i`r[ ڐM5 w}Wy7 W "X!*8'^ I D'"UoәlM}6cty{N_vaz+V,XB-{kO m{=XGB$iq xx,9$kdJN/,2)mݗ"FJ*|0 O [8ڐ8M,Xls[0~Rא3u=)k+͵oBH*;i6gat5y55΢u$Ľ"GG'5f-I;Z'4I"X` z++̺HB3gNzu؊BuB'd;Z|4UÓxoro2CDy8X)cyϏձ]H_7Y=*o@nh&志FB) b.UV2v5 ^ y$M F@@DW]uL>]K?HAfMw%#ȐB;~$N%kOxQWώ b*^~eI;/"}AB#K2$}]D<q}j.j3<3u&LAFA?OY(?)!;:HD#tTC/"^5R D5.z!X⼡V7q=bU5yI~Gi] }i?,)S%\H<${[N=T9쳅n^mb!lȉ3EogȄh :]{.'r . Mv L= l={$e5l`%#0K\U缒;77pO QGi7LTfSp{}e/B^':u#Y*4eх#MB`[l&P'!Cps)Rr[NC٦g%E}rd={z(tIQo˔@@DCAV܏?i%mT*PXtizcm,@{:O~}قCL#N6P\JD hd BhB%f;T-~Gsq@n΀Qc$a7}fژJq'!!蠤~.]bRr~H~8x}Ք8ymi=(%7p jڴ)W^q]JBg6 ez:i(Wб BK*WMBZ$Po8-Y ގ2/B#Piiπcv-Iќ vsCo`+ BK|^'"]ä /tx W:3 MlǦu`k6wWRA[o 7-MBZbj)+0*!D-<И0#U :KC"Y-7ߜ%qSFʬ;ܥK"//'wǤqB2bB2ۘF 4ݟJAS:j quqjJ؉Suc"if #L՝]Z=qu]w=3N8|M@˃ @md=t:YKNIō~VQ6(TN1w%8BDAPmhL BZh_)Cv7{~Ÿ*+34gԨQN({G!~z;N:b⩧t.1ez7}LB:b7ӈ$ n[%޳gO'D'|rd:z.`)4g0 !ꈦ0,.!f+pL.))qBDѶm`ˡuԤIw 'BBM}BuLR!*t,rB8'!Hxnرuoܤ0WtB!! ̴4*a߳ۻ͛A9!HE!D\~雚 Jx"oIQh(*bB:k׮e!rrrܞ{Y!9e>+#:A6 !k:zpbœf͚;Su!b'ؔO- փrM_\kp黠uO[V"3/M6tE!VZIkXN M1DQOi,4v9 Y,D]tCnzciz~$!^.#=z8!b: ќdɒZsIh׮SIio¹$gW2S y!&Ѕ+6&6fΜjcJmc_A&!D=ST3C5.H ~!hZ9CSuں5^5MBz~=aZT]ŏ*0 uȝwyQݿw޵>UOnfOu5 !![^NCr~#7b ѹsgY"~VfCmLf6't3qAh 7t': =bghi~VKL'KӖ&!D=mNo[͉ B,|M79,psl+yxє`Nh˧ IQOqhu-? jN!<3j4͛7~(un6 !1yV Dn!>}8!BM 5ǹ&`i&HfY^LBzA޾$U6~a E9SO8=CpMSTY0ӟF?LkmmM愝P0;sQ vH^\M0|\{G{hjkBs64= qA92N^ gkSA 5|kSD;GKQYi/&bv(CFrB;N9 CHptN*yB |4u6fBsV5s S!.$" [8 d\3oB4.0`k* BknpN {/B7a„>y[C+a_=5l^vV[9>&OҡCRNC +4ѷjw'|!DaQ=aW?ܽN ?kҤI F~O>IXKxrIi3KS+Z6ѢUn믿ޕ:! 5!D`zתu6+( K|s_M[ ܸ,4lҍ3 p9P!rȑ#&u͇Wr~ɷj D %_wBo~v5\SmA]vS3r|*]Q@{e)9YpOZt-r5ԪU+,+꟦nLBgz\v.s:!Sҳ⢂\+AYsBߚt_O"hn5eW;6Z 7 8m:an6'?7ZALǙ ֔ࢻpGK_vEEEN.뮻}SqAk%˴iVx6gh:x-4 ]qMLBFЮ[jnkM8щL 8|b2ٳ'|u8ܮT=L{EqAiӦw7"5(a)7|tM#ݳe 96m8悟4iB4"gΦ!%'Q|{=GDZٶK} įiզmn9Ggfh>W[~@ш8<ʊ3<*=o߾ cǎeT+Ѧa)ݓlU쪼+14]St|l8RqP_v7\ q@_e1l2)`nycq~eV) !Z褻|5k rkL~nΜ9nE߿?X9e ByM"jQTfI%𹹹NZAFRob-O?.ld B%(Bxy]6m4TOpgv7|3ä#(8RcC5@$)TA(ׂ:UBCMn"q ٤IZC'͛7uЁ4\2Wqp ϫs5AןXtI!R6%{J7 ҷr 5cXǩ}OlG%n݀Z̙3]u1eʔؓ|smLBjV>uR|bSOuo ãQjߌ;k,NBQ | k]Ӻ&!eU911=b#ptc͟?߉sƦ6׽{ww% XVQ˫:XNBm¾v=uחBT:#4 ״>eGVa>gwuW^y%'Qvk@VwӌT34A!nژRڙħ#cg'|ӈN'͛;N;찃` b*06bD8ABTJW FЖNDW\A+NE43`wM7\rAxB醻 qA`5I!*-lK,[LJ9$y y5`HrˍNɃ.5Ae˖'ɌvpRo3a~vMmeBJ4 d7@Z`oO wq̩,G\pSu!R1=Sرc]M2}tMQ'ℹ)|oF&!HZ%n(9DStmqsL@D'|Q9۷.N:Y@SM3yd撢Q<|$N>䟪b#{Rzw^뮎{SrB)%R9r=XN8Ƽ;;묳7|SiO輋~383hB6x)n4vmn}Y1yN 2Pr/‰}ݗZQG宺*{ƌpю1MX;UBGLLE D`nR1bG>CVbxJf;O>9FڌiN2dU {QA"{y"'ۙ^5MM!k(IvqBw񛎺?QHo֬";7}vdff:;ȑ#]uhРA|NL W֙+uN8%Щ}1ׂ75U~t +̫lBN %7fn"]\N?.]pk7L`G͕( K i뉋zjaBXnى>opf{fc{+nञacDˉ2j+ z:$c'Y@jcY$b]',{i!8);pfMcmU&Նi ڶ7ODڛfL뛄"voSB^tA5ڭm۶ҲP UW]YSkȧy;uLB) T B1*Ç7步 Ŷ%اO'lPF`oۦ5MB?q$*tX4F F5 8+<Ӡq}⧟~uò' 4֮Rg !>PR{b9sZ5F Pd]s ӞMa5]ji]KV5 !D$FAHDZOPc:F%ڡ{a#b7)\\ }GNBBq鯸 gvl!-..vCZb%Nͨ]vZx֣zx 1eϦ>jBBqo΍ RF øܨ ^:<;;9+p*Vw^CRgcɻr!D$hOf6kNJkTF54N1@Egnj 72}* fw '+ &Mk8)ȨmUTݦ|@ӑ&!-_}*4%7ݓN:/7t.6%Y\:߻:2z$z ANe:ЫW/L3w vmG\ !4޴I!ʹ4ش #ݲ{:Zq'Nҁ L!NW^yeCk+{o‘6>Bqii~8!fڴi&L.Ewҏ/f͚_ե!Kp:{y*cMNbS"j޼9A(mlk/VXoKI3C=zaT\ MLݔB@kYuĔ036Э[mP&ԴiҮ5ngE*52f!DK?P8TBx IvhP¯G}n NV$sϖ&!DrG(S-rJD0̊޽hdHd`duM_v6 !ҐMF!@favfTEނ.: _׫3Ŧ !&!D47{0'E333?4{7 ;wdA giđ٤_:vmO:-[⫗'[nעR瘄i|8vaw@VWWǥwĀP)$F콒&V҈b!4 ,- Q$ޅmȲԷmo|o&Ν7-3I,Ź{=;va'B;\p5gw!|B潘>3 I 6>ذa#Lx/g}smiQ?rlWEwީE1_Y; Iv6lpt'8_SEy34ڼw>˵uIF:ԴX VLBIE%6hax$]J'G헦wzP$ SR@r9w;& cda*&y}<=1t [oyLs[ɻ!t'B(h1Kې؋ G!Q ((C9* (E1(@!_9*6N/ &wہn۷oqEu׎wC37os駇# IqZDPip ~Q6 qH,޲QH)\#HA2d6`+ .!Rb'Hl܎0:L;o:,d݋M<9u>hsp3F`<>l6#C. v2N 0ґd_D+˰2pZ†Xk$!3GmT_X4h*jmlл.^hE? i>r 6KQD rY'1m2,|,X%S0Cqv$lhy,&U)/Bd61J'|Re>wC$TÇDL݉=S$dra255AixlE&QJԄCczqx 1Cpq$$"6^ح⣣& 9~Ce~w;wV2L(RF(Ra-&◸p.zLi8a ~_௘%؊tA.PfC5r|B?G۰K .b"͐a#q#,(>Gs;^$)}ޟuy:N{(?~iV9 /9{vEpC$SiDr; 7vDޥ%pK@2c1 f܀ 6l-Zqב‚AJ7nȲd#))IE /JGE ǎs' .W鶔"ŨY杅TlRcG n@H@6l\}+Էo_۷B8ϟ};t=8}ͱgn n*|*H} }/l2-aNRm6n馷Xfa;Moe*[~FW:WرczQbUj(f|͹pA!˱Յ† >{u ,A Dj&]2x /6t  K۶msѧD{' 4 oG.ʷCjfc(]#i JuUU w؀Ex#ALj~j;פS4ip z>KUЫW/믿޸q㴛o87#Q+(pXx#p#-8ei&dž˩Z.777:hީΉM}$؈xW4zl GrE[vmKx^}Uݝ]Rܹs& qҐ$=HdƪS`a3V+L€X+a>ҥK644IPGgM5v;~ 7z%9q{w87F-f%?w=6N@3DkH+HK@3$1R4c P^AG[7xcTӛ*$~*JbL:EE6\ g&I }0pp<;{`Xddd6`>TLn e18%ۦ_i);}zcYfi'T WI=~44 U,QT)؀i@,$a3";]J;LB. g)+"R9`4lJ|iI_rD'4tk]}-,'t$TG̙3ǡDd{, #tSPNHFWi$ux%򒏍¹x$?>\A:, p|O?)ٵ;TG ^,XzuDMmۦBxMB5v FN1@PU@9ʌTQ؊X.j,1#^^NhhiI`8!>G9 @ǶeO-Ud'ORuO.2ĉV>pgc*aA Mvb1E3s5EHCm IZ(F>Ij6:S|1#oK܅_9|u(- qgwB~JGkN1"C $T wS؅M؂$ ~-)Xaa[q uYը@);B&a/ )H2m>A[ 8].:^1c U@ٳ3|Jo2U!.C>k2|%XMF2:FR1=q&b)2nsW(ERQ[Ww]w9)))6xA[^O9t9DjhMBAqHFDZ  WX{×ltCHfP+rjۣ;ٳg5]_ щhMBn6%RYC1H{wYX@~ۑȋ]寬^]mbQԣ+VD]ewI(c=b%v 7 `2빎s!f"ӺkѴ{SשS'gРAμyG"*,G K ˰^ #!1a"1P8ΖY5V|_~Pvv@DFFs뭷ӻ6u*ȪCN28$,8Kx C$b4F0ӱ{Q^ MK1P+䮽Z1>oiEhv?i:_We瑖* !n~X؋F"ZIq$,NY1Ya#HpC2:*ZoN͛Zҍ@e9,hN{p6 h5RJ 0x}?!c10fc;5v1VqVEo w1_qKNcW_}uho~+'555".r%HLBUpaIb6" Ǎbjda PB"Ljl<b;2]]ΰ 8bXq7 7|j*'^C/ּ/z_;%ػw1"vAE^[e?\>Ad!DZY. 7J?Bc'y?۰Fg:68Zc }m- 8u. ۇ}-8+/#m\[$&\c'ȇeFCc\`!,,rl.$va#pyiblǟ` FZCItjXgT.r؝* s=#|/BƍFU8A&;95FHi]da:Ά&0 |ߊ,u_r%Χ~wr*a^l?Q&ݺus ;v,1"I*WϤ!%F1\(7FQBHٸciu_\҉(**bZGBH_Nݣ5 ݀$U][n/OF9" hLXDvxU[iPW'lܸQe ^Ow>䓈MjIȷR䣸$j*3TF$F ~^c8&`:V"p%(;t'QSWx|U1ب#ѣGGݚȑ#]FRDJQՀ RQKD18)haNPq q5~!c13'I5 ªXIm}ƏP竜wCt@5D?:j$%YȂaqlAȐ& X|HAdÍRsގUD>cXPc?xvF nq=*wOOOCj'! DTw p ҐG*Xka##-?荋q+F`c 뾗stۗ)^do[hf~7N$n*=oTE.곃|$V =tGɆah<~ljh^l5.3sP>QD tyqN^]m۶AH(--#H`hrBA 4!{2db0Qb؍<m;*5xb66b?PHt;X lҽNl ;ԝ@#*1aW1UUpH+@l?܅HG.PILDSϘ=Fhntgff:zH5 c5 \u Өjc?V ?6lRjq` b Uhҩ41#"Yf S]IIIW%!e(F%P镬$5aPm ۄ [W"a#-x u-D,nHLQ߉>}NCǸ ?0ɚj?M}1⽫ˋ=EMÅHdkh"aZ+~lTMFdDj@Yl˖-A+}ʁi7KJ- IAΊƬ]U& %6!i~5aRmj~aɒI`fc 2&tok6XVVn:"Rw4UC.=;3fh P7.u]a}/d>yduЎ1Ң_xA2p Af$pay睧EGs߿4wykq y'j5Q2RYztܹI9n֭[6^h^T$Uۺ)8JAP ! q)lyb:YLZCٺ(His=DC4jzΆ s:/m>kLp͍( '1=15Y?N>}, TMJ~?^P=! Hw+ XaFXQ01 8ei]Txݩh}=suw7oQ+O_׺OQ*Tά[_ n뇝*z&[͛]Q9tH:p ۰>E{*0v!&LU!,=Fb -t m,*l.`/݇ѐǚLjU;%Gct?JnJZ/i&ϣFZZv`FcϦ$vp:x7/ |ddC튚8D"{16lD|$7.ã`#.TBM%ź|h<·~(~S>hI+[!ώ$Jeff3Hqg)8l~"8mX=3B"V x듻TO HwR0I=tܨh=!ߣHFrPʰK6lDwC;fL&#i"ݩ0AeتSU(VlJ>MrQJA j2ձ_9> WJ0%T%ߣ$mMv ذs]օh,41t\"̙2e<%ۅJMZM1Y5$sN'\f͚wKh z2PXǝPsL 20 '†DM |GSKoUyy>Q۱cTz+/E9IH=RSSPU~RH-ʅA^ݮe$D2d`Aٰmcߐ;=j~;W_uVZ?~<$TM7i$%qx饗:N8K}~=ּ(M@؇y)l؈xG**K;uP %OtINsTag*IH}PwXѓbs\6*nHBz'Խ{w}ɇw+!?ڷogճgO(4.b5^ h =ڵkt$!+[.. q`QmGrTA 4mܸq墢"'↗^zIFX1lon6O$TUX1 62~e_ҦFo{ԅ@_}[:Hk\cgGؿgҥa{#D{p?mX1؍y:/#NGhW8U-tdgȐ!*nxׯmO8&z#ў\p&a3&Z$a7SJuJ={JVL_8E!wc?`&F_]!PM ,p 6.Z=N8,O~E}Zo}thqظ O۷3sL'ϫQT#' Yn۸`F\&1 #{ 51FW/\s)A"T_81—> 7| 9 Y|<a#.OiOB=zpy睐ӝ;CGu$BQ~a+* q k1kqvꩧzvgtvRa>H$qfBs;Ew$T UÍ$$&t q'55l djO$A> iA6>"* 1\G!2A-Ykj䓂/pN a#n9X@LZ/]6k,ujc#NnzUPՃ:u aQ=_x|pjFK sXGU=vg_a //7{5 "/ 0_yXU2#׎;$T?(>,po6l%$_tϣ;SVSU]FTi5 "/ i%;DRQDl \) -aF\F dFR U*|5ר.=|ςmOr.wnX ^')|InLD/{6Za@Gw=:r@9/k=z /:\r)>x;eV]q24Eo6 c*.=vap&` PcZ}ݮ*G%%%iq#7=Ǝ{,%0&v(p\+k*P#Xذa˰o(: ?~羧 *ݼШK_oTEGR7P84G~;Eʑ,dž 1 J#ieffzv?nN&j("I^zi`#lF>6## `Æ #a0>%ZO4\YYD{0m4Vq.ÐHXH? \EupIo-vlذq `rQ㍅\M͛ ETTdA_D& t`QX8p K%a 6j܁D .aԨQ=J}ӼvaL@OBҳgOÇ,A(LMD؇ ہ 6:,GN N:$碋.r~_HF2hAD&&oNKei IBڝj7||W6 Ht'lذQG>BJ5A՗_~x.~Gߜ>^naڵM'hk߾}j5y`ۭ[H*BnCTZj!OSq%#|l{ O@6lh!}o~EI *EsxԁZT<֤}Δ|A-D9MBf7ooCr7D5z<Q86{cÆN^AKw$Qn8.@@)Q9眣 ^vA# "m_~mÆI=ױnݓ58P;!]֫M ed=BIQ3Ym}7ȹsn-ގJ/p{ڰa%c:RPoNG=ĠƴQP'9@#Gɪgw?W]$]͵ I?,8 v't3ⰯuZ؇lذA !C]]H=M;%0}2W[1#<*qNoasXPIfUQoQ E?NvvPqb>Lt Ȇ 0+q5&%+Ž;t#1}iKpoc-PAߜ&z[4SeZ4hȩ[T80 (I=055!^ @dc- a7>Bm?6lhkq~3OrعsVV7aD;Y]yθqxh _ls8'lذW>Q.զEE˖-o5%'!G+ZwFM6>Q Zh0a>hD׼ .-8)Pmذ E?JU*j=Wҧz˳{*.. wB*4}.<jO#˴RPGr86l(`( |-Mꬂ"ivM\.\XOIMM ADINtSo|aH@gcÆ=p7vA!%Ə;kɋDc-^64Q"ҟv:> e)8vm0$Yxlذb 2QQr}r.xkh/'' eһFɅ8í6l)XGUcE"%#ttn h6YՆ Ne9؀47Rl≬$Z-A = 6Z=@ 0/-6#,6F ԃOP8~ꫯnPfVt 6"< q:@܏QMtWCM Vz)yHVwHZ Hlٰa`(>d'XNԙttAz{hL@9V/86l4G?<8dXaLD@z ƭ l|Srh'|ҹ[LD՚IIw{p%\ 6b ,3m[g #E9B}>C%w@tA3u͑T\뮻SIK/<[qJrN@؀ 6lP"/CID! [4V;t25kpGCR;hwvvU]/99YߓN9BJ?Zذa-#5,v&>zpas\"֭v0>ǁO89S}ԭ[evUJnH@'+ڰa#O`QcE6*n馒ys@rסCdyt饗zWee8>jwӧlbz0 FlذC q5Vt@C~ɓQτ9=rݽ{+RRR~y?ϸnpc˖-9UT$cΆ 1P+܌Ď$*m*"P;ǏMgnv Xls}) 3F>P 6b4@*Qc[Q 7 P}MH49+7AVUզ#_rJ%G>gYf8N`m1=bQ 6ZJ,u>FQ"0`b 6advc?Pv?*>s프|Qߞ}Yѣ*x뭷T{X'6l؈h[1)A!\8ט0sx='.c5̝:hAoQ+tq#.5Q]>|qFL=S`Æs1i ؽH ȃ ی3

    ra^bba>$g ڠ9"|c GUC2HrxlW61]tC(d8kxob4^ P܏q5.Å8}p&NIZEq8kTsǵ耄&;%6l47@{tC'hVhMp1V#nXUCXO0K<Tlذ#qyDZ!a{9cp$ br'dÆMDm7ybaFEz\؂YxCc'{B|lذaF >8=DA:Vb:܏^f 6l؈hb%2cVQ5PR#B:``^( `‰F 6l؈b$>l.~d\F "98#~#;=q.BK<aÆ X:"'.ƵC1/`+F w7 jL@s 6l؈h'_ae"IENDB`PrismLauncher-11.0.3/launcher/resources/backgrounds/rory-flat-xmas.png0000644000175100017510000013754015224505336025505 0ustar runnerrunnerPNG  IHDR%ŗ'IDATxZsm۶mBtm;m۶-m]s3i &*VمRJ){9b=,cRJ!ca,Mp.K(ďxas,jRJXb7G v(ŋRsn@xYhlhRJ͉YGuTpQJ)5O!FX5.bF)Ԭű7^G l^hRJ͊Y'gN^F)Ԍq62`3wLRJ#:@q"5J)f̀,qJз ЇQJ)5#> B=lP #K4DRjZb}hLxX'RSv>{~@FOQ4qaqV3J)&u@dž8`+l5sa9x$Gڃ5T~F7.$hP\܇ŌRJR >u/y3[>ga<R %(DZIܠlR*a$vu脋`18ʎoo9X6F2Cl>B Ai@ccR=RJGd9{N XnVsm HJPEC=x^Nh5RJFdA;FcϘ~m<" HGtodrPR $1MX(dHƹHŘsmئ/)|D⣑I$~N%`lEƤ;R{/+\-׵ I_Mu{Vw\H7 !IHA7@(IL^F)ԄFdu<hCkǶ~<[C?ǻJGZ`HB#_HIdRRpR1l65ֿ]dӈ?"O#I7$ImW\cRj/r-'4$ݶ؊_k1.>$I$o?$# RS3q(א'ۊ/,*>`2NF$p͕\}ITD# Rc > wՖS[&;ඵ- }8M2eLRHC~˚hGꩩfl7,_U7׹ #H|״Oz.W2+}+ac8Q2iXQ7} z xX#".0 4&ӈ֛5~G?]s'W\^ @Y4GRaB[M>SI7X1B/_ϢOo?kM:a:%z/a_!>pt5=I̊{_02JwҞɡLi]:>V"oy"i(RI}N]QT#a7$aD?{oWgM"Ev ѕ'0&[2 ۶9AFhSFiYBt1Ă0LT*EATь40Q>OP.4EF -XqIOЙ"ÃB襋` 0׏ T`Q~dO#H#iUAkYt9bAbƜ@M"/Dr$s[*`L<$aN)>ER!6F5㷶p*cb 1 #Š=I ThBrPi4:bAbFMu@EZ2Tty0& 0:TO ,q"F]^m,2 cHDDщvƵ_$iwz"ba+M9Eu#)2mOA }q`Xx (GÌ)%ف60Ti8li0Om.耧Dh7EJ!iD3Π776-eUNW&#q!BuA>Hw(01 0cc,g)ClbYŦh) p2Â0_m=^>/QLDGB4<8}; ㅪ'UD@ãOgeH6.Փ!D-?ctt>+;, H p%խvDt5eͦ)@I_O T # 0>Pt gߩ_;ZE{z;`*8H $=BeזT8 DR5) {v6Vp0,H H$$;tUT_Сj^ PV4!> 6Bh*D')p* :Z5((0Fv(*{"`Fv>;^QS}T$@y xx䁁97Jz֏}Su?5smE:IA3߼H4E`j?I$} #0l"1,Dk"O۳g`U;{ЇӻwΜ{.غuY;vmao$ spIߐcgHFMUڋf5 yB:$M" Fks(EΤ 虮Zw1ig"\up-^ VOH_K*&efd($H j"wLDz{dp.`3ϛRo/g`t %VR"uU#1? ҦiF4Rl{z* <\aXN\Κ9::*AW*!S̟K[I*OFȀ0 »%#gl͛JD@;U)Sh/8 1ls g@U<"_|7ۯ{W=>zѷ׿U/Xv N=%Hd;4II|E<%/U'fO(Qt'{iTs";U UTխ: F6(IzJAI?J?C?D1kJ[}n73EEoyށ=J@0i !PjxIe|Dzex" JcJ"2jFrvFR^B/PMǂ0~B`hVǜ>mru铦 H1<<]y%h$<.%VIG0O+Va %|iz騰?)5E5 ԓ^Hν;!&1!4wT# )Z14.W\z /o1PĖ2p\7Os4zFu+WU8P| &&u/c/@RRS Sa`AbwwByS0o$8'ho;\wb2L&.HJ/XZ#}}ߕJ%>76&C%VTq&]}SV/0~Ox8adV70u\y%sgθl8P%M(S"dg||Jet]O)ؕ@Ik{-92όS4 # *L$A:ԺBhdGkoctõ` %1J0i+cBs V.pp3cm˖xR)C  G X AnS ~F$я_'Ο :/\aN# x!h7\ tDˍ%uy{y3ʋ!$Pjs4k.׬'4]ݭ,Gn-3L9t>IKBn ~BPZ};,J00q-o <@*|RJ .qœI>UB0b@IzH%ٝNf_nIEd&̴3nz"]NW1]7a*ڱ;k ݼ;^6Zi]U@)Hq-Oo)YJ&TC6QOxBaA_cwy{@)WIlރ"MjHs)jHqX {vG=e$ɶn TYvKc_it-l۶mqmgZ2R8o}狗5],tǚdgfe{ |bEI=!;h,մL6~B)Xk7Vh‹(A rSp8_ h 0AUZ0*M//C t fv(!0$!&hk!ĔaH\f>ECHbĦm+=-6%V R*)=@ __r[W8џϼU$l6˞XB24ԁID(®C X:7o,,@A˪WVfsSăLnG$o[O"ơCL$1Έ}Rb 8@$\BJdSbf,bqC)8}B4=CA&-D,l%z*T^\Dx:`15o#}1m GSX])HPzjSd z ߊ}p $q8ODdrHmjB,RXr& zDr Qb@"y 15ԅOEĸglQ;]xN,CO K^f;ޅ~d2IEt!=hbH2PcYZkT@ŴZ?+oLgiKEc̚~jbjq[dVVV7&6٥Rr=Ƙi <=R(kj#ϾC4C\(D]>КfLЗ2SkJ$cir;A>BZyM6̖SyosuDñTͪj+˕W_/\:ﭭW˫P8&2b6WqenB|.069MNRdK.Lk=gN 5^~.]Cj,,q%bhhwgaL?ID)era^wO# iF k$ (cД4HҔ< CrVy̐ |ŋ@o]_s"I<Yvͅu|TBQ+ik4\[r9LSo{?I%j&B.{a YN_FxĘ,D7W$FL$1%]vV0vK$+x镫8 uG JHH({ ]9QkK5tzهs|W Lluo@ͬyEHO|@wDL$1}=.p"^z*n,Q9H ]sn{.uaU-|jm{8yW{M`=Nx]=&PwE1b"1*Cb?m+"jE8+ea9J!^5l#Nz BﺼԔ,*Dp+<grN;~|_!-@~+3j]Y@Ў*ڡAK  @Ð,:ӭ`\Rc! = 4AܜBwڗ>WuIAB m@3qIl?:) UkHti"1b6Q6hJ)za"3l%^I$CTmKFvhI{oVxoS?S?o}7d2* Z/%PGC1b+zr F&^tꈒ 3|OQo) :h!8Xy6W 51ɠѨ7Z&wi193uƪg."ƽD$1I+O*"2jM,( Kx, @n3 $p|4,jB kET"/ĀQگx?(z#4taQbvE7z ZH3P=RD,ZkR*wEU bA{Hb=bI"7Fev ՑFA!SL䀼6V֪X,UBQ,LŘ[c.װ!J&c'0ԑNbyl CcZIBL$1ڟOGPKcq5hJvfi:8Fdu]dy$0 w[ْPbKb_k#Q$9qۦa G/FgG=qI|Ϧt_gfa,[&F V+R!uHx򢗲HQߖ*o+,ضB/@^GJ$12LZy>[aUQ$ hW N?`jŎ7H_^Z}1$v.9{__c|6Pdl/K*,I!0Il6;،aR4mZ$,Л]F۟:Yf4W1\kWZF}8Қ`: $c['!q>[{M-8 Q(gX)!m_67JpC' $coTkL0;g2Tu Wx&tptR6B(-yM(> 1"_6pG?FY܈KUmL oED!FJљak%|xubh` B.|!ZG$KБ+~Bx? %;zsO.7eG9CCR&TdžN%`?SnܜÐ9bB|o>+noLokld'OL9ʕD2r=㪒\^_; ͆:6&Di"&zBtL.(Qkyl2M!>O&(N`7u/EH-S;֧|З@$٤-ulzmTg/>ڢZ[^828Kk>%ǁ%1Bom~XmbDod'&QuotgFleu `Dcrbd Wi6uӐlQeYoQc~ ~Bj,(gsm!y>D18GB{67ÃE 𵵍sd,i9=2IX!R^Y)E=fZ3jj$l||{xTrՊ=H3< o=Cs\z!z&;dҵ]Z64\r?Hз Cf gV=%ץ7P&B>C=$rKHw-F^ J׉[cn˄P `x6:6dp3٦R*2+bmv)yVyyY\"yp\ ѳ[OD~o#sn#fr 6Zmj3jz*bDrIo'l1ɤsN9C&c0>6 |bIBjz( ߣ&!#wyڏ$ @"6GGquvjs%oTM: $茚}nVpxnt+HRI NB!`xji(z 2cIDG=^#B2LDphL$1ւ_-C8@X[{n5886=SAMN aaqZ1+WgE"JWI"2ahkp4N9imHf9[Geef ν@ (P?Z6}%D٤SH,R C^H~o[=CȈ^ S})mfFY^zW,G=LD/HDB z Mև1䵗xhϾI;{>r<|SSW Y66 zP9~l$' Y`3?Sy-023CZRIUnȲ;0EcVgvlJ>~eD`L |zS hHвnAD<K"A"&O?e7{#DN00X/U<^&xˀY ߞ2鍍0?:< !(PFTkl#%JHͤS3(]C&Jzstߕ=7D PfL2)HH" HTKKH ǧg0_wZoQpnq@{A- ulj=HO >&{sfQ-åخN֚I\g3meifIj 33ebj&rn5`o{m$87jD¸ڴh ̝dB2IF"`}y$3X gtf,vGҷԊl+Z{3dߏ&@v$)Gl$F#&Db0?QZg 1f82~I, l_h8ϾibGv Íedd|A;0*lyzjt $/ aNdzrNދelu:^lE=`@XPu7\N3wzL$խTo e$7F!-=^ aϞdNen†,@l걭e$M&" PGaѣF<1/,{bg>}P6%/cpYϡN"iB"kYeCq[Vnx[k}}M_%1쨉! _6T:bs+t sq/ӧ&((tSvs gv$$Kw=QXAٍXtJyԏp#f6^nNN{V貒 왧~&yw@R4 63^Xow}%' R^b,u2*Mx@OL0LZz澭1Ia+gk ۊ?AT.&YLƿ[xB2#>#N͉= qn<#a4<< pB,tA~C PtL% pIF0ͦ6TQѢx%8s,']?7z 5싮ej p3cƖ ?Ip50wڸlʫ[/J偙ezn,Bp_ȍn/*3dS%a[D}zA7f FR[/{av<іL*EaJXmi<NLb`TT~"~V١7绸- sKw/y$mK$r=͎vd#oN|QV]>O~ 1C3NrRB]*mg\x~4 60p#} ~#&_ѡo&^ Ty/b-W.9YXZ%(`Bs2]Vwjoԯk [g+UvzQOǖDzPM!DIuI-?I!%!fY^ 6Luԍ(BD%=zlb!~"&dRj0 '0"ڃGFB^<|>SJAHD@U:3 kL^4P[Vgxknn#15;örc /o/^m;`HkV{ېڄ.b3!4-}R\oGg KI->/ė_}X*Qf5JmҘh-&G -j8ǥPxg=T(;(6DL$zi7(H1YF60N~XPLmذXmh|<cVH&g3u L=ގj-~L6B{ )9pQ""(>6Fr]^$!bEIѽ#0I<>{ r|JK> kb߾8ff0_Zli[mq%b"Y]Z;T90$U jb`b~?H,+%VJB XX+0Qy$2j[EZse |6u`ŴRHY=H x{7~5WAX&lo^7+ͼ\{✒sg𼐉رn <c/}2맿o}ezV+5eff,6Dس8mDwWG߯ MNAwN.[SvH&#L% 鵙)e,;މ+(iahK>WfmGloݎ؉Ź8s|BTBV,ʦ| nϳZ'GǬUxV?kvgu!v6f7=0?ȺVpɷᙷ? d{C%Y1~S4l46 Zɥڪ𠘏FL$0<3f9[f5j0Q Kvx0*{$N~Yx d.kEìM»72^*CȷY>o2m](Hr-j '>==b>ÊiiTt㬙'dϕYP֫Y.ۯK^FN^YN$ߵJY!E|_+fff6S&qnŶT^ی@FAH.?&6'Q([+̘nC?{LO@BZ< ږ*/WNhٍq\ ah4HZrϣ-ߗUP!"asB.TY;+%Hz[2i A6#;hKLY۰<'!":!a*nvO$ğX__'X{~ ۏTX1b""x~S{Pt 2S%ifs8c?ѶKkL:BRae}cԡ&Ν2?uRK3U{fBpK2E:a|4cGXu?9exFߒ0ط/;"7|w'6e5y%ٴP$emrywP)I 3.]qd؅8ہ[xش%x ,""[|MaRjbwZczٮLUUJ1㷭م/z Hy[䱂4C.cp GĄl*L8%kuo,Pnwj;o"TE6s既`٤Վz7v$ph1b"7]+fKۨH{bEw;gE! [ԛ(2o-CfFYvYn*bVpiBe0^|'#+ Lgxӣx#ޱ5ɕTkJO" tp+>iTw9\SgO0t=Uecp1b"*ƲmsEK21 l0_<-!.\xߗtR)ȌޟLlyc]mވ\8@hsK,[8^"ʡx9j}cJC\E݀%,'=\6a[+s !^QO}U}4pcmȒVbDob#)3hqﴍx"+Rmx1{FRxxTFt\db νBيhڛP׮/F31vP| ]𱻘q(ٴM|Ғ~u_y rVY% ֪lϘDCrvR@xi I^OBfxS'YmJ#1p-cGJ&0gHvC&Eτ rew|IdS).Plt3ɉa>9aYvm'O/T$G^uq+v$%%!R_rH !T\kx}Z*7H*B( kyYqIpӈIH)sH b^Bwd}cȧ($S҉<崟IvR$; Hj}[7RN)y-dž'1Qk4Mb f^aǟ{G^Epf+,4˿[NMSAHrխަfBZ4yA/z <|[WFctddc{x:93X(B 9 ȴ67&ccjl% FkMI&k4GLiId Ԉ,w+6s/b8N.|އ|x9}H2=3?oٴ83?q|w3 ٫"O[=w>u% q1IBC  q0! _֔ Gyy.$gqJ~$#?=+d)y=I! <(dU5_q3<0lᲺLm~5 5Hsx6 p̤Ou2'x@B.mКD?g<&cL)1=d3[߸v0gh _Ho}[͉HOItQSOϾY=}ȊJٮͮ|1=MC<1w]~$C)˞mRsc=eDVVRq񫕊ģ4Ol݈4ɉ[`?$`>``jY#?{Z6d*lQ=#ƕdQ ΆԖKE{ll'|>d" @u|4 j<-S$ PgDSM,"7<)N$9E\牁 [>4@ic)ЮC`?;*'| kuj!$*J5ʜٍFs >|p_Nu }xȕLG t1RI~@`P)W^ldsfKUR$ 0F30P A$Eʢ(HRd\ sH%Ŋ(]wڊ=Dn+0?z3vs6LaDLK H_?2C\>H&dxž2$ U+,ߗ0a͍\d2ђ`^m;9X\)&a@_< :.@nM4|`1x!ç?l2,$`-8*&bku-nPE?>hKQx7 81bYsщ(rQ# Id c GlƖU)gOu <{e3ᴈ SW LF̰Cꑐ0O%a賴Ւ M@TZgK֯b8rӓ8.%L28%&a nrb۪7!8 ł9sz:8w=Hҁ6!7ޝVQQ'" ItgVWfz̔/,[P|E(=x {+X Hދ-U4DсIK{BĈdCmMP\q97l|<%6* O.cV  =x"L[4%=ej C@4Zyϔm]dݴۂEcr1ls&T!,&&==Ed$Տ6M:c8A9"bQt7l7,tP0T6AN'I9H(YF~eQSB,0urǬH2 x(Dd?piĈdHx 4ܲAv ,I"g'M<.DrnzlJkH|( E/{![AL7x^ZXh ti: 2$Kφޅh2L"g*L}$ O73ělH""Inƈ=!q655<)]DzHb}00*df0)?RճK1ƴ 9zNgڊv 7d7h@l&yG!.BAӓŏ^%33gߗgxBV?Ot&Ɔ}Sr<ϛM~'E IDlgY#e1-roju;DHզ nz6AOkpg7X"B&k (r2l37`Q$DXΆ<(EdPj(DډnD2bYK+7\s;Bl  +7@iOrvbZxh g p)M(t(^PD"nq0MHP5ِ]f83ť5VQ߼"{9} Idn1|חC~<3&*hw0sx/5:ouK%)6j%3c l 0Y \<חl XJ>G:„ئd)^O6wRH{eId]@"H&;DYmZ0WzĦ Mv/h$$1@)P9Ðci"<'mDm}<Ǒ #çC !A<  !ޜL+UkvWր*C&0 @v Fm21P9C c|Uf7VF $Q`a2gXHo)fpIkkdba$ctu/.n4XdH+_r7q4.($Euv ɣb#8ڒRp:j¢:) k^2ԁ,Ld\`AIssg1h5X_\ҍ`mnq5`QhrS@jΠp_A":dB]6m&j2cemg߃zqofRL"a Yf+ WLoG=E L ' p޻Js ho|zV92;`0-T/N=h?; yɨQx^AHvLx/ n"Ú~+g$5R KȲ@*&=A!¼O((XkKoaP 3FԠa!/ ,P[Ly]^^9KEG%v[Hk3$ G3TY[䱇4 @}Pg0l/eJwm"m=M}C>5thpsnVbZS2xK QONJ-\&:ɾxi& #BT$.UPzR Q^YVShk3X_XF^\'lQx"7y7Q{a} $ &6QD'fO]ŋbDXG E^cK7Ha S^%{\zP1`O$IGo5c#,l]~  q ,"dnCBT-2ҝ'j +ubuv7<33Xq/FI1'v-ͺ4fqhka~#GJ 3fJt (aea$֐4?djZmRUʾB7Au̼!Q'A/nN,cNrZ'BZ\Clcpjam4^5ʋ)bDL֚5%yFxF^ԑHxۡcWU;G>Ɇ{33 0Fc͍9QL!@gΜǡb̳jnyJ7 DAMH!-7"Q›2ߺ5ׄX6*87mvV#&mbZ-D*l8 VW*kO'jau&.^%|G# [3!H*0: s31ԃgB'PNLvgv$B1]9="ÐMe%0 K;#&'p!^0ss h5$C4-ο֠$mABLARl_=0DG&qD&gb\[2&v[R\a''8׆1[O@{If f4FOAB[=l#qruDЮYYE. ~=㬭LC{ Hy[!Is+6S2CDwc6.r@2$2шQ5%"쬆'[R=O0Wc'ǃ@ {ėFMrA2YO?mnHe3*D)nzRlȵY[2YuNZLj=b53uƏZcG!B4ry 'WMHbI"2`4@=nJY`{xCS㩩Ncq8w SG=|(FNL;'$$uO Dz cT_P[ y+ #HĦpjWn&J)L\ŕN$x&Og#oNZD&ʿ{j+uq]eUASQĖ-z!b$Hv F|Yӈ{$k%(o 5\1KgMX(٫\ڨH0BRI$pO4:M$ޕ|dBjuSB'5$b t&TfѨo- FL$@#?!3 i8=iBqܒNoa[Bv5vaŕ-ޕp#2I K5Dz؃Z fh~7Wbl1} %Q`2R%h2Uu:8_ܻZBD!=FTOMUߞ\ózbPIP <'B"ZwiS{1HN+aP. .9xmkj2MwAV'DpO^Oc.ٌX67GL0%8صa+-Dxa6m^Ht㶵I:<4߻1Sao5pmf"!mT2 Ikq_59$hldQ-#d_cCˊ#2\Qw9#KA5ʆ@!ɥg16.hbD  cXRPk_^Ζ&B NKH/1Ố CZˮg)Tut<&LMq$lYhE7\ÝzCc05@qpH@HrBVJhtԖFm{>1 !l'D3DP8e[/aj7+bU >1lSh!V#,rKYް K{SO[Vhd2I([ˉF!;!iCe@oIp%(+(W y>+3!k%NMZ qe?Rv>w{c2=z lu'TaTFaAi <̦OA g8b.#zl}Š#pAI- \mSU}}#C"vmX͉> p&lY]1NO4o+F#v@V-rMOPPlKY z-PY]+!w \Gqa Y("y9z#B#i )X6Z"FL$O=ۂr, _{75TVʬL-TFB3~<[4]y}~2!!bv3>"VDmCkNZ4 MamL%/^t.m=rBVWtt_4ч6)fkEa8>7h%`s@1b"CX띈DɆm0j 7o.AHZIl/zqHD7y^{.`td\GC+NhbD ^e% v4blHSHk^Hz1H\kjxs G!u.CD08'qծ&zTCM|HЬP#%6c Y[gR=u\Uu;!p?J5l6PyKe^P(Q/晭%G!Hb?ɥ(E8 Y3A!.cn2C{$[gwc/PBdrQDbE|7NŶ6S H5<3Z{RS#e=35趝޴=%FLL":d  ivDyn`ttmOB HP! {. fuI; "Pp~c zs٩𹓉 E8zҰN>A"e;Fö3 sz31"C{$_BbtS3.R0#_Zk7氱Qeu]2F)»Oq]"ܳx1Io1-ACkqaڎG~ #E Ź=gnC|t*X2uMŃazODLg!14HJ̊vWRߍ'b@R<1S{O{+{$ 15i;O/$4e\2Wg  K BWqH:Y|x{%Q던D ۡ*.yH}^Cb?1#GQ♖? 0L{/ID:[]z#`ƟRvo?A#9}3h7W+h(ebj { x7}`X+a :-lݷjz"1:COd |QL$_R.846ywu*6ekDY%3C/q]L6D]iR(c.xJM+p.!]h !nǦZCh}HO)3>ڿigR(U5U jTk F ڄ#9= l~YBq*V Fvt]'n<_+Y }G$yAnA \ W֧O}WIz Иvql?e |BDAy\.Ѷl~Ƣ.aee`#@,'B=DּETWbW5#:dCfmވ ۻM2m!_'6 %yd8F<^)Ti MOq=c6HOTC'^ōeӨՔLXH0oZk6(Bh!;F 刹c/(3ݦvj$>Lʈq1g?IN@ʕd(7cO? U-HN&L!F+e|ۗ1z=1D8$ʋ[#0evFMD j~@sb1}_8!ʜ c g x@lF'$-%q>gdqzKQ^#1a6w'gKkEw%:nGQG]5~3(J$JxH"},9;6sg02gW.óxcH&jyG!!!B.|.ՙ7G>}\B3}$bPReƖB @ 5ƂeC.0 R$L&;Z{cĸnlhru=q{B(GC |oSOa(n?]^\"33+/ߗC816q1!P )L{vވFu͚\Y!,>;'*n%kL6.9~;0`~ y,:3wjr##H%D"QXGqVǏQrFguJ^SzÐX,2u8CaG.)By%PZz{)@ s/HD֢GҪmi!u5m a=B!Oom ʋ=moı){Z> k %=ub 4:FbzjW|K0ed!-$& _NsDlja ҈Fik01$p5#J+PPXȪ>*gۨ =ʈ @NpǧGE@2 v g4H{,)^Ϟě>G4fKDtA6. (Ȕ J^RB!V,A}Ň j_ _%ZV=]ƳbG,Gtk.Þ>a9O (ruTAʲ3" C , olo]d@u=~قdms,@T $ڊ0 g(uo%#YFy;^q ad4ׁ"^Q(bbD>0șqNW^rbHA0 A.Idxº'ǹl9l9#[ˣ9|?hJ1` hcH*dx˛D1^bk0$56bjBP9z7'boĞ`1܋]nSdoMGB"r܄މ1F` JG jp !nEfLX~a댂9 u+Dؘ1_@O;d-OLu>jIߟH?[K!H"od;&ήEF,t:CrbWx#Rx>flIBp8[H cI.24Cz['3 =[!#Pƒ./Wn^O>zۏ6Ts?;a/ʴߡL0`w |'_j %kTݗvh%]sJfU׿%?Tqnr$'QW1a^{Gk8JAЫyAI^V yN~j amɆEc{^o{1#Ͻoo(d$c)6d" 12+kr츝It[D"_{B<&&2 3ƻ!:,w"OGQlP[ك,Ev8$c@J* XP&2 9=g}ˌoxև3S |lz OOm//$Aru~S<X ZU9ߐ+}@O$׶{xM'<+^QFaOj>~aB'Mn]utc޺9Wd8V砗!2'jkbI#"NO7$b(<xgߌD2I&̎QP$T2Pxu<4_#R~;"1rlPHrpeU.P> ucMZz%v<[*Ƙm6*x%lѳJ}=ěI(1@S?,5Hr{~ZYn\PhDD6fgVD"!V=?~y S ±1'"X#'29>̶B*nJCn"HOː0xɈPxؼH_Gf#(s_8ʿ]f;d53.}[XXXHjc<`ӂvaQwS6WXkȯl$x#0K+#L1^wȰ'spFF1f$Ra;91b;$'wk6b 9CEh0`˄q%,mwa2 Q2=D%#H&f 6,_)i/;V8grFdd6mK@Lh6mw8V OC/F>+yGX~LByi@ p#O$;MZ@"!Y8cIhWC""T |1?ktb7]Cy}u{d ZkKP/Eg1V7YaK<~^sӶLqhoO d"|BDJڔ0Z"_n`zzjԠ+PxL;Ac8 `[\N@s"@W؈>&_evA"cIX" ?GXg{7=_JZbffJ)S[vW؟_켘9@/4W6mt6梗H<^aɷx>OmBkdnB9O 1o7Db!,/ۏ1Ƙ53<>׈ YlsdҷmP\3)+kPj ѧ ui5 .1A:#% 5TH$%-a^);ݽԩ 9QCK^('֛l. ^k Mh#Re;mŷm۴5JVǻKnfW.\ZR~寚W,F&VeWc`먭.0HfR:h b K=fkə4.;D6W;oXeߊ>{po-^K5ЫkW>.|3#+W;zkɕ˯fygnbKOa0ZuxZ0 2AL6A!E r8NFLM4&jX$t+ mt"TE wVL.Fϒx>"?_#!:u]--YS"89i2I]ÂPQjN&>HVϽ}:_kF(jsKXX\ť3_r ܔ^x'Z XCbX?Y?bu"=cnN&򅍛ڗ?tu3JoIZ !ϡtK4(hQYAccv#w":] ejK7R%)'B0f/"L}r/6a-zD'$+?ˊD:7`ބ Ht]G{#}@jh{ V h,*ΞϮy ee@9DY`IUrDHb XR7fSp-aD EC#1!I>}?A]ɡzF\>k7$ĵ76B6 ykr$ȋT<{Œgԗ2s$lժ׾͆:,%j5J{G2*6݃1Ppeyj:`*[5b4O$K љ;7M^A[x5/ ܁Lt(fY=Z|U+qS4 =XLt(gH!t8&--gjJ)'[1nN[HeeUDa1N f@\נ*b=FN췊=~*Z0@BjJlvh?R AY5ɟ'!~:BᥖWJJz _|Kqtcd3ib$1.mCoY4 Y?NFKFX/3^{C?}cсчNƑH./VO~Ox8qlc=i9z#v~ "8:TZJ ϧnJ6d5k&(N64>W^@ѫvYXYϿXƸ[%|Od hnwG7 gU+utf<#B $bm7iqtC$ߒ3??:Ykʫ& ?1NAr038NqtCw_+~R6 bNHdO?yF3o70ꝕLڶ:LI $u¦햪\m J)YI  ax[k!"oF : p#6Usl&ݑITlI}'G㺃d!2n\ fo&OH$|vO3=5d $v@UJ :<9{aYb%‘AP{ JШAy^KߘdfƘmt_-Qu=I. ylgwL5txUQj$Gu{hP=x ~$V0Fѡq1It$x6R-D:'.1Ţ?G~ gx0"]ʺ('/^/}-)$u*"ìE)W,|BPBXo {TkoD(8y?ϋrWzR?u_C+D钆 `UtK VTh$^׳+gekP6wTss Xާ 4Nrn>ΪC=`;0w+HlscZ҄O~[ ʽhv<׿yW͗_Lo:.] 36hZc;,y$\|u{`UX“sFv /AaNz$+tVp9I)G}6 /#숨HǴϟ@`[PU[vi0 RCn3&o$ϊQf bt hǿ1<:'21 ^ZYFR6J#R^ 'H1 dyeJ;?VC8X*tFw!+wgpxpx[CH2*#|e`g8iT)f*h^6&zL&L F7a Ծ ˝dUٓ/_׭TWArӉ,ҫ>ÉgPUw]$6 J8 z Rb5Tɓ< zz*Jqd]ل giqtlQe`48MZ \.ی87 Grruzaw+N3=0KGGamP_K D$^TKx|{ yp*e|SOlv2 \ B|8F!#%3~sD^nule&`oDtK!Rz.[F޳'8.Z$=Q:Ȧgj/-xJwp*bnW 0*nPLŽPϰ2 jZя52m>׭Wj:5Aw^;P\)zEz "A'Dদ$6C/&ͤ۹V6fZY>.r`zVVy˖Lhq!nIjM;߶Ch=D=LwXm$G-c : wc6q}HDz`ݜ[f/re^q 3Dl勶se;T ,B$J*olG-ĶZ ;LylIEґE;Pxiec!7_RY# vGk4j[aO;~ R]-4RxKM!|C׋{&X]D]JO!]j)8\oHQA(?bP ӮR%"WbKBw9s+&~2 #x:So~t&W8v{|U׊1}؉(nxrB1&":QOmO!YEfaD#ivo톶BIT].#3rrhzI&ҡOQ8`g|Hrb~+ 0byuB"j._žb\(CIzH6JÄӍMQKyJ"/cooz3=u]#8/g@ "eqp@D\ ŝ=n)QnG7_H?ɆL^<$v#LM ;õbz߇?1l-dYӓNs(^H "GB{"3]gWھǏ zEp;6V-u\PDI$p+Ix31448?x=d*ݐ$RdsEicW9ؙJ(Ӏ(jkFD"pz^>0=aܴv!" ѬxsN6eJwOc2S)}"go{6 'o~;{wXҵqZB 6;.@>tuUv`f`_UW'w%Ϟ֛[ʻzW|aTKT9'O ʉhc4nc a?4?^CwŶD$ˤXD} X Lz@ǥnn@hJ isW7ZwuVVWk=}G~tw~?sx_pyZ)?!^8X[8k{=/v YǑ){`G%.F&ԁ[@Adzy7~|ȇ_^BwqhBţ1\Z{WzGw~-^_Ǽx{+@V1%kip\2ex dsFxV5 %\q]+jڀ0C[S'4↠ԇ>H/>UaحC^[Պ?>mw]xl{bTD 9H m0q·+sdw:5G\ ǙH" o;p|( ]S?#?w [Ȫ.,kf> m靋'^E˲ sQ_$<$ufUeDdO y9mo9N?0<8;;T'P%,,.Jga{NO>ô>X8A,p4Ḕp~.vsU@L 'd}x >fUI*aBqe[0OITC;MkV? p}vm< mXUz**FrrI!yÞ+<~;L+JeTA&ǨJqHp:M$5pC7!BHnda<F390)V4\p1Lz&\{/I4~Q~ S0=@#xz$Idi-ޅrK'9H6+}4jhIu=; >g iN"J-7ZB CAchEI>x;oCILx$Wa\ûrMXU91TLW&·zL Y فr4B-O;&ɾ$>rz؜("%MBLc t)rrVĒL(<7|C .PQلz9T|*܄R␈怍ސ3k>um!.e&TyJ+ N0!btSHΰ}@u nx}J l i%9g;/]TAr\=~b)"'%xu:APh}ޱ ,?^cf |8A2qHWɧ0lnIgbk@b(ӣ`r0BɞYgwH 2 =iLJ"ϩ_Nk\;̥i`{O^9hEQ19qUR*8!Q'go=o%h =v {aII1޽g .NqȼߓPěß(cSBVc@gv C$7Bv(h@ U.lh}@< ߊa daiixw)A0 c]+l0fdz?mo))v@=Urd&P%9f 羈j+!%nZ˳੧Ԗq4V'C] 9H\ᓵ9@ *Q=̠3CJg__;.I"%߾뀆0Ugr/"S_7~x/?®wqR*d-0R>({/3e& TK`rf cElǩ9rk |vԩrڭ* UUNaDA-0YG2(ɵ@ <9d97@N=BɌTڟ^mN 9#=9,dx.F@̕Z ݇]x:!y?9ǎ@u/RKs"·ds>IAbUTʥOp_$"W1 ML%hZyd +p P*YfB6c'gr|&dV1O}%.|30`jR0U?:Kt:̵ Fh>MQ!z {J1fX@ H2hjnzYd;R9Zgfy"%O)f88ve6EQp k+]yxVUV9ya2VJ"ϳ@;A= ~mpH @ǪD[n(GA"١>Ε# DЬw>?&& gyzPųgYαr*? >qkK,v;,χ!N{aOAnZzūdr. A2Q}K0Q/#A11==%IVT&7:n|*~x|Zأg9sf+]sP3{x\Lx#QrPG.LH -$.{ o~戁ryߊAX8Cu3"9 hXomm0](k]BEgXY^suZ̵| wWy)0UFqdewC\n nF/m*}!eVxÂגCߓ^G7u:VOYa"|~M]f|39 =r ^h.Qt:><p3 $'P)ؕ"i6 SӢlŶo4$槀/j V|qU?[yunoU[myhUXյa(Gqs!@yLX>^9ZB<@#1 ORw`IYU%"ٔ(wmY\9ɡhH vVU^ {!3^RYϟ[̙P_aue"zO=1&ވLVlB ٽ6"&d޶ .tr'g۝2٘LZrqGϐi4OyfARYڊKN{,ahkEk10\Yzþ!0`J@ŗevיU7ƒTS&a`Dx"ɮ!'TU^Y[ ϝ](ar7 LU喗P@ J~]Z'dֹBG ^} Ɂ"? Hf#Hgnq"BSiW,tBVt;|69:!ltlUӮ<t7$` h_z|3b "Rf_sA8끓ߋ!If/H?e/fy(!0Ο] (sa_J,R2iDU`V7u`%(7oT(L5D^muP5,oJrP!t\~ pPiJά.#gXBt:-žk퍍yGL $Ĺ{0Y&+Mwׁ\F:ݠi] If;HcmEX*ᱵDx='umqMmn޼-d>V!yF0$K!oA>Ͻ,eiϡ1c׀ {/bX 3wA>tLi71ɃRO>?c$O2ٳXTIh/0iUE{ux1n?I2v'0Lt?sc7x[78o_{ 8IJx`w + w?߁!< | ɾmn@Q@`9q<{ <|agXZe},T V#z\zht )WAY-\u ;SҸAYz0@@t^x*(Prz#qd_Gr|$:|2;re4Ct:m;t Z{4 C P-C+;E5;SUί 12"ro@#æcN^(uIasNX\Brr$*>x) Y@BADU AB%:3ZI}@=;Z=8$D/ |Y@,--!"T]Iy|=dV/!y@,4tVxIrazyI q쌇_$'?H8)gY[s!D:}Rk\qTגih6DQ'}Zk6z3_ ٿN,\ ׉9`?|J g,{g4R,uq3b2- E-])n@4j  8}< %fqc_ֹSCLTO[dwKo G,3r$!kM-$ӥv|Y<#aV?u-uZ^=V++@\:yXɬ]Y"s S^xg݁svdY{EU=#TKU$ۛ +A+v$^.TTOi워.Ea,B1\f2 T0ၾ $=@y3cR,Y\\j3"ARo5ҬN*:?($Ӯf0g'b2+x<4$)H^xX4龲$s L'sM$ gZ%La&I6'BckA&N N#9xZdsz:3K]KS%^$IAr ߍ|dlM M`9rrD2h TvƷ7*1lMkIE(MSCdj $x.T"ġ,T=1Y~:|w6QSBHŰqp}Acَ_<s IrB!KǒU? 7PZ֙|`t WBX8[3ڼ|ðqv|m^B@ *4u?fq.q}36Y8 ;l~xgiu<"VIS$3SXå>\ȐAw[2Ѧ!m>q* yfpfxڭsPЌz!0l3 3ܼCwdŅ!0]~_=\Y9C$ɩ (|QDD 焬5J#kGv88c0aA BN ֯2ܸJ_ҌSὃՇ NꇀQU5գ<K$ D!c1R@Er8LcF!G5(jE2gkPB` CY}Bqx_ù&L@c.4) ὠψ/IR$"Y񇨿$M0!"Eh4ǵz Cqr QŅ$U&G Y:D]፪T!<^ @ <I)H+߃"@IrpjD!VNU n TgA=09Q~?r@%R`` XKD)H[j۠]c\؋0g{/2ԢjUUc# ^Qu!6X~LB$FL  &LL. p$y$̝ U(<#ya5 xUc@[6 xP|l!hf ȞWi;D IH{1 _%.3Ej=2Rع 7展@." Hw@liƟ׶m۶m-ڶmۖFJf"t}^Mm|1xm Cz_ I[]`쫈#e"+Uc%00X?V(ol4K&Ėmaz M}3H 3丸$g#crͳMzZc`4i+(po4*lkۢO4fV9H2)!gaW1߽?#] 'Kl[FփmAp{mol#@Dd\ rB\Mxa,U# 7 7M{뛓i1RP"M,F&b3@H83CNirⶤpC,^HQ9v .oY71U82ÓYbt 9E%qp7wl e.VktՋM82]q  .Z1cv]RqY ai98Wi595by8 rh"Z׵Y# 90D|]3뇝U1ܾl {sހ4psR#?C5gBޛ\ր[CD\ CdG95ŭpg*gFŐqVԩ[ϫ8wB2|q1xd:՝[tzXv60d;YX޺q 9JĀv}>VRWQ c#R.Eqp1[u)pJR[g1-wAi"qoB6II$ϟֹqq@H T =8Nss?e|Oճ%Qqӳgqx "_:{72y֧& sSg=/8^G6D$<:?9lct_ptӈB"T=S"xƷV9j >W]ܹῃnu t>/?<x&?DDL9;oԿY_d{V(p[=0DDD-3sq|Ach~)FDDƃl6z c[rfVADDƋ],foqH5#"")ϚWş O܈YjܴdvVDDdu[u*$1 BѿRl|ֺq,B[la&af7BvR7iu- 6kfsR8SoiL" EyvDDd ֍S;c4=,[S~4 2""";/ ޻ҰKم""R>֝i1NRw7o 粬cyp;#""CBwn"{!""G;Wʏљ}?h ]ffE%DDd Evɔߞ #H,"";d3qؼekߵ=w'6Y_jYo>ZwNo[짬ɔwvѬb.dEVk/ ++So靖){dgCDDprEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDNrhIENDB`PrismLauncher-11.0.3/launcher/resources/backgrounds/teawie-xmas.png0000644000175100017510000051051315224505336025037 0ustar runnerrunnerPNG  IHDR1kIDATxx;jH=[upm۶m۶m۶mm7=k/n^hc0 `ir:4:hP kvsMQАu6`0~ 1^Gdax?>w‹pO Մf`07Y @p)I}1IS8w XF[Vcvn"|ՎFCPw`0֧ ! ٰ,؊~[{䲧e/]XTsqQڧHK %8ҒJKJ2a SdT\FFquє܂a`0,`j<_:]wCvp=8O>आfe4/حyGק ty1e 1QPܐ*$Tnn?L Cq^8'Qz=>*[hZrsD[x:3B1.Isf]~O 0WFJz$УxBQa!hL߆<@xL bFu߮l p+!HG^ºObe+pzNсмnZyiE؅T 5pQ;:V( O2@wqlχŸtݪa jCQy22W+x;}cڄCɈs]@9Wn}E?|@e*m1wp35*4,dBM7z5lAc(^ViV U1ey+݁L17P9Hx/_&t1f)X!X( >mi5҂2pSحyF{\:!Z8Dk@dM#k`:Y=PC`Y䨛h*ݠn.{"{"T@3AKcmD>yx-upPEǘcwq3vIx [>]/WF:H"J;QwoħG2/DP⦌Ai3!\ԅv K2t}sluN' 7Iiض16mضmAZVZ{ձT}{]*"Np,A[Er`'Jh\dQq^*>`bHTY3vt n+i0M6C=d̃12WڔCvt<&&A}1/2q ?10[{1#@= ،z~=FN;~A~iwTdldL'?*F T|[DjuOPBx @7D?岫D: *KK.MxScT4JBQT|U ƀh5(֡5XÐ mkg{j" 5HmՐyCLyoM* ;mYLcFI1D6EƟW )G1!?*<Ϣ]h|bW]MW3Y!Z [W [&zm =jZI^I5q :tЩx1dAfJvJ TK(N@~L Kסn&Ԯ؈sbٳbb> tr NQפjg;p}Ut6o"L|)pecӅOa)^0K xg/pn:c4~Y(7yQ&ѤQlzvM2sM+XZk,2߱}xԠ࿭k-_L-ve1ӌO0>*?P!Tij#yu{ SO@nvLA"pGC>g^D{g!V#aSk;-]a6CIT<cO%hXE/CF.Atf*,}% "nT=eMm9c6U4 %L%T߈=£@Q쫠W0E;rE,Rsh2FKEηa`ygy|c20&8uA#p:,?GP40=YN8-WGnWaeQ%]qyvjFFQ4J܆qҔV֍S_ڂ76"SYZ6cԬ_VodGe亁]۩h.R]x3I,>l'e K>u5]:a35E4F^QWt6n~sFέ(ntt7 A%lJ56-]W_@ 3]apww5JA"BCBB!=]{?j{oW_ׯ2ӧ)k&7X8/|' KL!T)J:W [i=" ׆²hocxE-鰋~,!a`5#r-݅w_43zr'˛Ҭm?bL>JqXF%^ho.P*XNﭾ!C@-ɳV\wTzX(Rsv$ Ec(v𡰺 |27 ]ȚCQȑϫ %@ߔrG;_/g&3{/4 Le#ӄЬߙ0 U@x* m6Y\S&~eͣt굷Nl2ێ;<=1g).E4:5e\qnseCW774GlRPR O*cXAvޗ/[G,f8,Qmhm/qb9"8 Uiw?{* 3^oQpn9,VZmF~3 oH.Đ+}Fx̲)Qg6zް@!{ 0&gPlV,Ep='ӧx'3{?(Fg2+, INEWmK9Ϳ'ʞ ?~_9*ΝOm̭Ҷ͒~J9B EȼM:|*tw-6\LE=P)142-jogyzgW2?z,>hsCE$f+ZY^H (2:Sh >%^\0`!ߖZ0a?s\~6"ۜ0c/58Pz0Ց%}?,hy?P*!fҼglNH%xCW~ڲMCe2.\>|S P^q>݇b3Ѐ1r xC1-Vu0Q%fVwd ifm(E~s7Ҕg2` Qm4p`8i NN0JjَJ,;vHW_$~=\[00X*UPU]Ӌ+ Y50Q_ȅv޽ʳ)`YqH8fR@`>0:^n57σ4+xQl1 ^ R _@Xhy7; Y/^DX@C0by z#T(&HhVYpfWFD7:%yǖ³&v\d4lԩfOmbIqHJgҾĩ=K ]>u0|B(>طRP)3ߕ4Bg3z*Mg74џ8t&BVݞ3,C[W6?gZXQeXюTtj51q(FdB~DxӸTHਾ[M-00JyK|fᜰg'*s QD?J {MgG vO!sciW)zqPWh(D_zZ7xB ru`]g(=:~8V3ﷁ8/ 4c(4[S㇌ޗiI4dƯOԻH\Pb*G̰{SZbkEjm߉X@]@m*1lvIPi{\{>۲%ZW >Ӽ=ށqjY2+fϰܨi(uř{m=8z u qq,+.@Nb+ 3BC;QpM:QMd֪ۜW~ΈH14+Q\Rތg~%(fu?_{_Xpv^GNXBgfH(DX`/jqޤQI`SGe(kja@[qP&#_y'VF6nEEehS_PH k6b0ffѠ>}jJY@u gMewߙpsgz=8hGH,϶;!ܣ5^)x,=G,Gβgc@Өۼi`VZ4t$ Πyaq)dN|G:$.u)̛Od^6~ռ>@ivi޳2ŪF߱R"2lbք׶fg{ ] ҬQ "}E$"7u8 dUWUAP4/E/7{q0R^/= )S TC,+zChuG59̱ϔhڳ?w^SfؔnS^P,BmQuqYwjlFGX5uxXY)L϶sŢٽ>VTʽWpL@8k o*M |p|FdQ: $vV$ ct߷0ñ2%XjsvB^JNaռ= _“23{PzeԘ l-r uB!Xޘyz]~*{4%1MYkWXi3DJ[zuM~8@-v€+s,`i<6Ϩ 칆r뾜Ҧ밸_*#6\6 <.a.G4W,o2a2c(lFh#*SNf`vDwʁ#t梫ʹ~s ph־1TB.?T۽⹷.`a 5S\xGdtwPA j5\)R9b>3eCa0)튟{QD&J5nzpQ{॓d*(lE^~7TVC9ŎΨt9ϋr4:~tT[(1E5䭵zx3V %«=:^WLT"`N݈ōf6D P#JBb  ZC H< ?, uHk/:+ mXkvLTy] *if2 xe(E; 6Sԙ%8X1x$eY,CTFH>l7hZAd]1ǜ헕uܛ{}!6,ZAl[T-r|) ~UX( 6ub%.c@s Ђ}hx.Ρ76r5ksqؼ~U)&8Hi}1C.Ze[0MYjҚ,|W62WqcwZ s%&)O}IyyQ:lsrbjB ?ƚXG7_7bkf0 }e# J rؿ:ab7@ۙ,MwGGϣsw kQp,funJL̂j@oٱşи7wF9ሑ<{Fc0LrzF;)wxaja^aW p L+Ru )K )#ɤ)R!h%u&Շrw`6K'q=ۺZ Jn-zy?ۀb>R7Y.p Jf3Dfa5 ǚ ŒE Rv jmflXx!o+nO`Shqp؁&^gӚeX $eiܺqY};s!;;ݔKyj3ܬYKnK7vBq'ј67$N8襽S8Aym:~עXCk7Oe5H,\<=.{=u`t1ߦm@*OD#&nB %عGvjH2 [$mr*&ujK4( !u)UOMK,nOyGT(hbu\WQ'IM)p̫WіՃ׶At`]\I)&`!@$+ßQr4t7-XHVs%>8=Q%j ܚ1|C7Ұ)sf\ 6+B;Rh̜O0RPYv3ؕTGwD,v/_aܛ{yHCnЈn􈎣{;hu=401A&ƶ5Uss!ՋƬ9@@K)Qٔӏ/MJBSxMEhGi>Nr0{7BBV B+P$^TG'4]-tԪ?`Lszs8(⭹-qDFS ˝[,l1{cx\ E>i 8+4]Ls8mX84*+Žhb1z:cǹ-ڏ/nݸ?4F|:%CUT، 4 =7j u 4^X 7a[S6}N#RkEs+|€}%B-v3O[ҔqϖGQxHGF4dsj%+k*%XsEV)*J`)>!0f6&/"MIHGFɥԢ5 %p^8N̉i;[ (eyTZWNiecOJTw&j^U]Fc+8&!0X`sf@* PCMk~3083 4s=fy;ܒQمǞS^Og5 6%XNXFV$iow" p# T4y?3R_|0=`:[[LRPGHvćޤyWDB?nKJߦH)kP>gM; !PSQz c1j9 5Xt~z>˪WjԎ8Hj([*51MPQo8Ah𽔠R^DMUi%$ |m gO@~0wE6H=vwV, y3@A`u? n?=-5$6',/hRo }a:\d704CsP7 \;mN4%muƽ=jXnܖ; Z>rU(苿4yI*0:\1t%5tIDŐ,u+B3^wH [~?oҴ>(#Q5)*CSҨ_j52'-.e|||_z(^:CLO6zTkt&EU0$(ԖD1g9A0$&X?T|__￱F4V'<[Le:XTTokMyR<$#^hNi3&w  ƙkZAH羼IOܓ~T]:ܽY V֢oК5C؊% :iՆ'4y2 IXGܼv ˫7"'eZCؐ kcu|h˰y͘+F̒4kUp\L_ HMlg՘3k03# 4g/WS6mIn|VH0qM2BS71{jEFeVr!oqy1iV b l`ɛE P,&z\A탨cGybiEhTQE 7f4 >yv,<=&Ȍ=S_aܛ{Y!=iޣѳ/]@JS 11b!sdE|L_\zl~yz^f8KZ Bmԕ9<n] Yr04&L&o"B_ۛAK?sݘ5ׁ]H_?fQ qljEx!%ĠI NVjWhY3V_t\JlMm;ut\ )+]073wqJE , G9CO.\Ag-c T @3r5Sfp|Tfae.qvo5[5ւKI8_6qg{ Pߨ8*:M Z5v"-8M-9T0UZH] .D{]-5^L9ɴ%9wu)ԷS4a]g$݅ם*9<Ȯbl0a ,_Xc6fTT1p!l[Q\bqX-Rak,܏lДT CJO8ư:MB|&gLQ+Vn4a624to>h:R.JCdO+}4½rp iZ`JjKozixZrK/)972?gʽShБΕVJA'Wڊni6kfC@fhB*e.CQOkՆ ƂDsriI4pC4)9; R> tO-]rxu 4Fn-V"<|)C~ pzzZق}wB*گ6 FԬW'꼶~F `̨SPV+P!g/}d(異UPe[тkB2NVBrP*4v^Ô^ Ozץ%}['0ָ77tCG hD7gtT HhzhdEc2[oiKI>}-4vTc%t@S.O  f҄v N璉FՈ;Ex 񧀅t} fqcz0 hlD/ li)?Gaw㏂ur@,C zvo-T:LJ$#V(*񧸖uz.ϋЛ6sƯ;L-;QGSV}+b'*l%(y6`%.ZY&m>+ By/yFIg> 324)ָ7`*ɝ7?Y=Vz=C{cJv>hy gxAlʭL(Guլ<50SX~C|iwwVoI=ɥc6P =zk8dȝm2XLqvha> &vD/OPYXTK Ώ+>qIQ@Zb)X/!#eBx KW^{<2lO=cۈw Nd^^~r~'oV`"ֿ`ε61B(#1;<_Ugk ~"Vl(L[CM>ʋu˻zp{-~YHk#(&Ti;kh豉iՉ1r.O 49h97rhRcksg%IF؎݊F,2j$.’\t HKHrVn o>"q͟/՜C̺@v]{)ʳ-JJByb5ӫUit/~[Ԛ`6 ? ]kT~Uвӽ>g̀9ܼ%%#16_,d8dh9v/MMno6jFcȱ/ Xఙ[Ԕ\cq=Qeֈ;iN5]ַOƍeS`Mc jE(4 230@^S=$e&,Aއ3}Y4⁷ߒ͔yx2N8$%ucC(p<Ae(  ?x]I뿝ԬEJB݆/91oυviSw0!F&mrHw P,/-jf.)0#88ː1 M81yܑHuomӱ=[~o]kܛ4D{H)\p=g (88mH^\{:R\&*l[NA-`[/>7| @tkH[b{">Q`k]`kU`~ kP*Ԋ`)[Q:6W M<| V0J3dYc:8"=O{~ɷ*M~qlNu ^ӛ'm~d꼧Iioj. Yq.@~Ϭu[PX)vZ gaAC"'{p&P3/o}11田 )k͟d }5x=cUKMGO>N *DM,ܫQ)'E!fMJ]Rԡ&idk0׭CY[6?|;!8Xs!kjP41p2u{xW4>Q~Ka)Ti}r{N=IJ;]shX;f 8xSjlXoyA #ƖO/<ެ>~st8޴vAZ`2ƣD,g U:hJ1Ŝ X\Y-=۲P|i 8Ȑe0Qq5O m(ѕN wzd!H@2ZoɞvI )40H_-s5oOap[dyg jٜBB3=~G*D)Ġt g6`=BIiN]g.s  k=دBA)G@lҒ֛숖fD2N[n eꭑ3ܗ~:v|.5i+eGָ߲7(.XMm 5~h1߮-ƎbYI vmLlOqx&)Қ!ϓ@m^g69чZ 95b7@QѤm†#T o!`iD )hܘ;cp9 09ӫtb)D[@;#7m FWfu 8t|)蝚/)5~қ:u #Y@đۅot0 /RO}"+rgyzg_Zܬ&lWi)ъ,%̀#16"ڥ?GO04#g`qTV*]ShptGBjJkVh, BϽX.2p- $-gbh& a7c 6&?L=K.;v0ࣽY HڀG-ˡTWv8LX÷Lfޕ\&VAG ͽ)m϶xS2F"LȘ3B?n{5 6?Wf6oYNyqocPTZAt fej1y}|4wϋ& i=`6TPwLRZv~jdaELIB)18l;"O3جm83aSZm4O~B}筣4B' q61&kM=WC.kZp[~ڛh`k e7*J M"F'&}B^~c(8Cx\ ht00e%sE?vvvw "׺ָ77,A/װ}f4W*pmݣ5΃QYka5Z#ܫFSoF4>iʇ1kM,Ui.1)&wxS^a®Sot +FY+v.R>仝ø&lu (~7,SYY1@f lul~pc](ϻM6^"[`K$ lDsΠs8q'ͻW0),q6n\}stV聺ypFJ,/P!jYm& "&}F>ea* ֌F+iDd 4 tX1+Uf) z \$M QߢL(LYɸZ3.gـS$Ѻ/\缯t$LO<6ٝѠ3)->ծ5j: z.Az &wbHL5g06^\4yk1i1 ݧCA#K62*.H[hqg^j)*&u1ҶAi#iioECe˘כ"x8J9p_ pTcٔ&>*2&@c2ڦʳaPQ+)77-uPc[P:ϟy׹,0nj-/[P<hBNԽڵƽ&Ql$E@ :p8JЇ~DORS/C`#kqy0iee@|^NC\׀7Ƃ]^"[V1PJ[%&rl/[狏. yҳ\)Gaˆ6-9g`+rlOYSdc1Zjے\V@8Wl:( 4np*KS;(yk@&&wBjS`#g9Ҙ~Iiάv]^k/w L\Wָ77 X_NAC甒|tg4=tp9hfGOYS߃3VH9dŪτ))!yQ'qջ OETҷ%"}Ț]o9ghOeYfMј08_XΩ3fƓ0ջO'FSOiĢS:/a/6-Gg;r]BX1 C3 5&P~F.1Z~~CzT}9=>yՋ{s\ufCi S%,jE9GB&F &dzьgϭH|D&$4k /s|۱͌\ؗƛ5؁dL%#hS^Xbi4{!DD @F36+3\[H儚ȹ.d5" py}Vԭ614|mVǿrL>=xJjLX͌3pJvpLԷ.lsWKtyEGffv"ܛ?o;f{\fRp՗T@Co#a4 ZS;5Z$0ޙh-i6ky;~HH s@oftg*9O'D]4),خ$a^M ,YRQH=K74%j"U G 4#LI6o{MgPͤM_PF@hښ69ey:1~Y&Q68[7jJ)?:@s~ERR*4[c.m4SYZL}#uOatřӜ8L4sFKZM'Sz8"wn7SiU.)|5k{s0s,jx7/kLXcEB7>qI 45QHf4hWO(}#1Ai5HKU(σYg8:pգ<}e6ٝ1{soi4d&M,㟵cA؛29k0~zoPBd6`ҋݨ_AC،h1M =R_2,G(QJƉѨȣ7O(V EGM WAE'c1iVI@-1Էլ7609}>5jȠ5Liъ_aibscCvV=_K0M-`u~R*5b᭛>13m)1χ `oBq8Ƶ.9}!Y7gh,da3趝44 1NW}h vF%K+hĭ)m[0鰗aY_'Aõɛ`#a5 "0H )2Jڈl +0;Rg|4w9{e͛*=͢NcP~3|N;4-*L ))fqonRװzW1ZYE*ίP3>ffϾbԬ<4CƜ y?ȚÝ%8VmŃѡtRʹpd2 s[ЁBxҨ{_ԡrU@F! >ߧBH%HQyt8Z:ѵ;Bk2`qG)pl {iYe4w_MN.x5k{sk;kz*f<ԁC/ 0\UsEfgO`]SyJ'8y"f)ͷDJ+Dΰ:G;fg0D sVW8u(ԫ=Wh1:&{yU+P`%̶)3_hUbk!]M49ße-wRlF|uФXn[PVb1ed6u=϶$ԵowÀA*]{pvr7ԋ:2Ȅn yt07g:HBP E9It.tÎb8bBQule<Ɍ 軲\3 8 XVlCjOLaٷHUH0Զ:N1fqRD*Hmquq@@ș5k4mcp)QϵujEҊit\ΉQYVuqoz/fzW2oJ9:cDZT"~z oLZ?]UDQωM`|et zbڃSXjP0lIJ|y=H 4J1IS>UpZn?:Q PXWg48v R)Eafn';f-fUɲdu< '.va߮5d{li՘n|_fH"W`7؜EsƝQ j5#2-/yYqBM۟lA>fQxAby[U`ހ"u `Y6TL`0ͮ1v&%q5nŧPl féR}Oba5GsN uH~Y 59jSڷ ~ MrztL(z8f6q_G} hjՊ(.h0x$Q8Bz.uc hh@ǞE0}K˓7)^t 77e "O>F5Mk*I޾@N zq-<1 Ʃ*ٍ6v ϐrA{}zFdžFͪZ`9ڲef3J/Qi``(:hi޼ςˆJCKƵj6-a?f>1-Aa쒈V8/6n(&©y+/66eM=nyZ:$Q&^]k.v뼁T1Px6~g=dxxVto p >(J#>j͏׋A7^{0.gBN}DWE:0u!NsS'E6U%z4[A?AYݹ%'/uBu 8;F;x$tpq4 lZDCbRihb^,p\hܛ{ _ :qxS:TLO.ْhlGf5c:W}y c.0 InJ).$&5S"5ƱpO1cV͕W NRC0 ` $NՔ2خUqp5~`+2>,`fډ4}+<!ڍ 6(*;\ BC<b(+_ka_r s"Zl|\ө E=] 1:31A nB'u!̏;N$>03 Ξٹ'Jitqqo-NDx4#U0LC(h3gq& n9mgEuA[aHO\*Rd/Rr|/䬏 ϸЯm;Qez48RGiXZ y3K =@Y1sI*Hm}/5A4`fA@g?Ue&0J9y3BͨBǃhϕ?Q9'2$Si/J{UeqoھqMR<[ЦE+82Fa+ζ.FD7`6ܬQ0/29a.axn}^1Bz\:<\xyHGCٮf2GBZLvV =ŒR[z҃`'ϧƠ&=5)\AG8%j/,AVϧtx|tf;*4pHJ~O vkVC#1wB=W_e4mSƽ/~7x_xʄUs7_Bc `lq#i3=NbQQMMt[:iN^Q"^̡MkPk>` {pܧhnde̖3X<ȗK!H>80`M}H+]:[rru^1W=r+hy;dc y56:Muq+>i/U3*qq7hV3\QzUYkܛl"5yjqR3ɚ`&Ĺ?q4EnsMDБc˓lӂrmlǹ*`۠>3,-몱sPF_f0~@43@HLfGc1?{X\kI:)uPB aKi2O@gψcsq ʇuWz3Кgu };\m8|[Z^ѐ'#U;.4ͽu sn<%ERIc.k)?.SCbp]gE8@5=ٷoqkӘӰ^ϥ@bTZ},ĸó M.=5lGLCMgy`'5}9Q*uc835 R6^g?ZQ:z7Ue=Lb'je09Θ$ &8 qUH}`r/6zǜNj蹾muUYkܛllFO..A,1_nCK5O'x4ͩ-Uꝑ}ɖJ$+ncV߯ӀUU;p\Ӂ V7NGs,܃ H̤/2eV*?;w,Mn<ޣc 0=,_ݿsEZ8]yzm'rb,DE(u;* {D 3aUcH4Sf>gՙ1k THӉ;~zFaH-B[<@1*+^_L0vV>aaBBf>Ф{w4{qe(9{+u붜~r~ңY\tUsMqoH㩘CZy1P sf H4#3v MOww2@(`5D@a~ܯg},f%ugigz@[;f;1@K o.636}FyTQ t'L- 3g ׼Ӝًp0j&N(<$ZLUk.ۮwƽUl;~ӉFSJһRz\1s]"VmhTӅ 4Xl@@FX#l&v,rI04fzgTWzn5o{s%X {|M^PFN3Y4Ambf8i 84RM+h` yV*,Ac5{Mϩ  q: \?k 8^\AQgM@PL fvcNqak F?. U+0RsL]u':[~Uk'\Sơ|W5T.BFZ(@#X "J5r{G<玲ȓ:7mJtq+@SڸJByGzXQϷcohV*0UM}ߙ[gȤ@l桥>,Q;5=.ݥ*C-Saը4})VIOJXs3ZJ f~InJ;1y4<%;2h6cro6SKsp,`h ɝAœPof.1ITQܤŁJظ7/ >E v!G(S1c:m4,%SiQ=( F-W;cQ(aBHIȠt A b5M͡K@Xpt(Z eb^# Tg*-I:MC/IJ_ O S9z)#TtUMKO}NፚsMj:lϋlBcarfɛ>e ɱLҋ NjzFP4fAd ߢTшk50oM]9cSs QYx$EDS* 9a$dZs̵bSLɴYƌpquQR3;5=и7__DǐFc_G,` c N˝7Ōc۴x%ECT˳Mz+˴~M zu$ƬI3(q36߄I(oJ#=E+^`q;D,+i@aᒞ1AaMZQ8R6)uT@q=ŏ"?֡-`Cyl Mn*,uF{]Hn b˙ ">)ͤ"RX;ysZeWԼy3ɆC7RbR5 r{.-AҠ0XDJ=#h\NFB/o-5hjv33!Uf88C=F5``Rgh$$奁aުQWK/pM1MRGO|LahN ^5%jGqyYl*dά{aTB6.|d*mskU f'<.u>FwBg2ȠfJYpt4|}wPx9_5riI1GǁcmETcָ7wguދSR[6V4>`ph&@ ;'y>׭j谼ǙC=QBO lq M}m[#L+[{ͣocPyP`5VQGCYiϨD-b%v X%bͼ1S):N!寜Sx<6V$ ;KwQIv5ճf&0nPʤЄwE w }H]Q.lr:t:1vՁBK*̸P#Shdrੱ2vJq|Y߮thuTָ774ד5hi^-e!hDj&?} , 8r[L&!Aӗʟ䥘.raq" X".R7uage߹26ńqZl3E ]5Xg4I.qC=w<[`lL:lf-\x lS?#0z,61@5Uͱjf !i,P" D^ӷ:ySzOihA405ӲhlA);qSRaAe|LSZI{/(Jt Efc&M;`.PeKXp* ' 3D3(@iL9 4'tUK C,+&jԿp8p0s74d:Z4}!+n>ސh6t3v]Y8ԖC?J`VP: ?D-J {soA5j%vGDdp&x45gxk8_6j/^AZzNOXRiI8 3nϚ՚RBi-hsWm~ia' uuqz@uT)0.>8Âjͤkٟŵ[ze©@1I&כVʎo+x=79f hD,av>Rx{X d㱇nNaH? ɛjHBj9H!}3vխ7)p8އ.<ف f/2CFsd\{OdM _"~ŠWOjCQ}s =x.>%' * r[03/u@TqGe}<4{':f%0>^N_Ȟ9>-b zMyVkۊ"{ČM3G[.[N竨.;1MOqpR68 hzCkgOYvZɚm<ZhJ& j9}NMjLF3 wl= J\XO}f4tN8 sn::UUf9j&AMkL&s7 k=oݸWߠnS۶-0Ri~*`f-8ݕU0JLX6V #t#Aak! &P>1 <9 bA*gGxq)0-gf(6(z>MvzBZ,ӊٿ]Gg˪)}6!}83#Ĉ`҄ ݛ6)6by*܆-ax볟b~)9ܶz{bD-GnT󂚺)u h |[ԆNg4~HsK] So8>w ]z1E8s~cﭳQGaň"lP3v@zծ5lbѡUW84 Is\ M 3HlEEC8^:tE4mm1N)%4GgΧ o֮6W@A9.T/77]M[.{͇ +m(9 8ʪ_i?2 s:F>M%"2 | ؐ:G5c  p ͔mu =Y@HJs`2,ذ 1-DֲhRxC/*flEc)hZ &`PR:e'#nN 4$3%KovLZmR]ɦ7hL&Y 96mg: yia1jڵƽƅv.%s@)Yshd1Gڟ=w}7}1#V62lZ$u Z!? dC>EE}ʹf x}[}FROHu +X'V<> jJ^hWi٘,Q`P3㶍, \75cVȳqXBj [\Sy7Eh,vyNmYa1&cc4 x$&G<+R/G3sv6tT|]{EO9ٷ́-/PE[JܿG -Z0IhҌ ނ`DAv42$XžSibDM!y JOL&nޅ߀DXP. 5țV_mEx,V3cwZ`c`Ql_sjp2;c-\0ۓ^ŢB}&k> F<[IwӬ $}&&-#[\{ gދe,eSq@0vE[wxk`yMg! 6٪*503ٳA\p]Fs5҅F._mY Rwb="1>V &fsPZŚlUetbAt܃q sD6iIy]`<1ZRd2MuW.bTNu7Hy" ]%&ӏpT*n1(IZ z+_HV=K1ށ<{3e8v^G 3 n{|Bۅ/ב4#zP>1fBn5S{}Cvτt G3p(eWaY8 V_Oצ>{n'5oŬQ(֊kZhܛ{Co0ܘ,zm0Ӧ xInN- ΃A v EQe1+"yaF걓Sӂ>)2鰚u݄7mF\^0eog#E`2drxya41<>_jE a T B#h@RH/U=Ch(ŕ1G& R 6z0Cڄ7}8BʀrA'g4 ~3L Z$ L1 9F|v雿 O "ڋh4<9Ff.l(S\@J_?t93*Ơs;`w пǞ5SGBj< .w ͽֺʹZu ,3NCo7;^!IiD)'fn|_HsŽW:cf Uj8pyye4O_ U[iO RJ[ dk[21(-/:G@# 1+4!C ୴>/ w6bU#bK Xeԫ/p lّn,kEiNefR2/N;tqX}s4l@5߃/$&t$=&!5 \l RI(jE`6pzA|.!iy476YjGM4\.[ `agCPcMsXa:p 1kXÝBso lq JK76iK84oy5~TD R\P+/&D?QiYӬ @Ug!G鑚FK'OdXl8¸%*6bSJZ4B{_+x|JIF_܂5 :/Q-;Pi*w0U*(4j TtZQ{5L!}8N -n>o\_Najg\*t6:@9j?SxL e|=t :i2*GKSf*i CCRLlc1'5gE|rQMkz'Nk{sM":/Cxl; Ҝ}?21-?oLQ%H}~XQ$ѧ1 LNآٰctzeע},yV`I!ICqZ}?c9;r. ȵ f4`eQ7K!}>B]a[s^KFP55(. :Ùb#{ vGk: \31S1yJ٦oqͦ00ƍGDʄ{DaVoRFIt1k&{91]lVɛĴ]#ޖ2P̓{? uqoniB Oh t?=0;Q^@<`67nU\ (fads4xJ-6ֲ%faXb:EP ɚ(B%fHO^pwe1Hu?ێ`o{ n[wBj.-q\}Omb 5q Oe(<20<~ɑ\?kC%]ӿy9,D+Ũԇ7PxP?$Nb,,]\7_3Jwr9rJ)/ PAF/ޕvG', }`3Mi, bLY.-#T ̰.7||lR:IjoZhܛ{ ᦗ`<`^jb>[ܾ~ȋ޳Ofյ>!Sv%OZħhʻrҺQ\?,uj5M"4_G4kK};Qu yNӜg Y҈ј;wӄҰ[yX,P3U. ~[|>MB j&-sF}_,d0,K)Ra0pgQk~R3zVZ"wL݆,t\F9Qs2@Rs% )*~`@ `\mzG-H t<Ӧ1= qcja9y8~=U~ C&q0/ɯj9g8j4dD>eFȔ,:)\/џc"_ jQy`5`5jTK |CѽR"k5,GUl۞+ Szw^%Z[Y:R #W/_bɲK(}(/PW)9-3֢Ж}P,M;->h&=BzZs|z>{lr]qZ+d,_V^ -9ŚEu hnzv w^UwFZ X\Ҋ 8.a0ET L:BA&k#jQRlS1sl qCzl/;m 8Ȧ\#Rhr j,.%~6R % -3CWH/5U `!h"$a)B4sGi8TxиZU*pt* iM06M#bFb[6h@Q_JosEG|,ng JcPq^f `Ő!LC5u Xo1O;̦A8smF޹s}.\RX f-*mp# 0j3}§#Cpf.$`nW+N%5oy9Zob[ɝQVg:?߶>}~'GO$Scri?^IeHZGRv`WZɕ*|w}Hu  Ur FH06`LO>8w )Xn}nņxQL{]ږMc[r&3f>lO×of {(8$(*>n4kĬSZ<ĤH)0CU_&& csZwh@qxh<)V(usr5 h ,<&=}>\ =YKhg9ߗBƍU\a"Y7-)3baL";NQy 1vJ)j h]1-< nhN՚.̬Zi݈SC_^dR*Rm-V?nXol+m><,g_qdn6񽾞zMrFߓ{izcܣ\F_BFFClB*NIp[.9f!;{X-FTm훁x;ڏ0*(pdFxC* F3@qTa9/ ZDI醦i "m-Q&PmjMV!fnffZNS1 ,-JL `q_f?\ gtqqNu:m 7>>Bנ zcG FT9֜)Ѥ̊6o -MT@Ӏ&8EK:l*Nƕ>cYi ?ǡ8*3& P!X/?iF  ? 'FGuE`U{2݀[tj=F"d"Md #l0mI)JKUj#@BR (3M8qE}x2Pl %HEqlyiѳϗ`rl-Bl-Rm7JFٜ:=/ i6W| jk>d*8׌hkLUtp⺪BPK)zƠK k'͢È L%KDOYKDn4ޥoђ!1biso=Hw%x}+l1Ef9o3f5nN2;H7GY&!ȧѾs zvk)p)ob+fW ÕZ8oNŞU0 HVVH!C#+FRi{Ys YiO-,+h 98w{QSFY;V Yhf˫֒ɴJ Yg/B s'\}M @ H 5:1l&]/~6 )˜Tȑ &YZcmYlm$%/K/ C ",;/.8ӡf*C&&kHB9>L90"8Y pm?"\Mo 6Ql\ S$4VkԔc:[2>G ygJqB)mŸi( k$aeAZ_ޫ=w p*QDWo:r]}Wm}辖o6"ln)HZ9LCa}mpGN@*wDYQ? QLcK!w ef@j)63: 0Uhjk朚<9+kB %3u8K$Y;]2pLfanq_3MR ہjv"`hY4v|?LMf5nplazL )Tv8F+^UR)BLLx/LSȜƍ@|#fap??{Q`4 (d2x[ GV(3𣏕 CjPg|VV .89 ӀH o:RltQX8Rps0SfLEV%ܘ"LKq62229S 2/RYL txٛ #$g\*Fׅ|t\A,ddL*^_ hů/Ĭ xe}oHK/r lg5iIeӥδzi੬"O_ˑUV*}dDyg|`ay(-(/GS䂴Xh*Ki)2Mg9nZC?G3[Hyp|4c}z10fDʸ^ZOU/YqD`p{8e| l ibxpUIjl%}+ӤѬ7|YMw(sfgJm7&`sňkAjuy TBX 7аF$Sl c(-b#{s彀DEٴlJ]H@%|̜-Fс:6(։W`TKN%lM&,$Ie?}n9 nL%Fa0eo5s hqk_=*3zY[2V|nI)&Sth!`I-liI˱qǛǃӆi3 Chl% cA=y^>#"‡uqhYLnA8_k$fı٨9Sx>hp*Mc-l>RJa`f6EZ\6'7ŻQwG9&srBiԥJTEغߤ$p>1)X^:$6:c_l;We@TABe, P?P :y#^U1t1@Б (j]H/shb(j==叝ױ;gDOQUVU4P6F K#YX_GcmX&byi4~[D^ Cj4{jUvC߃` # h ,PT&0Yrk\(H:5e/@2`Lӌ P>LGML6t§t 4@;/2FMQT(X'HZPHD7 Jo.*1È^] 3ݽ.ayx4h,m@a@/Ֆz&};kl~ǿg -o3 wq,ێgTU0@+pRB!)PZxRyFK#oK_逥iCSG҆S# 8 *~$8\b9(`yy+L]%S^7)8 X Ks|Z!HɿKdNG9(M6VUfBNԼ=op[(;uM\^b,'yU.v[?FoJ*|37 (ZgqF߅+)Jҧ1% M}U@9<. H :I{9<ϑh;=lKe` Ěf/>էh}3 QCIuMk!QjCEfL g|)]_3sei8 a<g{A驩fbY?}oh_/?l9%sn.8̩=:n:5 Ek'&]g LWN *FL 2#HYQ 7}^Ir?؀ 4]tlY}1KwE1"}j./7b˥"8*(,IHa< q-`̝pBǘ ܃cg1CҐ؏$&oH)1~O^qЮ}Z*} B=RZ 6YSdmߖ !P XJԴ~a2Z%GWaHtpmۀu{Vb(i7pZf VQz5OArLME0ND 9{2k;/$hBc&+C 4 R"(y J|?K7BJѯlw80Tj6L|1Rr^>FWz&R,B'P6YP> kP$> L{/ly5/IB^gސ!6 aG K_.Gx"+Yb4jPm&5ʚE @6#orujfmEj+[!W3-Pʍ*sDY QVEB#kB.3tlZ 5>_XEBM#҇#x:i'Zc&rʼ\D;z" K*)3ol;"XVd!Kck= !͑@3.g*|$3_$`xe=.ZT^^*9~9-W2uMe.L(!HȂtL!~<|~Z!ydbaޛ TYa j4T{d0%XR*=מA@씵lZuD4kGE3[ }86㭛p` 3#YfƘ<Ɵڔ:I kdmm?e| n㜛 zAESBIѳHN$GNCsM&@jlSXMW1}AtBGTugբf:T۽]Ap):+oBKMRhI!|VK!ty] ?ϙ-ɈmCw,{(N.jZxd#t(DMGOdLBO,fF} K++sYKA4GsN,6F7l+JtȗIg:R1df=#3Gd8>u 땙t7U߯Dåt^#sQj;  a:\`X1v{b?r$0gb42r6|S+%thk+Zu!R1?g`QIqQ[7Dn-PD3kHG0!3ڴSRjԩkPq 2L2_H_" @u} e3l}IpcCSg^AHT%?FMqUA4aS&{bj>Yuϥba6TAW$uNFG$ퟌ={p  e6k$ngow8,{2 ,@י|6x xԾE8Q2sz[<4M]1#h'b*l<9%ָ`j6[_aJME13)Zb]&!M+;Ƚ7vD d`\.=.ϽaqsaZ ( 8?[h_/_ 6t!p寐scD+h+v,yG?GTLL@my +"FيlڏndgPV{™ FHY  E%(sp&!%U"LMhVry/3Gh)«YkNDusLVVTd:ҝ?oMӂCvC{ 7QM dm$,|P, o?9ya)E9#'6~BM136VzyH0L֏udTA n)B fNH#1dQVnA|9B@e# 6ѝֈfH儘-bfKDw0iv2A7;U4?_kz);3&AQHHXPw1N"}8^2@ЉtƼ {RяB7:R zXPH5EL:}M jI #MzMco=8^ nZeܙugdw6d CϦ{18B`?5oCDit@~6vb^VaSљQV;lvkOdqӅC\ Ejz=n/>٣Zd0}Ӣƚ4Dk;;!ɍj5W|4eccj Y,zXO5)N.9nH,Ϣ<#= lΎ 'mR1ubJB k",mC4M6mAf 36 %lf99r.n=bZg1X8OEEhթ䘍jlzP*,) GӛcY"fX[b43}LFTjJhc}M>/:u)fM]@?VX";W OA9T=ua_wua֧Upy9?to`? lQ`?"_Oo_ 6*'/=#6:oi"u2^!(`;c2ڃyCx +ȪD{bHnXBQm! $؄.f\ HHFRm6yJ4 =MM=E UoϿ-qq>ss'M/F"qى=36fb鿣 t g1b}x繇0/0D4 ٳ%֎`"sejJMа>#Fj>[&x"Q]DaDlb6Ah'tۀ!ݔv$@ILu:Ju"- 0%G ߢ%% 0@!A ,>`)2kXVQSɨYiU'" c\Ђn"̼<S@: k4f8Ouy}>KƖ6=7Vʌ*R>I65ANBX7|2\2\ȽptLbjmA1`d[Y|8C҂(~1\' \MKwH< wDTiɔn.AϹOXn^dJƕ/2x{88\f`'q'QdJ^a_g6ۯ!Yw()] b7jtYg>le@ !,e-XI d0J؁kZ<+[ V[=cXV9BH-MzvߪTAFhSHS]]Sd{ig*bfbIf=~Z}FHY `;>-cT!ٲG +a{v!hu&kFTh=wcP4f[-lP}voW9=74| FҮ o63KLfFRgTT%;܏T\Հ+Y9^Q ;) DDj]tOjݤ/bS `Z(i3IIL@dN-C)aT JE4%V"5W韪`gxLA]\_@Q꽧b') /2!|h.,ˎs'HNto:Ɨ_aacBU{B>Chv!xO=8ϟI V@Ж4> # 5-ܾu[GŘ<ϹûIϾ΁~D6Z8"J?4??s+X>&9 /$`tϱE|#[a4%%IJjFUxP{ V=aբc@B#04 oI;i3)ǎvz#,*E 8&Qq+=6FE#|=t{c 4jrˢΫ~ bt8g"(]7cBeB< 51 #&B:8߫@bAI$AbDfԀ N"K+jPkAbn;X% ",3(sRNrޞj_8ql|NPP.\Ÿ`;OY:*ԴL۝`ar͚5LI5}j.< ~L|#tbr'ս}X8wYWsώ?KdIY6PѵkWkKklR}c<>gflo܋`ftm$͠sS KyC{Yz@Iӛ8 hP@@" `L1(:eb Ga;I-+Vgg)( c̙y*SVcXέBO4mJ>u`8F}GHS cru\YC{> bTӀ4'1B]W1:Ei\?8^h b 8xx'E c"`PUT@at!"(&mEdUURA"($Y^[oIdMődk͙Qi鵏- ~}%$=p4~G?Ǎb0833=+JUU:579[ MW,q_9ЊwI/ŀv{=[+P.pm iz;˱_n E vo]x!Q/\NRGAB|4Nf LV`qq#/0s5zEVX)((͈cr5+1ݙJ^ps+ϲtq1f?G9 \(ko^僷ޣt%A6sgrDBQrR" %bRC$`@ 6u0cM}HL~t1u 4>2ᩙinq-B , ZegaݝALyIaX{IfX!(Ѽ؃ B~)$$ llĆxju) Yl@ *KB'+~w:PA$ԡF  A+ SɒsRNM@eIe=sӈ:a&?GXi5D 5Dhư4&u2aV >PhS_}׎*}_}Ktg,KMNvsck\U7]~8u1ӭ?/{W9Hr XI;GE J .PSC Ц0#LRXIMY|]P B"LS6x'}oer*eor>}(6; €&vFL(>B+,ݼǥNh(pغ=#}7YY_gEAYp&FpPJ>&FJ8b!, 1MG"͜TcF,rP5sf'*$: >8dIJ=*A@B)ma xR$P'pS. MOK>P۪#s_yEpp80UUh/7V]oUU~rWJ}2I=̥MKjfPP 񵥃. 3P+ӷh1 {,dH[ m(!d:P_}+c-dJ*M}k'Dy$6pAQcy.&#oj3UDI &)DPmn|؝,`I%^edL!v;ob:smc|<$ϧwGƈ!J o,m:1-+ږ6©F+Ǚtuo :/_pK^K(M+ƐeX)MBGXoÉ`KYFL/I4Џhxl*cm|u״/VE?5M^~ H(,^MT&QA ( *?3{.FM瘈)K@5`zT'^DSP.4sm#I85(Td`ûok/ʏď?YZ;7ΑtJ I. {yoUzY~Q5>0󆚚\akkpFsxɧ8|k˫--B*`7#@@MYH` |1 (5T^מEidA eZ}FXI.˺?[G;d4Qa{gN*/)n]fr}ӽt6MFWTӾi"RZĔ譅տw^P#v=cp;y+M7W $D6 L츈}$٩ij),@Gb1X/ "(x)K4%W `ԃI bI TL9kP%!^]+?{s>| ƣ82c|G۾wk 9n\d :¤sG Njn/.EkN/{aʺobl@ԣ"IgӤҩMhQ=& $D(y M KkPg\P`R፺6ާB V,LqB={y{gY߸G휔eiR9\W7V}RZYQ~[+@sǛZX) EUܸh} |^ԎHI%D茨x L X𓚏WLb j@q&|@М_G9}L FhQKVտz5ug<@:CQ<#b~kP]VH7eꁓ4#R5Ȧ`.eq?*$Di T 4,Ի5H%Tl7AB^'[mMTG8䄩PǸP=ޣ{hwh8dq8w]jWSclUwM):.SLO#*\~ Kڥl&n/le@)41*0epWAM$2jPk365' 2 A ټͻo_ӟ{j*6`T(@IHF\s(|PR2fuZ9e#M0^JF룀5F#-\Z~ЌƃUxL2"7ͱFQ1 Џk2=шʫ/矏mvNvݍ֚A~4MSx7pOlTڙѺ"S+ @֤ldɻ9yn2h@ UlhӞ: "@ (v tt;xQ5$Or2ɘO㜧u. .}j_Gp>'loo~OW𣏓Hp4`uq- D0(Y})ڌ0n`IӀ$4L4^  oܱ9_81 Qzn^ƭ8N*P^T1 "Ksb]]dwyމ:4mm&(-|mtߊO߈0i|c/MrR.>kq*o|k,/3ID*-pUf{w8'6 MD5Ͳ,h7J[x: x)1Xj2tYk-rH*6i#H&*VMA+dyӟyǎcy)r<Lj݃Bj(l[?*8:$IIl`8إgXIb#CY5;.uptx۷p85Zx=N_#3g{infԝ:nK-hрMx F!GrbQ H%1=JB#&6CjS2VMT)K?cwg>Gj&Q J}n_EsaV ":tHF)Vʠ`ƝH.FY8; v33$ClMrڬE4UL\q>-cdL9]e/n޸Nm&IRgfA;vUWU$ͯ@9@8ʹbFx!,iK 0K@T{x& &3&2I@r "`ꔉ"ff9zY'ÉZ:E4rKc?e#D1$YBN+|Aɘ>ĝKH $V@mLK6SR:XFEK)<bNII Ā6O߻JfYFQ#GȏwڤW9d#)#42$5(e@Pa{ yĿ-L"Bq VTy Uz`E%59t4`2\y2ǏqTln3BD0I[kSivWTc'UXtǓ9X $TclULRUnk9q ZVy}.ʩ 1ܹ{6ѐEV'5&i'G!Ф-kWx+>e?OO3wn)IA}fZC+c"bD,sΓ&='$A@TH1j*rRXnv: 5)=Y{ xua}uzPC  W>\md biRUl*8@&.=?5Q y~B v\ D#UYӄ8l6NZμst}Uj*LBnmHp E@@䝜t*%8i"ķќjV,6Y 0C?A5&]Zm><+|yI5eXی#H!"ع.WȺw3H#4 ^/^yLo/y$Xdf4DMUndLF3L):sZgIK&<<1= s`H<~ Aj`hss9`|iϟau}~66Y[]Vp$iY)k=2V,/,ٱ"ϟ./IXvl, 6;@KlbS{H*̾~1>m!M2=- sn>Ö˂ 0XNh$ 37ƪarPUKXdck#Ϟ8ߑevXC<G}^Յe}dӨW~ Ȧe Q`20F4*F*`5ND4> lf52Fpk"365"=_Q ǃO^B B5VF/ t.aКDن,3xa{$R4x =ACEXJ HDA^ 1y9c\k/׻0W摋Pn`cqI rABY- 8Q+u_T)_Ct~OS [.Zz1/J~Ugk}7^R]l͎lc>9QiYJ{1yKTtbޅ)BTRx*4JeMf^Mz:}3" R&7>)STYHg}pUP 2P+ thyݗdLFl{0fbZ2U/`T+j &Y2NW^jʜgfgDu H "Z +krۄiR #hL_يL`blTCVB(HQ BĪ䜍mFH-F2eVq!9KSM w-k'iZ0 o>LLIǤ 1QF[WVqc #nCuti__]Dg"մ/ eeP7 [I#3;ȓ3E'jTDy ޷0qhFϸm抧v[@^KrB! 1ej,=v'n*=u^oя|_zϝ?fw"vĆ !U@@PD i+#kWTTju;]x0Jygv 64 53ІAg=2Z#1u3SpEk1P 9(@ٸ)$zLK+7E F0X)973;;d7R4rrklY/ @()0֠nPO‡ ) 8p0+$M5_XxHnw x" $Zz#\LXP"st2bh)@E!׌9i4H 1xN iڮ ~ڳ| U-sv]-aG3q LS{R((8]Ɔ?p1H@Dw.}5htjc S`+X-O|%6}#hjudroez.طf;p%UFsjjOI8xc7P%q  9zPΨǧT52> ~%̶}K ^KxzS@TOdrzRb_p^7KJJsMZ 23垖(m6>Fo<i-NqhχGGf ȖzE8T0֒jw]Uu゙߫E/>OҬ7Ic$*ٸ{,g'hˢ*S|Stt@5ͪ$f +dT_<nizo ^@B#(Ͷ` 8(7 (oLbn*`ĀQ֤HA*{AwS{g4*aY0@f5+ i2kzN abl7\e6;9{M0sQa'PͪxTQPF2!T܇ŭI:#P̦t$ղBX/hf0O1$ zR>K?R1zMT$ ^KG %JtclF01h > bh"Kq6Tnmp[dLzZtȏEµ[Vq'^FO> #Gz ֚dtt]7jW,|13J*+U;xv[fg;<6!wYz} 0^ ~*Єhk: Il?%UUeC#l;K!Ӳa#QNa8WX ^BńU j4}A/\[M$Zj fgem\8N~je?{ȸ2[j Z~Vw0МnQ恕2L :nYKg";SNYR`RMLYIh u HA2=9͇뛫ʎ& (?L  Prgm-JUV=* AjR! `#jFYSJ4UjI*Ƒ5* a3A$ "37 0Ku[G+9ïbsrΑ`闖V̦^`Yʭo4ztrr;} fg 4g'i:V1i~*|wL# 7n(&Tx%;< kQ: F 2 n+LԲ ;PT6::w!_'Q1lnpXĕAC>-.RR:/\UMx卑G!lzr0&F'(BE KA7&aƋ]T--4FGKQeD̃A=2K- t~i _(0Nצa5җ=1V/@n$ePCV~$ne`W}p:Fݪ*.V4(+* 0.æU閖O$s .RB8yd(p/F7  GMxaTa[|?FizjYþ,?~[WY8QfGpZZ`S ӜhFqͱt(N`&5I*F VE0b z@`6^ar' F$&^{=A!MP^um4ă e7pܹ<Η"_w=g ִaOR5ޫ'9ȓ zo˦RqIx]K~p"-dmp$`s (!QQB +בfyyVe-YcQQx(]x~v)K}/P&"dY}3Ǟ՟Js.-0>S&3t^FU n{8~Ԣ[H [d.Xb`p9 E|"5{}W~sX_{GRj4zbni%ݕ$Apfӧiʭ c{IkrV .ֵukM{z~@iU%ж]4L #F8FoCTJ!%`T,@#"*Pz%{0cjpnqۡSgu@ZϨ'56NP3\vUpDU5k5D`y 3/)ĂAX ӣ-QVa$H ҠcNٚAU۲~tf)[I1p}.8˫YkŃ.^ص ԫhh) Zf+HrJϕ)@߷/dh}y gyHT[3(H 0 )\I*)jJʼ $@Q!2RA0f4>"@ɵrh\[N}\˒e6l|(Hm️[b3dῡ@'JVV _~bQ//z?lǁK1TjժCF&nD9~$Y{bR.1Xޢ23خ `IׯOS2ZJ8ܰbB&4 @SIJv+=A. uA`0`|9}Aȭ= MRZ9`.+:h> G{3g- VyJUeFo<݂ :D@18Ȃ@\E)uO#zy4%ݡFpo=A`k5vKpM vt#I2N=e%2",gڠ-""H0Z0QB=D|Fۛ\H{ 21.),1Hx'q ׶StQZoB2tAj^rIq eTMT%Ak$0@_Hh= J51d o[5txǞ~"͕]l g9\Ei7|"8L1cGsjmֺ; њg|f9 bÊ13 0@<A +%zlGSD4iOdX@3t(6x^4]wmU_R%JմZǵ:ݭ777?9OڛUW~'gg_ 6;T^ڨjIKw` keA>N=FLe0d!nPit)X(ǗeK$A5֏(@eDX  so<|ep%/a|tTcE<2n8U-#3̮㲱Sacu^GQh(yu]t=:tipΕ TBilT#/74x2<kFGq>ӥTƋ  -ʼneT:7o+Ӡ]hLLMijyc 1 ͽ76pA4l=qΓs4 M}'6GPЀX*}ތ>6VA4F>ކ _x}<Իp! U_w1HubJ7E5zԪ A$mebdƋoD=ݝdָ=rYlvݕzѱf20;F,Mլ&DUV=B+}ye7qSh;_cjӟ(R04̟mDEӣC @662iR2ŋtX&ꓴ-NSh:G o =OXU){?yZ6M(9+m +=\JYG[. p%Qrrџ>[7H!ZʣPFbDGne//)R̖6$M5=!_qlpb'IRen/Q^ב3x`zq7 aW(=xC2:jhϽ$݄5Li8 G"p">g|}, PyE\z-ϕc0aii#W[IQ䵲te߈Y?yl=ά06#f7Fխ //&65}?88 zغtZ( Bc巓C\|R.BބF{wq0L%Z á_@|G{c-zbz^^jj7/|+9 6~}(e@Ш6e-\,><#Qv߰ձCE} ;s玟XJCHSjtLDPmik̂3ϟT- y/GUXx9mJ)N)8) CmHT*J|`FVT4]@?WB>8xjFRO EiNLcj PvCaNP\4v`WD2zc`xkKTz0m@b,zK zs/ǮD a1Q!qKZkww[Ōbki \cZKZ51r1YVymR}S{{`M믺P w+?utlvȘ<k^{U8v=EF ǓEgt"@,UX5[퓈JBƃ~Hm| [Xc^N8tfz1[Qr32.{QReL&xR+YY~^|/D&Y8t;`giGzqH; obfz4A3,wtPFe ދ.f1e78OoyNj<2̷>пE͋R6öqSh"`mo: &Mp]pK_K<q\grg~.z 5j*yH\YG`RNy>l\^? $zC`e(vme:/ *4UA( )9JCvPQBq.2C ѹFƩ*]l̯]dF2=Z, ެ['X\Yg>r̾A|Z7 rƩ,4|H$RN :T& x* >m' Ssi$c>~+5^0JZ,l{d4TJ217dշxçt3,~'O"2fj{lu߭oo.ֽ8W 7STއ3-blawP}J]P`0۝S[kl^02_W(kиwʞ*u*kfvֺ֬o/HITVy !_B깤]ᐁ'ATgAPӦ i$ƖΩ ymeh 0 K*~2ZԸE97ܢ>-S P '.KjJΗ2uZ(ybVq'*l':325Cj#`X8~0"`6GǰA9X;$g.]7F6F0FeʢMNAg߈kC.`Lj)6KAG !2!Zݻ;sb*m(O|C. 51XhU_8Bx|Ww{vB Y..] }U%X 3re +@\g$R`mUN`PFHx!zJ#= A+Xt.NY赔V2et$r KR}Ҩ4o"AjNN놛t4$x)⻎͏_`c`X {3d{,="s ^5^!- $q UJ˂^0 bQ& !J]2z{8/:@q++L2Jpś6qfx#Vj[^`;|eٮcZxĚygiGYx.nwк~7)xRRJug~ |^ ˜z$r =4̾=Ek:c)FhM҂^Ew`cn3Ϟ`٠9-Fnc,bzݎ;bN_8EMg$ffqKQf*9.{fc"mco$0m,K)/>r@.uA&m+@ xT95H_S%=`=3~`}y7M4ú+.{x)8稶yO=mE6*&)rle"Ce/}WIJnV}dA0^Ɇ=!p[Hӛ) C J/yo]`}@r<rfjZilO>g>7o|wlm;,;< ?>ӰYdaXp *ƕs_/ѹ DMxyWw'o̮\;m}ZZ46^=v##TL]3>rg񣜽p" phgbvH{4x%z;)45Hm5+ xXxV2=Kg/j親 Fup+T-u:xfLK`}uM'xЇҞȣM|f[uVo݇ݔq h_߇>$K ~P&Lpy4R5:fu=,_~6MVE3Iw'"Vo$~9/AD0y FMpk@hܢ-q\x}ʘ4/|-/sة` F;!ėK㭴- O#p" AV*<~K/ Z]7wK W3t) }֖W @Q#%TQ6P ef"kLqNjWN xfl(0X4F*1˵U&W&)[&@nʧ[5y3PEs&i rroe-yNdLAf4( 8 0Cn2p~p G,AdIOt{#½jJO#<e8 3!]# ̱;ITDL)%g _[?NiZkZ4Ewxgl:F󐢆FYseG"2XY1Ԫ :Jmy[auIGۣ堋2Ue.a<!,ShMi&t Yn$_(@P4zS1l4i7f9BgY?Ͽ9Kꕚk0Ox˃uxhs̐9i x>+ll1(rXp~{ﻉ'? nn+_J=yɓl(52|+ ԩ(b5TD%z@p. RI4MZj(9P֨pCu &LrsjJ'p>2: aLyq |5IiK J ]} vh2Wf)e\!$7GFH)2}00\>w]qކy3< pnPi$K2vht\ͭIC~|~Мއ\'DaUoc+gzec9`u tJSUU)Y[ZB5Ld gk/G WN<ߣ>uo~zR 1|.+\<֚Ӫmh5!zN-ʄ+,_=srP蜚J7iL4nsvrd;\KbRF&'%/Yi}ի@8/@N,z,\BP ޫSEF0 o04Y#eq(eAfrj;?HGUgwP r>edPlvG8YX ߆lf?/֟bf("K c7wmL ѝ α;mpNCQF0%<" gJ߳'nqRhk<=i@g'~i' 3_mԚo%|Y4.cwH5nV 7O%6ϟbq9vݵy]HP@  $ţ&<Do(%SIƧЅP66 y:k=NP3w}W  ' RxmkƇ23;͋^2NxNgdm_{3'AeI-am~a{ٶ8?2.<ɣO+;f:0gU+I3(x^~͌t]&!E:$VR?K(%"5˭nNx\+PkT"SG9;?{VK<{?wlRWd6IVp ;:qs˧~W( ֗zTAYVeQe﻾,/z+j <~dU\9AʥKaKx?Iswwg+dJW*#>n?|]9YFk^>wVeQ* I$WMsP1SpKX\7)P2_8 Ii5#Cromq .K)Vd1J)5sVPF ]~ qȮ x@BM^ I 3̂_kb&p͋9S3u;+獘o'^a)xl~&g'j6Ϋ]j_YN<܇ =ށhB0ΕyJ~|92wd{ljoWk6&;=p yO\ǟ {4 #7M{-⋌E#qtK\ǏnS.n+1yOkt~2"ZuZ7,'^?/#y4Q70\: +A:-^+J-=g/ǵVɒ̕y1o|:_ߧa6;T4$>E.a|cл|ёFK*_.F1wh֚+nN x?Ei~ms?a.<#qw0kSǞaiy @/=Įvx=#4MSu|ar#<4˫8_-~h~8C܎GP4ԃT&z덷~rtj d#c>K]=*\yѡszv={@M󲄲&b)ml4y ] z"XJ,"c.胁Ȉ"(>T|_?H]J,1~Q_PBVhr|+=7jUQ& *mǤrِ3*DdÊx02,6a*9N5,lDivL;wb/َDtl=KǞӴoKl ~4MӟovfٴFc@Xe0q sG?rl>A^~۸Н,,,rs8 sj2,FJ%"[?c#ٿFI+ٵc"g} JB8:>1==0 󗔹t.T7]s dCN?w(kH}$=ʘОQm#JubDA  N shhٷ{?6fk {3w3v$' 2Q@A/p%^/>&z N,[g'hT)= ϯ-y9?00*u5<>8cqoU 0Eq(1Ɨ\~!~;5W7At_[|C0 41_18s xD=~) bpwFGoj4[qn:OtZaX\],OorWIxxw35 X_[gkcA%+KΡ3,O#l_^Yb0_a-F}V+)JɁ^Mn!9lKF59Vu'}"dFpUTZMb<^cѐ)A=aͫ8 (w\_~?sML"d -ne{sa>*\VVM0*,(äLm9} YRe9BQe|vhj#z_]Z|DՀb*)F?D}|F8׀<(fw^f̼ Hك _23333333DffbfYc\ wtt[JļIz/_=4t3`nl5jhLJX1کp̤\ng-Zf%e(>2ҒASUU%=D9aqnUTT́#?cO(ΦX/IQzj.=e?-ҙP_Z+uSo8Xdct\N80j5ECE{G>bmhl蜌Iz&m/-(w*I?Ɩ{ף\*YY~ / 0S9y=;$!J Dړ#L5!r P$+€':j7zn6\1!Z@% f`yQ3촃 7p" űFQh6q Nt<)5i;7vdP @*B0^UkشzxH3CÌh8V< Qp߼cєoJ6s01䋟q1-D`^ٯ$Mxϡ)I}qpLE ]/ %b ӘhrJ$P!yR3&7@&"W4lӇuq/&3; 0lAGL,9_Y76XJMf-B<1bugdT*6(F߀EO~L.ar> ݏu}=`,x8D6wd j``Gq!  ;ܦ,Cψ㲼OYcBP,a=kp{!m9/g$z6LYhZ GB*m{ 22#$!&@Y|4-&"c9E@B 0@7w:,)30,,* WnkF+O:!9h y(+C2Ճ4VVؽ}7nfW+0zx Y){bLx V96XD~`¤WF? "u:0>(E"D,8|[SG჌2mSм\;C1MNWv4ҍmcOÏ/@3EiXd[DD`S&qH~dKg2 R^SɞLDLgG|jtGr]yߺ\!ᅕRN2XU1`687׼tʜYN^'3^AOziͼ SƔ_TQ3S*YJHF|ِ9Bt #1* @Q2!:UK@YPK PEhQ䱓رy;ƆGTVI,ӐnWvsd/$ TPAP,XTˁO`"h]Ue9xlnvhLZiR6Ŝ,ZF` cw,K Z}h!dP21%uzKO%jȧȬdr߈y@~ɶ= . C)0Jخ},eq1DaXj1мuSk uǪ&q܏L5=l\B%1y/x+&/9e+΄%22𦤇ZǚO]tcq5oz5Zȓ֝Jeת:CI`Jĵrݡx k6X6ux)+,r 4H` ٱm'߹c#HP8ѺVwN/D#80)e3>t| !8"&v 'e2#-(m3A`sG7De bh{6L'<`}Ql*ź1ϊvO!&} c67 aٛY̑S\d-;Pd'XLyilЃsHutPjuUBIAQ^Ԕ($GNnOusC{*aگՊn``ᄑ^?lqiR)} jp>0L&,}if0 & 6&!BۓEX{ }Jp\|= ` Hﳨw}_$eXfyU)-`:w6!t, "qIRL {:jD2ۑw$&y8~:lg-N6r˱5-N+qOG#:4Nʆ` Lx̋Jk HeB$Kh2ӑ 6-ɓ\xJ])Ais+})8Wl $LeI2y̮^zȎ{NXQ WCb(ߛC=#2czaZoohj\eYcsh_JggQl L^ WŞ_w( x{ބG0K.CMz R=!0O:EהlcFĴ~,QF~_[M@#u轰hBwǶuq 7C2H= ZD68.2Y8 6" Tx"G$C#4h%`6ؽn{N ndrt Ёd Jf af<+Ӳp8g Rnڳ^1q] YgffE%VK2CW˜#`3B#> Z Kr<{;o:Fhخ- ?,Z.TQ%L?.&Y4a*G |$}HIfRv`*Y'.LTw+W0R(b΂9h`VBC  #;l![A < "7{)ęG?-W/We;LziJM}Yd֔O*~&ZD%6cP{D\>W )|©~YkYG[WLfK2o5l7kz!#Z/[S|7h1G P eSs]nU&+B/f}22)e Wm~f5CByzUiDδǂBvLA=>Jy Z@ plەj/fOCnze/oK_+2M4̳GKIHmɞT3\2@YYJHԙ2Y$8Z3t>DP=w,e!JuӼV _ىzM=44LǦc-ۂ'+_NQD\n %lSlbHH!N!ͨkg>< gyx_\C.A4!85 9PDfynhk`&!1uz>))5UDSEfAkѸvh:z='ϐmeHg(lFe?-ABÝ qpjsRFڂW7Q,9D2цȋP){>D=56S/ˣ K9΁$ `W\Jɳ_B~La`ɇd)K&B(`!TA6gyr6Ɛ c\[`U>cwtݭ)m5\)T}4/7;z7M=;iGpt6=4[#ٔM L >)؉M|?JGv!:4yc Iăs//jf IL_GB&ۀP249YSJQ EƉwn:q b9^JDOv`LX(B t7bH%Bp M2"7CͫRc?1)2wf)3M3,wUGʏo#,YH.hA )`Tɤ>BŘH$TTqo%I`i&)yi64xxg0! qL}Q 4jDc_W=lAγcWw,m;.x.RYjW(ɬ"9e+Km*4s*elI~r |+Ēe\E鉀A%Jf@z9-$?DFYx;&Ul :#tVeEU7Йy$IYF\(OƉ6׏0D aedRҊjv m"R Z{t:C`S.ޚ@3)kzj\,P@?kȯ!jԯY /" I ^-`4EN~.TZd6"3 ȩ"Ke Д Ӓh_K6K3$f\9S()g=F96XDT.I;"Fz70⧒4)Ta9iXczy/d,W½2R4f/{ݼ.NJI]A Қ%hh3p,I*aG5  Kr#rL>!" Rgpp- a" CpΉj'`d2H9)^͇k0m57cL7(+/ҭxMZط {n P-+-h_p"&ÎJF&8P:p@(zU{M)=8J݇o%8~ߵXn1jU3 Si.c񱝶e4kV E lTq_./kxT\џiz o-GS 9ϔ jUDm܆Qe 1ɹ66g¹BP V$V*6JK^d@lh#L>ة#IrXH/f:jÐL2FFQ*0qԑ!T5v.4FGȌβm467 G_}I3->K_1Xn\(vW?$e Zb>s1+Pّ2H@Zٌڨ7p(j#-?"- Ei#?12~ 1 ^Y:4$Ɔ YMl*I7׷{-0Wye:2m̂;w%{mR0 Be€"I M"va zfΞz |^>gL$(Kkr`CऐgL66W(\mHc`y:4$hB%Y%[>G^pü2M+1~;сn|BM@R@I5w+Y̼ XYu8~ƽݡ7zM2MS8rj?c>hAiN -Pu.PK-A$ߊʹ~Lxe,9{l0}B0h@UOT`2ۡu=S (&FK!)M-DwT$)@ d7@ )xnDuB|c974Q>ex"jӀds(,|0" SB>ԾnR&B|;' 2zwo@qj2(-E̱:m;' Ћ;fvaxTu*Y?2`SCw1RCse|CeY,8\{ԙH3̤0Rv`R!\! (Ba]2Ga,[?v[4_@E%HgMPScpCze |j>RvU 'AZc0vmځ#8峕qNƎ?mz? ̷'`+?'Y0Kh~?/%Q x Pum"Al1k\(à38*enH/zV2ZCqt'?EԷkZ Ӱ Z|scCTf-@=Pzk$x<3ې?fMXx#:{1XFJ;`Z 6wM 4S**U(6s艛68LK6L5 J$&b i`lt;(&CG<X&ځM+90"5JaG 6^!`ω{xfQy?Fm+hno4Zf/ Ͳ*8f*x.3Oe߄lxq0;xQaE55v-Fӯɻ )f AvޓQetaWFΈсCnĠZՇl6Oe[m:}#mLcj)@=ԨM=.nHGQުW}V&SL6 rB4Oy+V`4ׅ+6g12ͭ005!YDR2}g{AT+!cx{ތKH#zC/Qs-e GZ-7*ӆi:[MG?==8&N٠+=bd!)@wťU i'%(aFutZ O~%?` p&<uJ%ކjM$$DAwp|{}U?BE4k DZa>aU({Ҧ٥c"( Bq y-H²,afo+AX}q!v !Qz\ϦeSNk^F<0 yqm2^KG*~&Mju??p1YW<ᑘ4u2$ jXB%JwM@_O?~_ẟ,H9u1)ŠW|,^'(aw>[CƱƹ:rӔw~, 'TyOTKb;hpnq6ӋR /IGT v6^ |0$0 ]1& qTs:^gAgiYwWEl7 Ά֖K,ge̫ H L^y;4DrvyԪƆзw#rȶg]4P?qfԂ*.yc񂗿gNO-Rz&Tkj&pq|E߉nd3Yt-['>XrRdrY 4A  X&4+jz/k޽ \7EΊV}`M۹YTa#@l}'pO$\?RD,] RmP[sT2;dGݩ8Ww;[8yjF )cMP&2),į!.ǥeR(.ϘQ`D2socsS3`SF 3d3 n]M`|l?|؏/z͋i^y+YBCEI~}B^s@Wd:adpW~X㝈 sre&,7x-V=`VǎXftfF-YKm_$@,AY'…ЙҢ?=԰/6gz3];w^~lۖ]2? 4MӈI/Tj,mqR+WR(=f0+`1c z07UuNds-:۴卭ci UV =`=ZH33W_C62mEXt 7G5MӴׅXx3?I6c*7RP*d8^u\{+* Lâދh /'yUS4jl2VJ()>Qё19tuzEZWa&a?0ٯeC{cw>ZW)zt=yBMb &\imAȘL3DVƞ_}MolMϼe{`Is\`l 2aMl6]/LZT2cm~6bS#"j=8fF ]Y*u,yYEsMӺ,א4m;FIa!|>|njL&BaSlU\>t7-O5yn:sAO#Ig0uL̗֩>OxلFh2l۰w\s Μ8xFyOTl@!sΞI:#m.==7߁buE$5<)OČY$y|#xTײ&8fg`2F g-Ē7f5EU 7Q\̒$=(؊رy*u9 ͆rS ǥLinh/.8}bxtv˶QF0JiLJ-)x}:Y3.hmia#:}V^|.xX|b*wMгYϹ@lw- , +]s'4j區'Cph`Ǧ5q!8L6l6GSɌ^|!җbᲥ7j|33 a/^SwCkn@ȷMBǜgl;6)1#6!FQW} Ń0~q`uf9ԣ+MFpݔhJ粮n<@WMi&J0|SEMMi 뢡ZNa*" ^RwZ1R7`Pt>򙲋9!mbw;r 6mW;p{,8Csx_9p!AYշݎ㤨}WM?C0x8r17CO>Ri&ehiץًL ”8(|ݿcFcgGl2:R GDlQU`S]x~G85e<&޳i^N?Z}v,_D=Qzܸ:q+ \7֞YpmH1z).z̥8k,vPD`edL`^O*{vŝ7ގ> 0Iwlbβex\ 2-mێ{`dhc}}M h5u\ ^i]S5"<}LKMfHhO2jFRb{b}FJρsc:6EF0>C1?J_xZ5d96-e瞍bɛK"ېa$0itPaDr^7!CZ=aldN[}fT% x6˴h*>Xqf2Og [֬!}T%RN2W!"hpca8!t|쌧+/'PnT\떡j}i'?3T!pS{߆EhcL6PVX,?lYz3'745u>؄&W1̈Dz`3t 30 c&HtNٚK_s3)g\4sX^Tp< lYÇUOٹjcǿ"`SzƋG}ZB<#UL&G`~#N f Ns(! \.O@iH;64fg.M t65b]0?ч(ςw`OHf Y+7\@Bs9Hi-X%k|ƒUz^f&,ԣƸn}XaYTIa.hjkŹ0ޏAi/YXl9MMH30mr^6:,@GQQdh`8'FOD!~\ER!ӠL'HZן"j\{Lt/3 q{ $R1mEfeYʢRwMs$"$XK%'d! Mk!Nxi}޹՝ Cf)<[wSưw~)+,N£cF1@nIxb עzs8*І\8:*ZFeAK)WQJoEK6-Z|?,fí ݼ{HNҪ*xK%&Sei͕RIYNޝk/37WTŏi!|뾝s\3p"[{䝜4I|FR6\Q@b%jkטԫ|7ۯf9cL#ɪ(OZ R$yj*F\i$~CE"1&_} x.lqyw^lU+t45;Pe[cZpg4DvN}^zh#{(7`S0"D+H*h-KG+-Zi~vqZO}?eU>gi(&^$iU$.rPg5Z{VVpѪ=JS4e8sCDqkLv }s,%RJ]7hLk/ƫϾX)yK(LgYNS5% ƌR _7e!iV˼p$\]|}~n(JF'8GVx!OOGit [ڱPʈb7ʣQh /}3h|荒AL$7-p@hQiȴ16XUYߡ ZicI:-ʘ1muScҳӱ6G,ut]5MJ (5_וViEEjĹZFOR~Z9YݚjE"ZibTn&TQC=E"I DgXڅ/yGb:I4Iqމ\1ZԦ=hŜ@QtB'IPVyo=Y%F+ r80s,D y,C%dalèm3wdMn ~1T%5w ҄^wKd9|@;\滞eQ=9ek _wM =q*.Bq|Փ_)-Yyd +WIZEtuJq".Ā`d{硳 )MNbiВMUn4i_˟;vnUih0dpΑMCb L+!BjOdYEBv{:LV%)8:=tk'"U4cb\ ilJw3z.ngG'Z$K\ܚ˗褉XK&iX@R#jFYhZq~k>+瘞q!QD O(<888x ZQ&fyd9-!H0\B< A+Q@`l+ k4obeQhВMnLJwݸ wt6c3ʦa#A{^y;JދEc-41@6isJ ]+Ak_Yy\< IyC?+ヅt OLRB3BB֢ĬЀ޻yRx{D /J,"wLaݨƧr顇 /ŗbd-փ5٤!Xs@gb4-ҁ4˒~ĒT(:9zZ܏lZxZ@hd{kK޽G?SN0bP|) ##Fݜ-b-6|@%aJCwhn}=uӦ}L0bO WBŜ|7㽢ŗCK6-ZUyEL9uG'X^kh<*J>YD 'gҚd6qJu*YqbEC&@R%WuAxG,&VAHȺȹUxT =PƽdeY&fˢdY`!Z#- y㕖ߣ0P(LΗ7^j{L $LLyި .FCqy>G:<3x9O/lZHL2z{J)cw2֐$Ă ,*0r'YV8xbBDk$#h DkI=āK(<˚Ib( wMd֌  Q$  8p>*aV@go9+Jr$8qXuaӄj,E7|.Ͼ-ՐXșH::ߧc7>q1rv 8?L$$&I6yˢٴhmMW˥u!76_օPeAN^:R qz}YW)y 9!["w(F,B\ H^]8%Dw E8S|$M!AiyGx'YzS5 xdmb}q'Ρ1tsK璛o7wXO\cCM`3㉮3\Z#NqzD=ZiY΁%- eӢ|6}ӾIS, ƃ~s7{ l!YTݮdYNYV5NG‡ -Jc iEK(Z<Ō'4MCc1VA@D&O994綶ʳRM,Y6N1!\ ϊdfQhU?N{у HupuFbqܺVYhl$%+y'dP"MS5ihVhɦE5ڦiNUI?ѡ̧".GG)_+P :yuפâ%lkrIz2>X!gy`\t.^Gf`4fkwot{olZ|9pzry縴K;x۠jE=DƇ /q,)B((`q/g-uL&ZN@Lk&ZFJIKH&N:т:gQuN'7^. Z6qtvʲXE o(NS#$1(4e,3(mkS_ ;nL-t\ !E`T$V)5YF$I.h[%-f,ޝiw$Z39G1.bf [mFPJpZ1>ѰVȩ״đ!ia}9 VI쨪Cz3DL4AȔ{ Q~qƬ ^K4;NgKN˵MƆ5MDW|0[̘(Vi 7ҋ$,qxNXG$HP:khB/4uM*n-&hy?OϦ9EXIDN;TϾ"Yj kH,Z2˨g*QDUۥf gjΦZ&r ODtNЅ<5Rv{euYC]y7p.ܯ8@*6A>=WGy6q[ieZ6-ZI$.?'}dDTȥNIDr@{|r$g,"Z,JEB& tbYd$8X/#i89E]Buxn ɼDy"u2kklj":Fy;._(V cR޸{D X;b0(MٚL%:yƤ?`oΝW8ꕼ8=%^KsoO j8Zp:X0h(I:tNRƣۻx+ommmiVhɦE,]EZ ?s?ڲ;|(i&.% 9杞d aVRv0!u`:Ҽۨ!2cmH+] Blh7HHZR1l-%GFմYPx1p 9eXu٭];߼ݜ,/~lC}rx+dӢEwib)#1j- EY_J)@@@j45i*u6ԜƈDc~#L 7k4jk ֖Kb4i3KNRY!03d]pU9Ayb&yYiy4 A ")Q'{JtUo_>Uowi)'\muZ6V2{M dhc#jeIK"Z%Zib=v!xsBze6A/C6y\ $p2{6F])P q?$](ihɦE tp^fAΣ\K'f9#nbb wVnKڵ@#۰pu6gVuU-MEE ;lmmYn!o6vv]cs0ͬsǹ|!<8? X)krA>GWst[{䝮7Ik;xZ܏lZ0iئe1OA ]S31dXo;29;;fS֖1򌨷LJK* 1HJ~ x/4kkS "=uTXņ0_ 7%2HStH{}njb(jǦ\MpT Y,DvIBiw'$/_{xo~gp:_,Nsno~&/_8x\ᬼIr4WŪha\{os~dӢ8'&ypLj gJrύ @7~ TU%aNNXW7\Z EUwg.]ۙ$LuĨ0GXN Įzn*aDXkYz`*쎦3Ff1#OŘS$Z]:IEo9GQIj<4;ϗ|7]k{n7^{OʲlRrzF)ɲ;98{7kQy;~%/Vߞ>yvzz*EUYF]饊abSJk]t}`RВMibRw 98۠u@9 b!F0lͪF7.w*ڈY/+Ra֢^6;0SorKFI$ƕT%S=jC#t9Ǣ\(!&j:B !IDN!rW:=鿻?Gߐ=Cӳ8~goݸ~Ѳ,,1( y/UuўolZ0I%w=R[1I@r~ZHHJ=3I+㹵qpcxb[`l$ (T !H  YhFݗ|6ү7:pFi}n)^{g8gLgg,VKEɪe\u$.Por&dĒKh6LUMe;EL^|OU?t~Mw~jrgwp[׮8?9{FIDŹ?=yviVhɦE*̧Suupx{ROUALQ&<ވ cg293ONMb- qĦ[)oֽ>SBm 6% ek# Ux~ƙdM\NkP N 1u dJCОSr=XX1l U)Eu{G&|8*')34_@_i懿9~QWzdxZD'?D㧊EE ZiѺJ3ڸ7) mGٰ4TĂNmP ɕNGb2{tBDc3![f^0DQX%6 TE+V|6~ d0/jڢ$$ MD-8NG.VnQ[-1&stDOjyo<+3OIJ?lZhoƤޡP4b1Xca " MW5 & ^EQ3!Z#1 R Ѣ!MCB$!z8[QH bt봍A9'@$qn>=AQǯ]nZ+khJ\ynOjmPPZQrJћ1&a{8\.ESpZ vxnvZ-KhɦEO_zP$No0$th;wqu M~%^y ŘbQ%y"1$q坎”%UBئY\\\*knIQ6 :nc=g4뫿CnexH=pȼR#dӔ< q%ma1ŽЫJL(V;Jpo>xi-Z7=8g O F.;FyQFzCќ4JS5 %[?˱u8뒎14a}yS|耡4јnK{G$i* !/woچb Zqm{!$b9'1)y40Ziq 4({y^]Wr?Σt(Ňo{?IRhe`nzlZ6нNzUͲK~e-TZlNw$u%nh',$MxRbgg(zYνc!,9n|Jv9@e'KSćPAWQۆr^N7IImCQ(G:F3!'SӛL(k+9=JlbxK,~; 2)*NNQ c=v'\fS_-yZzK-olZ7 Px\ZIZ.YU%xȆCRIJ?1ZIzX+(k'|Qpzx,=@kҦf*Fޒ ɹm \);\y vaƀjjk/֘;FN_G7Y6-Z|KtuggY~q1b&} :5r>OŪXr&Gt󌦩#UA˻e-<$ ]ƽ{$7c{$J؆ cDjgÆf{/6tbk5pZW:N됂ZmPMxY 7j M nWqV5'P JX;Y7+k?b9?k uϿo<姫⫴V?o-h˦EOl{l;{\-T/5|G.z,}9YC?c\lSچF)gwx+<1|=邏&tBHC!F7Dc}jYN6*?™ĤN'bs.&ʭ՚uz)Ph! Ѿ@$ }P^1ot /T>b45KRufZ~+Dr$㭪`充D=5თ.6w(r,_MlZ^USE˺PO_'+>N4%tq*1b8vKk"EqO!BέytUmZob ՐrMI+1`PiÍDc(4GsQyxM*`\a6O@(DZ<&mՌ) :Q 5,^+?tŗQ$^\>8h@9liO>yΥ?pTԜ(C8bt YW'm.Nr:ڀ2~/>6t@;ёW%#@'^ BfНSFX#vk UJ p6_p , ToJqIJQ!a:'{/:K.ӧp$t>UJE$P']Vܒ{;{߻EM;<9 I~H$ʰ{% _s:iG%J+tؠ=g=jF}UzW/p|tD1^W]/ք(w&y;FhRHD)gką C/P+˺AQ${ք]9n`D5Z?ѓO.-wY6-Z|T5'N'urzzLguȻ2p0w96l1ƄB{uG7TKr7OQ:6G zTdPJFl G& p3v ֍|kFNUeM35~ J]&k+twxQZfqFy9I NOJm˻ҰO?y m#Ĥd=ZF6-Z-EYۯn]qךǹ5뷸*&.kt>@ɈU`e4JzY B6#2a(E8"ӏ'ըt 7\kґٱ@8u lVU͋7oZp: w$̱ZXh*t7vTU%;RCTיJ&AWC MwZďl*Z_M6-Z|<oUuWuVcUUٔcr^o{O5 5\#i*E@|Uc_3d$I GGdGU7(s>{s^kOcZrcj YZ SFY' Qې$IgY~mZ_I6-Z<{/npmoJ Zy΀ ˇG"dK۷9<=wd*P1J5JZҴrY}I9e(rQ@4M#1<]n>ĭLxЇ;>euX-Ε^ChٴhwYCPm?~_ćv{|>_,+ytbBH@[kVgʳ5ޢ) I֠Z[ N&~6ņg]e],BC fT8pq&fA .؜-@AGHdM>l"|PyJOvMԞmcR]!&iM-Do^D`%טNDJ<k,: T/u+Xf,ϺW7 i24-?|Mxo| Ord6,Kdta$اLhpp-4Y[g9!Mkd' 7ZQ<' 97zjDlblso\tuJ:Eg#@> #ņ+W׾@SϺt"{u{t4MpOR|x?W\n7e\RO&odG3Ѧ>hC߿g|2WZͻ,Vsiuwϲ(n)I@3[{+Ȇh]tnٚ;7ƃ.NFhzY!Z8^Σz\`݇ !5د3&\؈i6,0-m':_qkw"Eww0>>JH(8煈 dMI !%㢨>*IĺIUkf/y_Uۧ-ZBkٴ$s^||O?殪E8t<%~ MPDͳWC-vwѻ0h-bP<#KTN:>DyYtDyjU,>r?EKY'Ee J>l4nQJ|/Cq\Z!l>4*ְ^ްϧ*V✍,kRC\/c l4Mu{?.+ZAK6-=W䓯>lZVA5+lu xPZ/ 4]p nТ\pt^gVb6#79qGNKD(Nٍ U*RXbشPѣL[:ļ([(yrYIϽv]H;> ILQfLS,׃[6శxB ۣ˦? VQxTi|6IQ'-O.ҢZiN?`szF _X|[;wg/DL(V+7y<0/{ J*iV6e6Ջ{"LbWدU#D*׃5b!& DB5 !Q4uL͚A9 +>}VmI e_c!kȳ~H\a* )~_(57)>|X 4G*b޴N꺺 |?ZoEK6-ޛvr>knï~٬dPH0<w9)؞f]݋dYΘJC'O㚆Ėz:yW[!76-XIzq.Z8Y_'n(U֓GFoQ^G>[vMtɽtompQZ UȢta Qԍ[Vu8j5L3[-9X * s@ 7tsj}_ԫYQ[;}Oh|-ВMK4$V.ј;7^7yD`|ի7XJڲXXvu݄8ĺISAS0e8酪O/#Jk (6udoC@,TlwF}0Y%Uk' 1FCZ[ NuOS\Κh;_/^"u|%L @C j RU=?%AhB=MgxoY D$j|>*m KuAE%d+'{?n3_L܏~nB煣W5Nk&(:M+\ThUϘm4;y.m4]i֖E:Ja}!^uTFee&&I(Nyu$,M&gϓ8]8d*f`ËNp@؍VVgbhK lMv:d1,dźDxse2,ĢXۙdYCC9d{]zFݛ}ppW.zYJwx񒥇o| q_%MLΑ&f6F@I3hX,fbŝ^hD‰p)s] Šjcmh1EK&&SoXyK& nC9]|Kidl]M  F+D~'b,XHLblN< *ōV58^,Dkm!{P_C꨾ϣrn0,O&y- mM'#{ϟs~aY3ޚѮ槼qYqSӌx!+{N\N;x ^xۣp*J&N.Zq&Qa8xx&# Xة1ZU\Sql4q@_<8 ( /^1UPtp '"Q.pGo꒪*Ŋ1$;/7hYk9].eMUsx }.FS|/Ip*v'6Z lZ|?\xcsw֍+w]Ӎm@Fif'>/qpQ?OR o@ޥ~OY[B$aMXuc1 |b9_&5)97b1ƵK̇ϑd5gBk7-^;MYG}yn:Ó2IT EFzٔU#a  R,l>. f?5nOYt]&&-(|d]II Fs#^B;'xk$NZhɦŇ ~kq~VJ|h~=:xBGF?{'ߡe&;; 1&"P.gݕ~+ϠaUO@@K6- {>G}/xefgd^7~XZe9|I.uZja.TF@9 [SVdF*%22QFoZ!>l6Zj kgvns i9ym֎ճ|̫{Qe޾|EZ0$ V&$ D= XPY+5i/ܹw{geuK@h%+f'qA'L13aff"3 [5_.8g{Ug#[}n,QF=S F>ީ{Jn.<%+lm^w]Z 6:ko;v5t|riqc`lb$u- c|(Sbn;}GA(ea:iLOQ#6ea $0C`fbH{1L/Sg4P5Rg>-q%A=*IF,PÁͨ|]?/DҧWsiܱIV4YVX~|=u8D395(IhD>H5 T7z֑ >!f'&nDܵ}W^tqr|b Pl? 4`;yr|0ƾ5IW?أ?F{s]1J(_y=cMP9@ˢ+ zÜY]&/ .=6NꄁTƔS-HCiM|gg,ZGD}8F4Fdbp4hy= GDe*;sǸH_Dn/Pk0$F5Q]eiXk%J9mF-r4BM( ֤iF5tԹM ̴qhd3`Wõ \9=TX"dđ`}_^ P0"nRp\e1^?v-?Ub^\[*כԚ B A/('ZՂ:Q[^ch(`0cp0 UEuV@Y `˘rf{ShqЪAewVl !GyZw͵st@S1:@_Kc>#h0= Eދ4 4(;nejv-f-K;PÐ( ?[wnVNlJ)N;z 0d`Jr51/. 9IMkQSjQȧM ֊G['5 S3 톩i [_ fESih꯼ 5AU ?#q`st33_kچR uVǗ]%GjވD1 އM(<,A} nJsY쥬VaLha6k'̀0Z@^2U^|SEU=Gfj̎ ^)ZDA=K@xIzDD`3yי%r ֚ m3|_擢P|DwW)D!@Sg܌W$sQP|nWƍ/N*45ZOѠĚBI2'>eڨ| -鉼|k3޻Sv{omA8m&Hp2M—ۡn_~;_z Ou;`2\YxvWsѣ~a~uϜM#Vc45>)_wۍ\83$#d"^SӼYu:Hj)zz',bTCCB1$ 9ud Bg _{o3ÅX|(ǹXC*A'Ngys >KrY{i./)NF@]&qWN@7+2cj|Rt8& CL5*&hoE:{dGh6ݷ޴E|Εj|QǮwg}ȌM|d,kOJ pؕ~3~0kqy\2b2k&=~SBQL _y'J@1-@2\zܚ"KK|ܡ%z.ì~noFAcxS/WA(9.]F!Mk$cl:Akc,ZD<-Gv2cHs\dN`@Ơ͆p 0xyg-7c3n͜\',85ѸL C) p׊(xst:6nv)w㺀V4#YΊ<0L5S3 ݠi?@rGqRD: L#=z?&mIGB_[ZV6V)f8bKH 0:E[{jx3[c^o?sYjFaz- 0&V!"}eո!i4$M D^o6z`!{'ZE&)fv^2?0'/\ĖVqt5d0D2^];1?OMHb0դoʛ>U aGen| rQ/8h^. x=q~&F*gz*Qz|uw@<>Ra{|3.UCBoqhZqx5z1i(M%X_;(rSz%m͖seu`ɲoabJkJcI֡/;~dCǦ&'A# xM\Q͟nnomCH/cDW<&VL hsXع"W6'y:1?>6y6xч9{ qwazl)ddLe`6ԩ,&R4JSWJL6o ֛;;iEۻtjGє6'v:J*Tn CC{Jq"`Ì<%C20;3d8]G{=Ƨ7B0֢~d=YBU},QGn۵D21=M!24‹&g]57Gn{[Uwq 7om̋onVlZ1#e׏O5)H(825~u- P1&|=* "oۇ 69ǍIkOZq݅ N{Sz3Qe\M [:JLR 0t׽K+KA}5 t 'p)!6A( 0РhgW0*Oc.ue>iffczbTj >nVPJԟefl.zYAgoX`;VqkgUڪX[,KXرobC})}?pToŸzuR۲$o/W(:Sf02Y\,NSÅnZ!JL@ۭsǗr|qS}=3@^[k e< _~5\37H((4Eƪ~cg D"ёd5yQ0H='d,6j|uǸyoe$ <ǰsNc@PBrzg 7/Ϊ8Vr5eQ\4`4NG*TICH n$Lߣkb…&v~5-MB!`!_=M)glQ^u hvr߇N8Mh} |IiR6tplvkh4B9 4j/esE|zkXYL&1w5 yfG[_4đY ^wg(*&TWJ@2\g4'ljv"Ƈ429barC{ܰ8_HUn֪ޔTzʃ{ev~<t0ӔA'adkc+fM}MhԶz9T- Cj[%@-#ˀfѡɱ]?HgR I^),2\c-,m_Q7B4 vB#{aǼ@JM+1iABx #%iMO 1@M,RPztb %`GX=`l% ֚ghRћG܌2JtYV1maa| v Pn"(e5@C Z 鉓 @R.O'qܯҠP\d/MQ&١xRofE.ƖLZCjZ"ȳL?*!w+ ;92Iu6Wa(SǎK^{.gzv6,p JE@lmGЫلV*lbwG;uRqQA+1`s9Q19xסC?sϾhP&V#eUsS/(11]0w>0O.-!(?=;n)JY=)%k6@+6z:c9*o/~^42 ڽ59ըgpYG{ʼj[b'7E`SS7,:PY.5ije!J(E"@^!+\̥φ|c)[~+@fE 8kbcyeOp W(ETJ)> %Jn>8$,{578 |Ρ|*E= :zl.LJ 2#%T|8$fdEU0>5AHVFak]@96N9=Ss&У\ܠ2 hNNPkq$%-l5z'pNK{yEf9|0I80-YX2*3?M^orx49w,5^S @87CD4K#a[^s`TѠ** =gbbRWWh!c4MڝMVDpT9 sc#CzC}nڹ^V$> DxT{^-^!ڸsS&tzmЊxK|$hQ ^ORh*R'W^8fq^N[ߘ\Ǖ4n-cc (o6c4hԂ!6R6sK|vb ~F=0&;_*nBkFks$bTkƽMP'A՞a'FJUyh4hF15kMSY JijzA%7ggugs/ej )D6D*RJX Ox5qp뚸zzòdu0~ސ.x?i8gܵwQY%N-s4{d,Ɲa'i P뵼9}=:7d[(h5e yQ( 6VP@Ytm2' L7}ڗ\Y'?DSzDӯDx5lՐvvmJ,PX˝gϳ>@Bz±ZˉkʐXl U!E> e0`Q Ctߒ  7cM<6:"ǔKkk#ZoK`̐'M 4LszhHXTEffx._8m|<`JxÔ c8!)Y3Rz,;!c-+!'6: D_#rKHT]g4qY?%a_tQzě$eb竏YcWl0؉IglG.-;o| ef{St:s=BŔ00餡PXWgyk(in+خ펖652[_r7.m>KEme<9He̽s( %{^`=/ Q6aps5 tglʰ\Yqأ$Z@2N$l 0j={IFdo+*RZ1Zcy=pZcs~7Zd5l,lzK28]Ո`3jYΰ4>]:4 `̗>!NZR8OYikiV֨Ξ.ƒZ?ne/i~_<37?kwԠ&܎?pa?+f&Q뾮QDhbtwaPOn}8%ҡSS@TZ^N slKk%aʖ9`Y6ʼnIB1.k;Pj\ dJb;NWE6} 2lFk+y‚<̙.FKʅ6Ƥv&g)ٻ=VJ-JS2F5x+fWxkNP^ 0K؀~nQh|iП~h'(L֒Jdw%3&f-xVJ6`2Ip(82(J>T\11 ? >Lv`VJ5P`hrg䭧/[@#֚DXhdsrb~]W}X5K䴸#wW\Xu7_b/ul3ьM =sqqB67j;t,E7@Qj4\Iȕd+1qsGXX&ZcrNT͎rV?e* ߧQo3N`6pޗJWYbg8OfS ,&(hb@,=1|rbJ`9 F81O[rA UY6Bn0:Čh,X=h)_ٓk(6ք4caUJ]':`,dpш#6'6R}H`+EV8EqsgJ3{-;Yi~ÖR|":l.cj҅g6>5ux^GJOF&'9﮾SVbc94ϲx#vw>=4"Hi u%V\kX0 i#^qerݞP,m&k`e'D#gԔa$\Vx#Op 0 ]i"QHj$8Ϝftk iw֥,!,D@6uU*z2nhSl:Bʯ+;೯= hᩳCO2Z^wh/Bi-/8l ^'ZR9wt8k31=ɎF]&5Vh ߣiQ-`^~T,KzyNAkT2ΠUe379R"ZH^?"3g! "ګ+he崸d+yWiQU-=cDPw7W^CA1W2pvaU:LtsP}{ * Ϊ\, Gݽf t027Sfj|FE 3+R(O4Jڤ5Pӓbi V8P&U JYLSQv_1)12$_ӄy ƛzqԣFf 葧#2&K>3 %…2>"r|B"ϮOO$-fA,n945U?L͹MSЛ'dȢjB_Nñ+lcB֥%b]sa݃^ZBv/r/(l\# +FidFZ)߸Jkl.F8g$OYk]p6DDwd,; e^2UoIcW2Gέ:nV5>!+Z~)ي@9ZB nR1ТR$>T|񾥋v+XCZs֜yZhXo:qV^wx/R(8?͟<򔔱\h vۄmUg&u@'lgR33+ t,D7̿N,9OL|u}yJI;847 Rn޽PU>3w ^_z=_.Ο! B):;w5sR|)I&rΨJl<=ZW=2JyfcjfMӡwJ^o JıՉlN$qsO$/ X1')* WfR똼@)U8B[d{ti>>q^^ߌg-?yޚNGRbo1Du[(^N$ S8ٱ$[tϢG~X\.'Y\fm(Xk4^ZfT4YJ^8B_b.$P{OkϜg"uZĘȦ4Lfv+:?Ƃ ֛\lwJs\c9.a@k0[v qT=e^o4IEO5O,-uXB]T+.jH2zf66Y9jڽ˫bR…>} AxҪ5ֲգQBxn.yw]?iה\YS~sc|*3EY(hq,9=vv.*7o2YLDܖ>'OﺓkbMRaTDX@8B^jFAH\S"v{M@ܓ|$l}11MiaA53cЀR$%AȉkF2:XB0p i(e椞Xӯ(e1|I'P1g7\l@=u%BҘ s\cJL!$+h*Є&@wB[ (Opڬj٭PN& zd!"Y[4jhc;(&ʆX 7eCǛ(Di hWv<x%^mT5HD_ֻuzZը7_ E7N2>BMYVr?81S, uG`,Kznr-ΨlX*V{UhY &)Ҩ(b8Q("j*ǐ琤x|? thAvbw;4y=(+A Y)ujx-'5&>J-?;䛯;{.,se$  iFeaZ3ܹurokw8p:8>, Omv~ X uӖ3l]\+SY}1(uC 7?X>љI?X}]{)јi1'efﺇ?X,~K"nbS1(ۂ@kI=Uڢ(߷rl9@g65ŵ[ȴn1Z,+0惛FU=(r8Ps^*Cč1E X<5%)`]t1ڒ4E A!~Q(+-4kb$k+R"JݚL,o[1u1c{II\GRe\oR4&avX$B``ӥQo `^7j)j8O/F@g_uScUQ{Vd-!~WynJq-,g(p)3X> S\YH-rc^8d˃ƨZT?/adK`2I=Ro@~~=U c'N-f EjeOM9Hb CF5%JB-MA=ץVoip왮|Bq9>&酼h܆qeikEY)Moӽћʨ%E{.z#-`s22iEXkүc EWjaQm` dqz>ES]ܹu -7(:kEK${G׈Q\ ڵxё5)3%C6Y'ver?PP:%!|^Izt>_t@-Bk_BZSBs2^`vq;wrmhyWY*Ytr˿5$ ?Wph~NE(Q!T4g?&%7o ?@Y1>`9١=d0+ToJN+I<8.[wo| !ɫ2*P5X:R:^E2`T ϔcsR}.w?S6у %tk[߳ԡi/ q俋#d쭒@ 5EGWEb06ίӛӷ {W\m qwi &x;g#콕^@kB1Ƿ!v9``Y?tO>yg/s @ns25sO~iӄЗpyp{өܷFVް0,?౵M 妛Z+e]_+,jKkQ(Zm÷^5KX௟<{ˢ$::RSDmj X79@Ѩ/7+߽xLs8M0(˒8ۮWǂvVGfi;lzoya|1(53iv&&|/B21&~߃پHfJy *|i c~LJDH6V84E 80?O,Pc ]| S2,wd@ͱ<q@[T6,ȵ;wA֡Lv?]ݮHLa~|2a#, !<_V6LO{|We7_+4u1+icg (Ja=9`;'q9)ґ2:YZN'76ݭLn~ [NX{x5Q ..JvŤ׏ۢ~Ekz-w'tT;M_҃ype4lsg.c;o~L^/8Jx]B0*VGJ>|! /l^4c> }k+kcjXb 4-)nwc8s; \.3ϼA-&ISrh|,vB삔9f7j8{q)j&t`q9p Q5B2luGGP\fOj(*;cw=]9˿hP!HM&T V,9v7v亵V8@,cl7kc]9S 6{=ج/fs?8*iCN]Z,iWv([Vؤ&[JmU;Qq6 t6tM`O/(}i/w`W,f mVxMmcNPE#3r79^I4g4vd)"$[b͟=vwnM'/ln PtHP^^q(CZ$DV&+)4G=;fi-o;s~ Ti[/UϺesC.9XK6Õݷ.S.nΛP5[*׽5ara Fs{u6>Ox^?^K#aMҖ o1eJ|ټֹh ?]IƚYkD9BcEu xv`rq(k/-H4-ƚcq=L.bA*tm-`i(8b+ ߕ|-"z>(\bWZ 2_P{+Z}FKg˘p9PxώFʉ~-s8Ǖe}4gkP 3L. |H+xŒWSÏK5Ffi^;ZRr*OF>Ev#Mf3>RU?n~{ws%y)=210(V[[G&_0ĵS 5zHE/a[Ô<ʳ(㧤7& &U XL^]#4~ raI%"GmNhONmt1JկW鞆܏g^ '۔ }tI G1|RA8_H,&FS&k ݴRf8,e^tJ_)W괛|ʇܣac9/"YN#FQ#upE;vÈ~-=O>%}"fIAjL'bS(2PE9—\az9Vr S߇p:e>kD[rNHaJB˄ PHZ7DPavKtUOz(.;(FT1)EIYq@e$)P)23ŹQ 1k>c-s<_Z{;A9nىr N_Ҙ(r"#Y(0J5.|,W'/d.S(~N9Fl-P …Kf[ΑΧ&6\zdi+> ^Lkyq-0|ti$̦VXBZXniĠ-=KWzb(UɟQk 8? x߹eEiJ+Jto1v$a+Å-, =Ou?ݿIc*=\Cj-w/{yZZT(^0抔d!^TI-k,@ ?@=䏿dKLSy8Vi('N>Ge)j֯1X4,A^CkL`k Q &˼`,B^p옎 [Ͽl)v.N7:3v4MIa!X{%6q򉾍6@{ ,NZT AQlQ#֚:/ڽh77ebŶZ5I{ɋ˴77%i5(o7!L%?3!Ai\o)_Q&1 OŅR()H2Y_;HeVEYr^xpT\ȁnQ60OdlKrDK@U6a<ȀP3δvϝB~Ik?Ȋ#Jd+g} [ 5|G0:"w_8?Îf_\}!8) M6♭~IY󍜥?:IXͬ-l\ ]T x ,rH!ZZ<ȋv-2SKd&[Oek.ȭX]Ad(zmҹr%<1*=Q5M|E֍Ӵ'ID>o"5ໞ3ظ' 5^8N HZ,?cfltH O׹t<,_OX^[fy E; "˖($Q$ >DSŶZ޻_rncVLNDfΓPh=2=_On'N7TC4oV[mo~bQM-6Ƶk9>-QS*Pdu7q>W_/z[: SI}P J=uV#'8QE.E `zJ߹|)}uJ_*l9*%i`<{^2Qj/R 佻{IP}Lӯy%v߃޻ĸ6"BΗ雊LPE( @di;3r➕( ڶ!O Hv3{ e,Zʝ{~ x%Oz[g(p lk*`WſbdXd" W!UȄNNlt zjJ3hlb{8ڻYS빵Ep^z7Z߾kj&Q}{imp7Z[#:P<|'s?n^^x%$P0"7h^5A}oڎ::{b,sj*xrzhA6Қ˿ygVR>DqEbcH{i@grdK= g|11v뽆`Di" ~I/:բ?6%¾1z}'yɳ e ZQ^kbuiMSE>0JAQmh-*:樓4@?. 4E4{QX[N\`<0lDͫ9(p{;\~8~`!nP7SN>J( Pсcf ǣgpy:kpCmLApˎYƫLG(?w?>yaQ@8Ü\x H( C zAa>M/f6Ԥ [I5ĝ]_k~/~q_w>-b?b^Z'i10 _X2f*TFK+'yt4x X2 `&ѭ)Ё.#z#SGTנ@ ?D?ij<][GJo2r!gg9iO+FkJœ dpY˶\ڌ(Ozd…{gYib%#_gp?a:hԛX)=5zkorZZ)7+Ex\<p3ת>b@9=i6 L9")4xŋYFx4dw#d t ^/^OXÅ--åkZba ÛP+rwN6YZ[?"?$O,+lpww}`?Ȗ9[\ʬ5jIܾk^}WRT咽⽏:Aco"nPS/_! :lڜ ,C; ؄lW+M40&Ծ翈^Z8ZFֱ7@%vQT L'CKdtdza_?ƻ^ӿxV8P<\6(~1bF;$Q-eQe<>o0N DIB^Vз™݄+b`X \-R]5{(7êa%Vp$TJo& āV@/ZC6dI@j85ԁS:pMkT/*eh"SxMr'H6%u_E lj^W@@-H&җ[8Xq jB U=1Ǣ7PZʀCAU5%`lEC2`R޾a޺k.a$THwTlX(n\ {.*)ײ"9SkR:~]vk>,o8,/L&hH"IE⩄.'zX/lKMcqV9Pl?sCz+* g'949l--v jɏtWao퍓$[7]}mHHf9m Fr\V2xdX&Uow^"C`xnNSߐf];xG80ϮiSƬ}T6}bPֈm #HFNI93 c}*pǺo(VmKH2<lγKO*ͯ&DqsWضL|5G+fm0uꛨai`<&>w!@C闔Òޓ߱diY7x;Vxǹ%~ZRrqnF+L [L}._|}KGلAUC [`c}W=ɏ4I{4^8`W;g4;RJq/䳾x2Kv7JzcwY`Kӹ9 RN9 `RVyOQeVo@XfQDh=N?N(S2J&-ҌD)8M/Mգ_?Kl^ӕ@PzEaF#(,h/HJl\v"S3xw2VAlL!@1rJ?B3{;ځ׳c*u`-nt8.Š':fE>8e2=9CTV4uQPQ$6A@) ,H2΁YT2B2Q,{R2|JG,vǐ ͔])QT@k$z0@^55ҫBgfPLMf7Ӌlƛ@'^k秂-GLHndO=I'r:ߠAfluHd:vTQEh8򚱿|#j:βeV5q!6*x [X|,kZKlsz6a~Xc[^IE #i=MhdUQPe(GfB|ޅ%(Z~=ׄIb^>!bQߺ<^^[MA2鞁4ˡ%N,,d5SYdQ3 B)̀[,e,4^̕i$ K{alɅK,7z—|_,,Te*>[*ұļ]v遮JZNq  x*n-@- :LojlnvX9u 32aJVEX?( C$I UNK{zNU_{ ݉<٥Vo܅m%ǚ䃌$㵔Ԩ4#8G]&4st1r߃0BRdСRsD | vkd9|S7^`j{6)%`|c42cx|քZWɉ6:Ro(`kᚙ)9ζB2^'4D7"]Gǚmd2Hyp;1"HGd?]HnhQj Ϳ"`WqaPi UhKZ\2dXT5 3C~!A3B9(scktY E'ynZPGتPes(l/mc!E[o!~G3'@ p[[sO@–z+ q,.bJ~]v(T*@z-be2"vװ:~ȱteFSe`d:_2!y= ƎX x~J#&lN~ ˧iQMRaʼrq#bEwu^jt!ZH 6EQfw VJn8=%80p (M0ކ{q =t{Ok6q]) 0Z 4Ǡ g.'zbg&9іcxcϟ==z3$!Λw]\Kk~"hjlWc׼GNn^L=eQbLt]dwv!w #\x|tF ‰+' !:ҒuEΙ8|N"h~\O o}۪7 K Y0d?n SU,+*0?-A8{KZ{u+ú#՘@ ƺل $lN(P"*l3UIݸWUxp ,~!RTYMWj̨]zJ8lCtiҔH@L-)=@49c3L.d|q'+K7ioӹtAPIMkE^)(uڥKQY L4}3daA$`QQ5(|_|KʲL wI纅9u,[85`y6-)l. ;ٹWs.BkA(:bf"F%W5I4sb-\mv߹)5*.&ԯ ^nmj@c*2?8AYJ*Zu8[5b=Nw5-%;tB-7 ~=@ጮP(ZS2)ϣC`UOnDZT=\=YC!1+uR~NhZ~o\~➼c2w; LZ|^@VkPKk,.Sdϕ,I{{ phiKI,2Q2l*ġghj %8qR&ɗNR^:( ‰9, 7=sh-Q<ۻW"(^gyqM#{_Wk(ʪ<Tn Ggl']y=9d$ c̅MOω9Bp/YM D12;ޤfXKgNr_ƪҔnM u;\( ލe#^p`ftղGUFyjulSlU6~+r} kbl@9Fpe ),4Yi ? Hr ;~xkQ~=w#PX=mz"MrJ^!/OWu?P) k UռcZ2Ϳu07frcҚ6B+.$. =fמtK] &юbx.@Q˳} hkQ@D|f=b< Ɯ vKY_?8RֆtYd[D9Vo1f#tY{H\.$v2k l'ptHXEV;>eֿ09U7f6־45ø}bvZF J,ԥH}=(-v{)ƏF2%;2J?niP'n'{pA[M6[VVO`+QV3#ccq#: jN`) ?N&Ś8;3^qԓPAUC~!38[?x{?]=%6VXj.mK테5ꮯbASE^_CP~/u x'Ox*Gea&:%9Z8^~+9'X$o\b``84zZ*Š~2k&T4OWX殇? /WZ52=<]$HSdFb-ϓ36iI25lxEeu|`s#1 H늝B!]+0=À,oQ(@t,$1D! 7Ժ׳!N˙x(U @#>ѱ .שW2\OGJ^/Z`i޿?_xcOpj#ǦƩamvz ly<-,.Dc~z}5ڢhQZ+ßÌW tIma¢"55̰,;gTZv9(FّE PD aXTO hfT袪y.AtòyA8FGfP22\ɬ ˶Bi b{׋(Bp&vQUxQ?#rsK/V" /P4[ByUG7AChz0vd˃rYu1mKRvd6 dAP3|Pu'BCfEf_(^}*÷oB$}~:dyG] ,63@: oQ|C21r mz=.v#O1 <\2KxFkkOe*2tK HͰ׋v>2Ƒ;NU6h8D^@0Bg}-~S!4Q@M=mוLTt x^E:?~﷓hUs5^)x!p DXc Qo6T#gd8ʅގwQk xu`'wVNV&7* {UgKQ }[|CͳTRo0jGJ!@\&`*&!~N7E}%|?{Zřs]#8p#5C_0(vr̓`۫u2OE45:!"@u HqLck(,dpl=,2s0VӌK^2{y/|g09;#m]2)i JW ʗUBr]#OrLzuAYع/xs$ܣAljG(OOql 2v[oF^%[p;vZJy:٩Wv3!IZiy@Hʠ(@Age;yk,eo[Hp7P?wlY ûwy 59Ј0ÒrP*/w"虐Hh1%LCZOΤb/<]l LQŨ͌RO7kc,/uBPnsy^_@@ET=l \BFB{K[۞g?6'/xVD) )/7: @ EǵR_ƛqZ]Ջ^u&lD{-NF#;vmӪT,;KkuIr 8x zUeYdʳ4 UhqVay@JrQT`3:G6L΁S &ڎr@p6<֩@[tD(mʵs2vScM^l'5dA=@)T\ 3pZ-o9MB运58+x! c@(ua@vBK04uy2" bzkSn%J%8Ɇ( jV?Or ,_jyNAOVigC6=goI"hf|2[œ N_3&T4p|?UO,2N>Mmw֑qYe*, 6J,EsU }(b*4f^ݭi=z "S3V|Iؗhz'j||uZ3s(kH;=̥ I ]9?UGi|d8_vUTFTi$D3>3gO2ս(_[ڨ@FZVzelj@PS,p0jJ -ؑkX{闼}ᔐ p;8z 4Vl3Si}(Tϕd0{o1Qvx9mJVaoK΃{X0#հ7~l֧Jw0;]#IS։^즻$'jI1tV}{iM.[;ZHiYv٣ieYQ1J)[*bAo_5Ǟ zRYY5:RU:Bz}4/-3iiBt3FBD׈`kNֈh! ,cmeYT9|Z5O%?*wx9G '[JSy|XL)i}PiZ+fc:.La1풬4eU%+ڦ4y\.Eƫތ &)&D:GO5G@O&~x܀7 VPr7-pM1$@H•YQ@ʘ2̆nbEgyI$ϝD T-W{f^Yb0Vm6sAQ+a3EULO_uН2V5ʲjTlZh7nn]WpMw$6 9|_o!n }y2&h%}x.j=MFe佴"ړ^5|Ӛz_Q7[| K p%ÙYn~u 6"DWrP$j>QWVz" v`q5eX8:U)$;*cٽ77/ZZRQz ssybfssCD.L(19ÇbC4'ZJvCR XR|OWhAaMɝEP #  XpWyQ%Զ{T͑Ȧ/=ލ5#6ġE[(=$m~%\9S FFMk'i4cD>˧UiQ5_\bmC8AZ)!g%.;R6~Uҫ 4XIeqJ[ioL[3^ϭIv"e2("pI_h߸,+J}6a9~ Q@P tT]oqmz \rJ!i>fsCm[Ht-PCS+pRN|2/w#B "wF?=q*OnU#LU蜿D/ֈvטmqE4c>!H#f P 6\axG?ЃI5Ij4(m7g;(3F$z[|$aP]TGl% IzdW5B ,cf}P1o7LnW@YQy)J`צgy~'8sIc' Ӛz't=h8^d2(HhIT1jJ@ WLo|7t aA|>L`Sdw@Ld#X{;e ߼G본sJ;*cUťB|'S t];}W5u\)W_!Тd,zOgIx谸(v 4մ<=`y.n\KDZ|C$C1q̻=9f}n( FMN%RJ @#BI8ՍhZ^G%6P{J(Pn! P!6$Ƚ.Zeb >yCՖq`(`mDxgL!{(׬M:0ĴZX^!:V(9G-tXB+iby"gsM6w,c7K@Q<15tO$c!9^tiMuK}\>"Zd+'Th:TJt0Ep? s-hXD# -(G1iɜ)n70h.>0[ߋmۓZ4cT=FŗzءPU-␬<]26*_ieݜkJ !p A4&_!*)e{{)qRHȶw?'|*˧{\~n6$Y{bGʙqJ^[!`w 0 f0xdcR[{xKYk>hK­.Ջt>Q$*od#0ms0=w l| w#f ;XQJ'f $ Ph@<1'5nZ4[Cek)\%kp#~H~w̢ N䠔d0 बO{}:&|[D¾^Q\g˜ǿdZVm Fa (FX2{ '~F[12yE5ZSNHc<~bTѮmccPKԾޗZ_Gc4kR_j1ЌP<sKUU9n<Ǘo~tMfyJ[LYӚO=1X#j%\9Iv(#.sA=kx۾|?@^9S0mŘ njPRH\* _r܌Tmd8D=iLլs8&bf5 ڞ9n#s 66(B!2oMYcX6ga߅K±聜,=K?gm"6 zbnaX%:tbZ91޳{NVpVomoZƓ$Ln 0Y9X6 $m}exT)-h㉡Cg2j,~Y6Wa&"}?ٚ멓o $*i=tO,. k|/\|jIb84=o&>r2&*AT?!q6ki<͸H !j%FlFSWA m&*A3{)VAp,iK# ʢo~lT (TlY mK N~YVj>~W^yd#zBȹ6xnj7@ҎJwķLb; 6&>Zeq3:ʈcHά$=)AG6\6NL`m35Wu6xېqv&&_^O`²iy+WNqb8s{$Cmy6eo&]BS((u2~z5CmCTՋ3l&>' zc[`eʦd4ΒQDͰy*ك#*@fvUMc-Ajz 7},֚k UflX2k}^|>( v{L2@dU@ $L'V<_s&6N$۾^64:ex>̧?9|Ѓb1% ~}41(CSkd3l}+=350UmQdZu?Cv:Qy)-n ΍p_{r0g&>g{/;{;!QO$֢ og.W(:dp~ 91c{pm, `+Xv{wXh;L% i3HI4s-6ҀU`-łMeqMmi-%f6PdC@qCPFQ ၷǏL=!} 8w~O۟z=^f!1cp &6D *b#_f3R)ߔD"Ch ʪ@YC/}'kM4ki2:![h9i쭭 -+k,J'"\}TxWXg26&U6k/֕m?ݿo_MLv6xo"6 l'1yeE? O, ˡX6'tT4oO( aIv#N2`5a>E揜*i4bfBD x|d{gzcon^ %/;*o0 c*-tFU[|C  1 Y|]Wk\@bʄJNR0A@XV͈>M"[͗V|)W:~КKM"o,hW: fVB"Ae_9[C+W^ijp:P{/D];yۦܯƔ!zk]- k8Q:3| %L/qPѰMⱈxg4t /\cRf${9Y@7"s'V-qF$SC@;:{l 'l6e%67XurAw1ą_?0NDSN;)#=Flk7|/TtkAN'rK/JYF 7MqI \n(jaL]cRTEDGsD6ևf@.6 p*! v, pyEJA?=n%%R@Q;wvXoS|YaNEE%ϚeHY-H UnyKō@Nғ0F8"@bW[B^UBUH<7j+Ѯ|~r!^.(|*nβ%֠l.xAn3mD֚BZCX(F?#a=d)}(H&?`ʓ<,A/hlyY|+MJQS[7#NYKb62TM%RVp TeP46xJ` XIS  q"CQVTeYfW{> @5iŻzjm8,K@q'nGѬSWe{>HqHlsXbsp@hm!ZZ^Lp?xhP:d9v9La1'KXDS.lXr563؁bnĹdK-o3A6&Ы~b@CHB#x}2A[QB,g@M/_^NÃ})MLrzI_B<Q_x'^磣\pV=a`]k5^m R6H4HAʔXka.g~M뾕?vСM ,VHrot<ԬXkF%1<3FZo9Ze謊οٗe6=Anfq qA#WoJphld4Uyqbw*{{kgǸNk3Vߴi0 stO(>T!pt1g6?9Ts?drؚ L'qe5un}K|ޏ|$s}u3c'P(.BpI&YYY1IQ&SSFXAP-{ks#r>ymO0$FnINe1;6U]CZ9Xa6q8!Ti4YH@<]=}I)cN p"Kl:Yu:%RK1CIZj4%2ԕ H14ٝF5h $dD%ɲT6.ꊵUΝ9CAF>gؙ6}?sttfl:Yy29ݭKăgYvb41CbTГ1BM$ DMH늺7@O]]=fqtYrN5edX7IdQiZ 9*6+gl LkąoiXnaceiF,4h !D@CTz=i#jV+@>?$,\N:gsLbfeC>4Zwhpw #ڀ 666Y{a^|E 3=Cf-iĚݺ @l1By `U98?9,DP ,CW꺕LcBT<h@&a-AFA@U H&gi^,W}W׋$Ͱ}1[k$\%Cu`VkC1Ik7'IйPeCKԩk-94ֵ_dW~!kIηvМVa%oE_M[;wA*KLg6T>c8!-V0ѯ]QVx ;Ĭ#hHn1SQBڏYT>4L6Cbv1ASz|]՗ϊ 7yW~.r]?w,1,n2n'F9qX&m;e;#< لrrȤ(aJbac6%i ԨH!L&jZ)OԥXPW;x%(?LmTZ^9o̖ |4,1"#vd v^{jse@T[[m6d0@gr496Lk[kM5c1J5KDr. 4'Yoyzy˘(xThC4Wr_uxp9vOPd{)IAh~C]1-Ʀ/%4КS ̀ϴlGNE_2 P.OB9WG}?o#?s-Y&n4eRG9{ZC6R7z#NtVɄU$P @w6+FG#-^o}\#[g_2Ya)9\nzZ%8Ohb ,ȭ NE)d MW2D^/j˫W~?K{TuR=sG=Zh h/ f|8Уw`2;wmݝ~s:9 {{s4_;/k3bҔ82 lMD0uz}Td 5H ͫ8\RgfK=)ZOyBOUuȬM^ZWԃo#o$Y$p-Z3"̑|5(@)٘߫ŒUd/#D M5jLJy}XkfҾU&+^'5r,ʚ$ӑ\\DU|#t6L5w{9`p[3 &4w@|3sB@'6p> dxDB5kJ̬ڀ.q'..^>{?>d ty!TW -M?4uyN'k̗"H;ɟ !e%TdUzt7戬ɴo閉d'2[ 6ď M1Q ,f"z3wt *?)! a,ok T@h;^_ 1Hd‹zR|>B[?o{_㴓&J ԏyuSx?t>mHN9>@%iQSB h͈4tOnaNT H0]5e%IcȂhZ&DN{!*v͓[=TR6\[S+ݵLxկf~' RRZf1,ڱQ)e}jWN%Pt @8dHWΡ"&!HyK315 &ġцV@AE-ڹV91I Etl69zϳ>rW_Ҫhֺ*JE!%ӴHԝ5UOh\\kHF u?c) 5-X,*M1#@p.|6I+ G#?g<XW7C& bȷ!VAIS`Hj%҄Ӽ@?4:yi?&\,5s`c]< ]bTsLT 0e[ه1Dp DRA< II&cS7Qw#qr b,(77yI ‚,(pY p\l:rIt$TYroYQ /&]lIڀ:eGHۚi1},YD`o-?7,Ev5yH_R Y ŠHsT\;WUsq&V-pQiTHA+jQdM_hƔ"lIR/TwAFȻGεKE6K_З퐘8WUZ-[_YKc>R4"Q9Y ˦fA͢O$m\2Rf}-c!Uٝ潈V@QVI/Z Hh)('OJnbY;#WdU=o}'[O!e뚵Lo4Vd/4wXk X+ UkF=ܚ*"[Rjz!;o\ b45Ha*KiD3wC'#V!0AmjvcV/YY.Z]^ mRNslbQ9>~ 9|%ymXgd![c~ѹSޱFBz38&gsyk ngz ã ϬLn1uA },$妗\r|}7z?g=,O}ng\m>|4|7W^wճ5 eawm3C.h5NoT a IT ?+YHfQ G[y2YPaH0^sɊJQ{^χnt?q&3)KV2@6(5PC2DXjy⊳2 ,L'JU 5jxU R7}SnMA}fmL\<}ߨ>PJ46}k {Ѡx]Ljf+f;!_-WR3c=L?d~^A~&O JbDk1|4xêftʦ ['~7});‰ 9ylÂJcy:pul<-I&p멖 l:y0V,w*{sz8>OD}l<o?|}sv&iA! ib3r\o.̭9]Sv%j#2BBs=/'ӝĎFG je2VN|wѬm4I_-c?>f/?f,.DYh`@ӯ1QT4֠ >ΰZcvx ;4w\U.?>$&3x$eO^Wk }>axx06$frnރ4eV$.27a`17pU[pZNpƘ!bqjUߟ$^Fc{OeO?.YUZy߁IG5?VMpTt{ #'|jT `R+`c0y PXOBr-mZ "H Ga9J9mć8ϰ\d.)|7:dNp.@-cEehd(IcbdFe6'W5.hssC,f@)D"ӋxTM .B-*x xR^,_ Yoosw'z$=<&!1rdql_;+O&I\5-@Ym1yI a=$Ϣ¬*XR#iEYOl> 3d9Y/a4Q?(4범~~UVWIYQS.y`{<[NgXd̍y;F{GؐnJjb3H1Z&r2'6Fp>m]2[5sxȨ/YТ4yb o} Z۞0G-q\\|kbs=zcdoY%hKTMjLhDU*"Zk( 4gfRwW&SqN9&;Tbd6 Sg-|щX|S%9x2WA0pJ'd5w(ў{i" uSMrcq(ח0DK TEEVE˵!EGZǏS/dc6ȝIcL*>L˕X}pp Wc_xGpN+rC67G۫ɲg i"NG$ڵxѫ1&zh!6 'Jږi DsbG ʜ҆f3+WCc$FG$n^}?-%j;fhKWJ0#AoyxegEoz?Qm94e !ěNH!DEJNR6dYҼxs5zLη!T!"2_6\-C1 rUB^{ D&nZ ߳ƜOVlJf^[!$hiFIXDts0i ԈV| Y@KO2(4JiB&j% 11h6bmhh ΍l5]ɭdjW#w[[;mlƩ-lKgawF>,*U#bZ\!Ak\m>RaIr- 3!,HGCb `ܓu(4 آ&]VKѬGvNrU7{|z>;zk.T2$~5ַ{7}Ibi]kBn^~ةϧj/KUWHйs^.W1i}/;HH3\/XvF"B,sa8 /BHH|xt'Gp2[1VM+HK>hM7X$I}$ ͵VsMtUW]bEZFZsR2eW:ATɉP0G4: t;M3` FiqDpGm$M UէHOT dvF-Ѳd5FhYgcfB҉rTP FbPlH"H0M, hքNO]4BP ,Ŕ'PU%['УbD?`I5/W瘢VTwa:!Xl}j࣠*LGL;EfT1P:!0C=L$0hs0+2)i/%Y%K]:gsFx8zGUU_5q~|ĩvJ\]?y繳ƆJh.KWՅ$!|!ݟ5'g9]8 ?^oZY %òvڬiʞ2w>bI9B:F(k~Q험wl~6&|>e6g6%w4de2˂ L47r8nD"=Hh/6>IHuk3@L[0L$Lqrx"OiKTW_!9C4L zax{*iFmŨ-*hOD s];"6PB>OPX<Ǥ !Tz/ sɚ\U/>k 9<̽'ey~BUi# Qʵ)9 q% M^nkh E u0!^ VB#f{DH'އʃPk$Ìd`9~i˥dӓ٤=hgVo0ן{Ezppp` FB{t2cvw`I7@7c~cǒcϖas{}F:?k|sgcPD:_bQ$&Bd~Rp"as7" 焒aZV598q˜ޭRBW)UJ#lD_{s^^QkySq2E;Gh-YcqX)uVO,kjUo Fk<}?䀍M3Wq',:P+>GH٬׏s/kYPY4FZA7Ÿ 4:.D;k4[:4 Ki5U&S2*ÃA-m0$W-HᓘuY3`ɄQ i>ҡy\HHJrO 5)cfaa !HYփQOKnX#=)p׮lꢜ8y.]^@ Mkj\]Y$ɲJuh^[`x,>˳ ?htXM2C`V׏ێOr0Z7frAeyB[GX9sk ;6&,>U~T.!y#FX  (W9O:- Х(2KhY{3JbV`vQda#<W^}|h|P˄0x]bcBK[xLZID*>+K+u540$[b/'O )U(u9*Dy}\bb?[*杖_QJ{&I-MT(-u %B#.F'Mu5-+$ޣ.k7˗߸d]p_ 1ZV|b~0y<νiIAX֬03 GpN$iBy9te 6է9pd-nX_ci.ʺGpByS&!bec;6&sWrpڤ^OXڄ~#%q&8ks H/#.spp$̘&t{)hͯ_j #yIPeNYQ7Bb/Z>*IA?Zz$Q(}V]5ZAK-Sd>!i3|K)Oze%MtT#Si|o{~T<҄UñU ^+,kɲ<;8sY|̑>g_%eS#%Dh!] *!@;pMM+@HJhU)MVTph$5~P}=.L bOc60`6岘Ow9V Pj& 8/c/2fPQ?^O,!?ⓟ Jwh}u^&6\=|怒%BlOܘD8J x.7FxJk4p=$ٲmM8m¸}g {1N8zY]yYk皆2sdCiD ƚLɳThrfce`5Fg/RV=A&WMB`btY8SWs;ɐBZAYLSQjX;y15f?y;#\qxbk}rl2]K6yGnN7ߓ㙯vI.<||?7ng;>K4A@#*v rTA4iQ,'Of?SŞ]^zy@UQ^Vų]66T",QZE퐑I%  ] ͻN Yn)\j`uW]]WWUfBwyO^B_D'S0GzJ!m98kq~YNDd 4G\L( $с/jiiX?TJ((a>?]rmn2eyz։ֶUp `fOi\78d hlH f&yj7@r ii<$HF!/$D`u4Jq7ЪUBt:}L8[)z96 $Ф,GZUy(V1‘~W)"Yd?h1H?:J@hmy9ҪL DzYEBY O<~xL]UŸ>}F6O7rn6YQKDOPDAjJPg4 @~}inʓSx$ >\5Ɣwܩor97?\z兮m%#L]'qА״A[焏[׭=X@_fpaZUyQξ}$ƶs(n$qT-{/ P;} mӌWWfM5Po`~k*-8m53YWӛ!48=ΖBedG2Ԛ/+T4j`0^x2<9!~t}mhu%%ڏkfMCj;hg,Es<˅Gg qH7lL4,[ՄIAX\tb*9Yl6r$&eY~.SUUUW0;NwzdH#}1 5FT!D*T $F4Okܢ,`LjәD?x)O'i%oÏm.գi6W5٘je^djZ \o4j<бio@C,sԽ\&5ē;4  }N0/eJӽ+~JvAl>ƴ/ 58ki-xñ0yv:T_)$P& Rj3j F:#(s7@Y@GJšd/k VŒp> wɤi] ]dm2L;';r=HZy|KFd<ٲ93q {jb9g2=c]^;0Ƽ|?~p v{ CU}{Cwh7$GptĞr/G=,V*/hQB3C}XqWCӶ*0R6 VVk+>rjLCpɇëɏ~݄`o7`GS9bp2:zj n}@yb?&w2C9iMYL:yZ$YN0BgDC5_ep6'^jBeҖo{7Ic2>=b )u]RNI7 ި Dg-^YڻiL"6} D34o[uTPժQse5Hfh/K&2>;wxgg^?36Ob,fمaa;WĐ jdf/1fӀ鷵 [bOnZ-T+3N|U/~]Fx< 2lmrh%w{ϋRqGx8^NI\xpXPi#M^zZrA]H{^䏒xp.Ezҧ,lm%L;OP9% Ӻd7KTJc :7F)Ry>{1w017SGeE^tjߌGGUf(k-kUaGhz>9d?d)Q64sN{54* (u&Hi5u<ΰ^DfLW-EDtactD1c:2 s$t{D3h=Ky-++ts$Rܪ`jc̬]'W軆OBe8,tߜ'Y(cHr[g.V$Xh;Ggk+|+bR WKL[1[QQ,Ҽfq~]f)~apỀ? /|`?p#X.vUWƦa~owl>+ ?I*wT:Œ :q\y^|9.e^~y_x.Y+f iR+f+Y` AZRkZ scKჃ=gAS^U( )o= kϐf)G.: 'O焵a(v%9b{|3_z˯/ SܕdؓqL*V&Q:d L#ʙ=Ĺ$cҡL%x^boBaAs(SAcK)%,%Fπ},p I.F.(4qdjѠČ)^t9sR=8g=`uadIuyg;ߍ:˃ðtv XO;Uw ͎iK'+O"o.jv~Wђ9΁OʊeU㽌 z^3{iۓo)M|^Q,Ѹ`dxop=OJ3kOc'X>ΙGΉ$ JSgԮ+:br9vOd/\yW8ܽF1]J_ ַ6_*+@ \]$MMUU fd-?J;|(!}bf֖pUgQ3z8yFokMR:c1oy|K\zeX-Q=}0Un!IJ# עЉm0o$wHHbbɔX`x4jճ(9[Wx4!R:X>C:yC'~gXN'660=L+^)u$ڿבvifK[TWy/p} WȲKsF)z'xrj.>< z # .4zDRչ1$963ܕ0` `kުh N"f~aZXsO.&I^pa"%,E`{0gyU% jZbjEWdNWK6>d,eHɲ\d̋,C Vׯ^;~Gb1/?s뫲(`4^q鍋 /uNgnܚbؤ(W UQTz\߻(')Em2[LANs,u@O|asLg7=̘Vo\>ŷ?]._deqC٪$=zîn4U`\!B*J ņ| L#ZX͐'YkJuU7f,'A2W\PD<?tsB)˺}cUv8xs [:R|G]t'?jb1w7^&x`my=ȗ=v6Im&W!<%{_IֿkmmP{o=꓇d챇@WK~ߏ|&Il('1ߛM?yJdz!|io0E'Ν}~e;zm˅֨늲 ` cz=qPLl'}OK!u]Z.VY>EYeYfKB>pSŵ(ڹeYL;*c[k{$N ؕ|AwرkY',E3q+JaR1PW5|"e%Yɩg4Z.弒*Py`}|J9$چ$˭Yo۶m۶m۶y;s32cz0O#ZgXo||xbЩ#ZVku/)-H3Y\zBVC(GfjBp aZ;~)6lIآnoM!XHaA`즠+p6Y^tʼnެ':ˊH,y`_?~ ӏ}KGڦ5~[&gg;rk|<)|۳vRn?-=KnoFgCx'N'fq~'4ᾟߏxCxv?fhvG 7-?;Oos|ٽ'թ 6 v,4?FG56o`x֚Qﷰw;._r)u*VU|kM4i4j$&b2ac|k'kuk3?n/˴:kNhmFodZod&pqecc k̺fu8xh!/ [ktjL9n:(xdfғVs{gg^b2v&31=u_(J,s nF>zn.iP7 ˟Du#e{f53I؇*L՜ǩkuQNb}vb`vrc˜: VWr1*8UƒM>{9Z>7شqOcpoozM#6r.؍+#RcwcU\)뼅n< l=5ֳ *I|ϟJҤ:T #f@EBy׊kT #u?3u!=Ûwo(䞍ʼxMO4w-,Tkٍ*QM/޻akm `V=<ϭThJ2ƈ%A23=k CeI ɔzQreQJ~7轀%뽏YԔA5ZGov$X7D*Z@sZ dn (]`l\d ;k!*s2bōH}qalJ DgPBbm 6՛ >]B1oFW=qE/kh'ъ̂ :8>(TŃ\:"Hh\l^ |% Rrc&ףXku=LOmFMK 8'vKum,ݽF J^W.qUW12Po|F:޲9`zͭg3-)z`z=:}5_?82;/>?EC.:M4끽G;ڊ}u 9 ؠB5l~f'a5&1+QnǼćVA0X xSSMg<. kDV>Ҩw޸?iDU`8pJE*jP~JsE:MNj`tx.1NQ +Jo.#Y[ᧃɲƃQAk^'x) <-H>|bgfj6j[(q%Vm!XrM?srmS_9ul[-'k?oclcށ@ >rKysH$wc>Pm"dP]^O#:Iyw# wbOZ*S}iYSsIaF6йOwG1@ep< "7FURbS􉒤B%cZl%Nu(ڵGAb #J<8SC.$Iw叫z.VaMƚ\`N>'ݝ3$P4`,~fERK]lN}tc\fz]+u0/ĤSkkhX7Ҩ1$ɻ_=ޟ>#Pvlms{ sM&tiz?;;;+pcnKϭZ^g_PTvYhK?p + (,vuWȵ ykv4QJ%P"4j6^d-kj!XF)Qϴv1R͛ՍbTn4culJWfELDZQ}ϋz퍚Ƙ,Ql:$&31M:,k/,. >Ż罺QtIg4~ P=,\Qۇ 0&LفX&v60td}xuV\O''忱 %4}O> r\@K ;6h< ! ҍ9lduS}Sug)-* :%J)Or= ySaW Dpz&qOkf߽٘ۋY^ZP:hȩs'':Y =  d@ f @D3I"AA$?_kwKZB7[Twa|{߲B&Gnm v.ONӯ?(j j{TwK@{xCZM &5g1,4U[19E\ x;RlJ\*AE@-##F*i``8PJgqC9P8=|{fW?盥9=q:M 8v<U{>H0;mgW~N!؉hۨd n %87Q=<+Ϳ9kQg}L?̓Q?VáŌ70k 񶈏3EgH-vKâߢMVMYYmQMI_}OK ?%7Ƹsa"sLζQ Z deLF2r08w=Hh4<<[p(I(Sl0gOOSح9BbVNR#OT%bSR_c1s xGX7i{2*WEInZ&>V`3:׮rّ\HS v/_;7(|4Z+Ξ{H%oߚ!.N3}GU0X(wpjoDqQVJX?jת5QEAv"]X h@)/@9ZdD&g[I\2lq Ν^ %jр\s'9tMHhV(ycn'p1: zbSa6Á]xpk;R71b8^XED!<=զŞ#!zjD9 “dр W\ I ֢ZA}fvmezie}<Ȓ(Ŧd>$yP)85J)kͦ]Mc-vd7uq,/{;]FƮ(Q8RHYs2e}:ӡKx  XkLgXeiMӜو_8_s4%o]bSR|ee~8_ %T|q}q=s63@򈣰ޜ=4 D10Z1G."1[G|6H"MtVk8heq;<^8'))y2V=EއExT*S#R`I D㺮ɷMgܷ!,l i:Ūh"(* jEjX% C<%yzϯVĤFݿzW^w:N$9j))))ŧ  kS40 {/=h SRRMIɝZS40@xUѣn7 tss,8ZsIUjV]7Won\' nWѹ/yPRSMIIkC 1% ٫uz ^D"2D)S))))))))MOQIENDB`PrismLauncher-11.0.3/launcher/resources/backgrounds/teawie-bday.png0000644000175100017510000047266315224505336025023 0ustar runnerrunnerPNG  IHDR/uzIDATx,,ttv<Ƴm۶m۶m>b|5{lEbhddTTTTTTTTTo̡Ƹ7dkwjZW+7ڔDeeᩬt0L~'䏒H<=gITVVu5**e]a@vG,[CR%EpΌmvz :_ʡc5>9TՠAn@Z:EBY#!" OM H iȬTFA,lj~wvns$鲼 `"%D^МyDzd׹1C@:̟¦KP Z(p]7GPEoz0RJ<.wZB' FMC,_7uo}TUhm%ޯ`"XEw2ZC3TIpDJ@Y.wyy-G^x<1 >eLݺLg'_  =GX/b90fv_Hݝ[_i&W1LieN6,'"+"ЈB4mJKi`cy",}҉ vEaá>"/%!h"$p0ւ 1"Goԛ(:{?p~an'Im̻\ANf !:0voG&EfƄ̹'cdC)U$ZaCey^r:xơG`zux1_v "JaG] z>suYl ?(Zh:Tt",BIz7Ჟ~|eaYhm$ΝnɚK&WQ4XVk7Z,<`GW3ڴZ!.ϕ@L=E9)4T0G12f&PF"CZD{E ".3#s"Gi;bΌ!CI]TME;yÕR+E$K ~R$c{V Ǟz hxXat!$( f Ё;a57m#.ԛ0w0ݾwP_"N>V%#3Te'0ǧEʤ.\ +)LPP2#caEXa3Za{?ۇ">#*&J)mH_e٭3JkȹJkXcA|:);B噓q#Y"f#0Gd֡,dB߱sm$Y^,cu阙/cfZlYdIץs<劏1aOZ_?(3}L2:\JWg'糧JzzLlr\Xif8 F4`D*Y>.E:ƏAi, \ H*vul(F%I_n&8R.KtvW8H}+]$0nX +A;awNTQ|￙ԞzҖR-n:pF1d9kX4&` `Uݧ8|O8O!}'5k-JS d4KrCywM.U14K^^ƯcY X@ F6tA쫜ͷ^j|6!qD>cMGcROntQH@y@E5ťjʔ+I=JL_SaTFxW˞(hnQ#xa$!_js(*MBH>Ea 23nǐk?o~?",nȄ o"3c 4#Js` p-S&r׳lZy,$N>Б|s7شi450'w'FA327UJe-NCcܴiHC ;;΍?PѣZduPιLZw-x7m,5) s}l{9QZlm+wLzbv_|E(<;nNr-Ƙw0(}eWk_ijJY3ny0 &?]jB?o)\Z<[A ʸTOzLKױj_>`n*d:a1Fȏ}2EPo wpR9 Z&1fJkPpb/8ꄅΣ sD1O]*ӹkF}}),E6mih˃Xk _Ru@׃4(:k[c N{^#(W\#f3%21ݷ'ض.~g*UJ60Ieׂ0d*Bz0d, bӗ?Q\*mr[xlɢXyu^t1\}݁OsNQƕYW<;[׍p)T%OЏ=2,Zg[+qAfXqRZ=y/}7]{ãdgB`_d~V4{뢛m;zc,ViV]ɒgH"@@ՂvJZ(vdW_{QZ3'y%#x]+>:Ŧ< <]E)AM.Oq_ZXSS*xy[,ˡ1=%Oz5 =bAZj= zp{p;qzގ>P{JB۶AC:灒JkI5B!wa.{~ȏ ׺BQbx#J© 4]T,lcE ~WJ3o {E"Llkke444D2B%X2t=#ot5З立h=ٵn@#A)wЌ ;|8t|Sܹvz T*}>,~jE[ TO-\wv:n vKuZk[؁V kQg\ xTA"X-w!wo"grޛTG(p0=w#CkeO7>BH9ih Y:ݑ<¡`W:ƁT 9UwL¡۲'ۿD$eVMuTMrczb-"U0f(@cٻ#6@&Ӆ}R495454Rh$HpnX!ױM=( h߫wn~.GRH xzvp=1#9H r+clG(gq9X<whZk [N40Mh55D1?`3x k Z{R> 0#jCy3qe$2D@n]a> 9Sws*N{2Z:gB:xAdC'~11/'=w-1T+L;S?`{+W嗒uuM7dBᐧ 3X/3/$5u.󇞥 8ࢂgG?<[? 6R(EL4=Q:!J%֞* ƼѾGv%8fav~5${rxTG<1\v%$7P٠~ai&Ov"B!/`hF?}  cCV̓*BcOe`_ngPCJ)=6V$B-SFJiRv@f|73 Ύ{ltut&D\,kcK )qB$7THw.cADk[[[L&iK!,n 2^@QqG ,i˷y Sj94EM&6l۾?ox8%{\#ܹZAm1=Qs:xZ: h? ;o%̖v>˴֞ }=L @K}c."JJDDޅKoڱ2QCaK9MX9%1l* l&(~JeRQ-6094t>{g+Id#bt&8 #L!2g5}(%wTX9CJB\gˏ/NH-4]ǺR,oxߴӨ,HL )$Gq$.%Z --K8zV!WD3 D`N[DZx-m2Ħ8ԏ46%IawO%x|;I z ?R"11H"!m૆` W1s@:P.W{(8N@Y쀧Z{3R2 {BKr_A6S)Yxܹ bSJ%Q}n%rnc߶1M7F,qC`;BL3 x B e3,8,:לL2DJΎvT/%[/@(mDFIW="ϟ &.vWܛ),mVlAlW;?'_bO:H1a||ٟo#=̦ iOIb"yjr1)s@Fb;ɭNY Xy3A#PJ_lFnb@Ji&PXƳi:{`UyGM=:>8FP$&*b[ZJ dH!hd,RZW߁q_/wphH@:kr_y7۾d0A@E,\ /U+W"Rw0ac)u4TRIM-YEK`j" lޟ&iS xTj@>l.2c}v!舤? $*U{ʶ WN֠i3 Yqt똹 j$ҙ9ZjRi`)= *20ci`] I`$%]{uE:JcJv!L]t!DRJ` Avap^w7i&ZQA %cTo^zfq m6F@BX,- Gaxm&9Jm3gnk^Ia"lJJb{l}lI2::c9PqYx=Tu!Rz̈́}Z” E J8Z]L=M4"()2R)\xZ\K68v^gu8.V7TZ A8`3g.A&"f R *ϼ ?bwbmvFR 狔,DKHZ@kyr?cN^>m{\;yBJ?JYEGAIK"eR+{a#"eS8}z8/yD&&!jP,t]/,] ("gPB^3{X梖[;: Kgōo`ƞ!W_ණ=m#} T1~4{B)GUWO|pD1a_0Ž8lJ{˗dd&ҙ|ݘBYM4y04)eZ)EC>!DL>iprk>0=7!l>Guףdf';cA Ïw篘AlUVVڤi͏xtLKΧ}nZ'R(( 3R=JkitD4 K0CLf-ӊ姮e/K>K{>R)4'Q5;5}/Kp XySexx(YM^erAvdGFA*)#cd7,gWXu~mbZ]W147YV"JڟseG^~OV,/P9x͡sY%.۞9=K*/]&W}SkGd9vt66>t4)x`7̂ D ǝz:wJk;b4MS6N?@jt?g(~#:"Sd։+)-XrT5:V@;YJ&. fg3ȉ mm>p?~6O`OXY]h&2pESTOR)z"ںعd:q WT^꓄c` C%Zت5P|6Sgvi֜zc{CAz9w9xJ8"/kx%Nt/(zTk8 / !R;>S[ɸ|jϛhLq}P,yd#lٺ!Fɸ:1)Ohh&5B+ (%BY͡yfekrr 2q9vCX-21?&ELsWcI3fH?CLH,Xo0+u+m,捘0z}4Ffsc%{^@ B"ۻ)"I!i]9Mb*c`k9kEi>dZl}6Zf0w4"l"Bq^3,bxAjURY)eDVHL>;<< L ճ={ ŹKR4Xk ٺ&$j- }? >5JXW\Mkk듕Oa7hpL)lܴbQ L&ir򉢳5J){<\T)gjC^?C7m]"wkO};ٲi#ט8t<+Sh$?k"Ƃ(nCvKUIeFItQ<)QEv*a]k!Wes@(os~ ǦҰi[(Ew m݀58f桧}q p!qtvx=nlKgca,%zپAzz&%Լ0qTk#c^d3ZEO#J3DJߌ}mzM{wEZ~fP l~Q X8Y=M4AV/36!wAH_sK.w%>#\?έV8\Y:}>g9 .Zz'd$ fn>@5XňM䲙LE*c=D4q a &fU"yh*I_bavαdϋqS i^&v^ZI3!?Sn'7k>L3#I0R~*ð˿qvŧk;:ggYI,$ Y,Ŵ_ݛjͩCaB;lE2:8Y;._w-8&WB e7wA_8~5/p}ՔRZk/H~ Tf(NR~q%H:-yG=D%8I0ߤSc_Ȧ͛T& r9&i_Ah=ͽXcG, i"&yÌ ' )YzMx2B@ V6l-9S^]лYc%9H|Z&P @ 3Mszehbn!.Z%R@k|c#ˡʘ8~*NcPJ$  ByYŴ/ݻq ͩ 3 <ס&:*cv\tE6]"?9&R_[(◿UZAd&'0xthwGe2K/Bp%+t) w;9jgV+9pү#쓛#޺m[760;Afz>t%~.9U3Gbo( Gt</W(yڋ`G?kөiP&&rWZ}ɊUuW}&2%imcf:`e^ihNW6~םe`?(E=he5 g>؆{FvR#A2V(mH? )jd~B@ЅjX Bj B($V^6~=p#<~+^OԺ&zZ%@?nfVO)9-!z4clSrM׎%T_%gbbя ^Ȝ/D}br Bc>2cfJ~{1֪٣Knf]"o&&x^4d N=$i)&GtٓL_5> ܭGV|֝=mJѡ_O4Q(Ԟ=)+iQg@|_2cҧswK?Y~Ͽ_~-ի ck{;! @HyFPGH)JaHދ$&ҞF+{` C:Z9]i;MBXP6@:rIAAE`_fzɱwJ#Fi]o5#13T+CC}}7 Aҧ'[82m={&GFٺi3#HXpEd2 S'I6xF0p5sΩgy$6?}O*Y$|Ҩtp֑oݶ@8-o!DL.WVJim4Mh1p<ra'|:Gرc;0"5$X7rB.$$"! RIɳ`H R$SJUB4JTS *(CO%9)[V8"'@G#xA:%-%6bᤞ3MZrNOdB8)~$D:>/|.xðll._LR#οbB`B&BAױ1 lW8%ߎݯB1O!_/|j]u2a7n:m"ݝ٩HIǞã#>9/{m:u?&#ixZ-Z{,[׾$i3C)M9'1ݗ+ٚ?Cn31?`tDN"[$;{!=GCA ߘ0jV' J-z aߟ: ,qӇbm[XR1/\Jm=tt1<2<\WoLnjUJfrY\N oh;-6v-fqH @N%n7zDbFGGg'K`H"e,Rϝ%U*)ҹk6l7_d[ sw~~{o~3LohK=3٬gˆN=}![7IS LJMIZ%B` B#- Vҹa}$^ "tJ|fent(N=YaR¾̵>0 Y[IChhf9Ă.h[ Ok%؎XLмBݯ.AY`{Jhd'n[?3pv>Sq2:WD(懐Q:fd(<>061BiL.7PJ}Ss3Zd򝳝us,551dXo}_~\6?G֧s~߻C SB0uJM;s;vK)1 wTFA|{_ۼw>GFC [[ohKJR[jRF7}k18b' `[6oxͫ(&NyIjLYwlbeꕉdEJ:ÛdbxZAB{DqV/wORi[*WB@4z{=XL%@'a[*)~fᖾqiI",\JCmߓP_yѽ?E'c۾A-(a= ½>=5QQGo푟tY%+i'6Ͼm/|DB9nFL2+NXDs>[8"p3ԑJ~}?=ouyrzltdPLKfhqX}uFX=wy`3yҢcs%)W|_AbfW+c*$Gg axapgqynyz %enl{ 3YT*)JWDxxB/3dEe<-3ZoqMc.; $c] 'dҒ @O$dvۍC$'B`%N@[QOLՏmb~S{@{zc皳UkQm_c5RwSq Eӂ{Y} TwacsIO\__8wLbǒVʾb|Q0PTkU(+<9?+l[7XM##?RoY}a_XJivY9x92nf)~?ýcUKF,tF!?`'* s/sg5{-\ÉXj)`!Ԕ"$D@bȋtO<4߳<ABPٟj21Z~Ǝ?#a$JgvmThuk?ӓ&$I,-I4` ܊k^$c/aۧ~A_|)yم-3$r f>JtҀ)&s~o{{ N2tKolk7 аo{;yI%zc KMXA22={6] Lj!adQΖ WŰr "8VȻ0>3S:EFX@@%Ci9En$ ?2ˌpDףI{930nҡu'o_j;G(mq ȭ-5ԲzKCx[ߎ]yB."2\.UXdF;o]JZ(^3ZM-vJef߉ע֊ىG d"^/: Ь}#'PȶƜM)DkPRȩ꣯n$@قar߀bsǰCޗb6QrP!+noM=¼Iʬm]]?Wkϭ*`Ega*M6{-X>ypq~8]owFU^>~2 a/ ~b,rL({Sۗ[fdSa>[mFݍF]kͧw㝨ҘI/ؼ#|ŠN+W~ 33@5GcdL0g!'Lȟv 5ɹ(o@׵ CLN+ |mg$3-9-_ I7eLf޽xӉЪIP{,MߎezEq|^Z\.I|_އ QC#Sy<~/~YjYMkb„S gⶐEE4Ca8jŤ3P`hՀ7Ym[<־p;NzlvVO=OkayYhdxś JJ29"z R YLh6& 1ܣyahl={7Tc S5CPAq*! $Fg~V._ k,GMzkuxV>A-ސ|yjyqf~  ̺bgؗ/ZdVSk*Jo6.:֤4)y"=̇PU~'ۀ֨r1.{X3lQL9ҘKZAc|{/'< 8PDevw?2= d>4@6,Ll#fGVAC2CO%@ƱX. rOJH`$guFm|%6O}?6]zGN9k m20t瓤cC8{ StZKK7~>҅gQFwޥmtaŢ{?ɠvW;d~zZ񆯵ZuַA<229B+cfxTϖԖ:"G ?t]7Q/{Gpk$”̱"{x"?Hzf(;,8[ Wј4e%Manyxb۪S`uHIyj?# 1YI@V 9WPXz~<߉dz?kv;n*zz3^?E0@QqYt4әP~?0?wN[9?ɟА?l|3%.z"~+& rW  ={y#GOe5V>{'_f!iĵj%"-pYgcf%H֧e&[ox~@gu14EAGg= :F4Ɋ=30YQґ%C'%Pd>bqa<21:‹g#8H1 i̧_{ZbB*F4f&IqO>ѼeiJ|g !O,7]KO汓Z~l^XcB-M߫hvLfTN$'NYJEWV ҵRNd ,(DMa=1t cu(3O2,\MLf0ܚǿJQ2 6o| WUuXFi^K/#,1'sjWwcϕe2MqN$3iwGQi $%Gn|Fn"sQnȝ{~ 4x~j(0޺(RqJ{cJ6y_B)  `xt4[M;24V7iLMo&I*Js6nh!ɐ:mhO2K:,\) *P{THO03PT 53PcN&1Psg<+{z堢)<&ɢp7ވRGOeL*,'qg $#،MH+.!٠yxv])F5Vw<^#0vIX\[caPN`V]㺥SWl8q 'M=ItaJWaU|Srׁ Y"~2l&/p5RUlwխ k֦8fM߬VApjW*JkԆlrY"TF B+ߟtQr4 ]Agi0di2S`f͊5` $/ތXUY@g˾ۖmp0U9kp,!Ŝ9Xw2a 92SԘ$/1GKqk0i4IEKP_!#<,B[!@|܆9pi(D ^|{B/%D&+a?IBg_S~j%.-Z 0& @[kZ} Mo]1"+{ٛ9 3^\A}1 ia+UTu(:SB2b1bΤb- zD*lEZ46=i ̼FNuqWlQ(H4HH&8¸W( }A)o]wckF^&k1;}^Jk6sϕw$RY$ugQ^?מ4'/c|[A] RrQ(.JaONTUlf,5v~al6@ӷN 㸮u`rrJOY "p(<R|@X1'נ6:  Xd//FȦHm/-tڰqτ3~p Mj ߇v7bi@G`,iܧW_GIyס=8@@ hj^Bz4J̠Ow= ǰu zaQm `ig:9~Z\!Z7jPg#>G`SYvWaBrnyɒWk_#4n0NNĊ?CuZ-l.(kMfUY^tİ+867|Qʣ@a^F53rquנlZUՁ!tan)m {Н;GHd끢*yX@o+ P TP`uʬBcس4̯ڂ@Pm͸[J^ȝq7?%Cͼ=NpxAstɡJ62Lu JfJ+o~?d (?ȃ4 N4Q㙧ܖ^d3 }+F|(ǿᛰlbRCK&|Sx=ggup౻/= 8tmQ 8@*3: @\ }4ttّصo R"/]+%gݯ ˒ގVHA8W4 T{#Af5V #QBƨ{1rXŘ#) 9̅%ΖLkxly'V{v<Gua''W7֜LroU^;>Df2ʘP< -eJΊgaexvsOuߌ5 ;bw14U 5llǐ=9_k%53;@  J/F%3mI?5)+*E1Z'?JX nz ::KMߖ)M%u~y~[^R|ITy=YR?J+g;-#[MbWaxi\h%鍅2 d:I[=^9RoBsw騟^I]ˡ70b dy*uPۍ9@q|t: n(Dš=0T Y5y0>MgC9 ;:(G/c/3]!%؊yN}c(mf7Z׮B\Kz`P2+Og l k̊{y:8畛9BE$忁1&L>o[tؗPk֭sOI EO’3fItb,? oeY]w!5ƭ+rM:Fqҷ֟7|OA1`M6d+餔bsp'"g TBBs` Oj -?ih.@ H߆l|\ uly@m36iP 0<&XyƏ`6n [1z%\WR2'iVbJ&nfRԪUaڐ_r饨ժ2!M|G}"&|2ر܍מc& %)qm|>[k[.?j5VqdM^*j 0`˭]xd-,@ m)~7Aip7K,@kDp[>oVs2veTIҹ^3߂liݤZA^J&ۭ Q}U4c6te-Yan.,#RG*1WdڤH[V:[EC*[j.@n7/T,ېlM#IXhq)Ou 'oSx{\B2C{Wo) (_"c).;f~EЀ'bs0Q<.(hfz<,T h HJ5j[n,?fbKˏV֤Gn=B)W~/kb*A(`e.|8*9*y,q&L,].DƠ3I%߇y!>{0J!Ows݆(kbltܱ'yVN ,Jh~p FVDؙ+ǔfqx.]XURiAk lVk:ۗ+5]A#Iz0VXQ_*[֖5:aײnY޶b0VawbGC'4a˙R@%!'[PvIzUݔŇ2+*QУ*һqP[,%ح/`,ND}G:o#L`g%prk2 ` ,އnnϺuDƩіw˴VvR ^R&iTzwFaڦ;[\\p?htĘ^4}{eS*u:Ԫ&+ Z'~!EOK7MyZ]v!x +{j : ` "9a׭1e2=5'T$f2掕d| qa z5"pcP"֙l'h0mvTa,&5VJoF+N^3N-Z hQu? =='ddPe0DZ3w Ԭ$(r=/I^+'xd ?Lnt?|3g1y^a|zzaH 0TNתRfTEK6rŵ0@Z?K83JIƙ|@@(f-,8kt=vm}1+J'}7 QrOi`GZf "['L#'oBN-LK4b8 Ö8<@δH+1l{ Fu uԀh9( ;:jIܥwİ)X{n v_Wz'&7 }ǻA cLrZ(`)Xf/ÇY7~7mjVW+M֛j֘(b\rXr%`˛9 2hss⊯oî)Xk`j8~_:lrVH 'Q(yN<_Էb#eO|s1a s6S j]q @z=;TG m7Q_1$%tkp?֑ApP `w\a.8v=^׺s%2U Wq]xeg|M u[X%=y.x˭!cXcť<2MߔRjoT+k(V(%^UVEq?W_|Í ϻ * ˆ t!_Ba&T1*= фdc 1s1a%zAdd] Ce+RKoJ9 ȲPs5p{f PtItyi XT  ]8F{VM8mqm@Z)5s_1+qcEL<$RK|% `st#gSUw?nLOA$xYl~3}Y^JNZ""j5!ΘGD !ڵ7v+>?ĖupaalQ^bNA vUĢFDAq`2";Ҝ9. ^?[ubrEho EpԂ==f4 80`+]:2JR@$+$P(d|24;/V@QxhڃEV6֜pҔ5X({=~c",Ҳȕ2%AO+(D  &.t-)Vu,lO+RZeֺ2P-%V_>%=y]xGIb'~_XhKXo-N "]T]&R(S8YZEԉynCLQbM$!" 4AX|kr؏}jD!Vjp6[fͱNH(Vgb>"A> DqT.iK$d~+5(UvkKD)D hElxU|Y(jt:ht#Yߣ[Ԩ O:aݺ*՚z)>+ yIM0DGIgO<@&cX1y;wV)eahj%pKflfdt%K(r"vUB0JZ!?K) 2-nȀ;7kgܶ^F bCSjQ =LoD],^g3!3ż0$ȂKbx2K;՜RsvZVҒezЖKd[,ԕe'uk<].*]/92H/āVEԜbЅsո4};=R*kU(ݗ T/7_l\/g>??=hqʪ8]߄>+e-8q%YM(a3jUL<','mz%"(/AŊQFr Rf@'#-%u[ 8QwB{Mbc+eH7/kTP 32U Y~I{kbN; **Tfp)&d:._}?{Z(k6ǠnrPNԱBM'=vnE>iW+}[Y)$榧0x:fڒ bQQ/ťE|x\ZBpB'dN{`L%qra(NOܧjxp$ɑeH~v4_;Y4,'x- H2^SLՁ>qBbkfJ7 u3LR@ 5+Eoa#Hpɧbxhߢ'ͥkUUTEEy+uhJ#v RK@7RiVQT*k %"5q;عsڭ uSsU4>L>9Ya,VȒ V~1Sg<.]J +EuwCk-T(8T dXds5ғ$< T19ph~ޱ 2;sh 27(z+RɁ @'ÂQ-& zTxtw J6l }9SOs-2>qbO~ArN4/WTw<3O{$I0A&i2mMJM%Ydu]kA%19>jq2OO|“)rX{Z )2WH[-NG%@]0!P%3r'qCqlE5gD{WJFBpI02Sh:b 327uzn;:.s7 (@>8CSs͐ss%]XԚr~|30_$KTEXLx-/"U~'_yR>:[St~D z5i `[c`4}+d @ rώ5kPU}5.&Myu!,J7bp(pОE~zɈV^&lLL9%0@yOef3"XO]۩{3fbȱ(r1XXi*毜zt~]lyaTJ#/ y EY:ԜcPA-ZhȦ]g (k۰cn$`.Ɩx4#IrY_<&!PJcpxNcܬm NLTk/Y,X:(ذO$ȸjbcXikbppk.a0;x4:1 EfSjW@2㠐wBw Kc^ s-#/:)ֱԲUs1caD@AQ:9,%QE~IZh坄 Yļ(c{,%&bp>dIa"I<}[DVNJs7kQxteZ"&j3<ۅ I1byͳ_Gc!i4W[7ڭp( ]8M߲N eBk4V]ž}̭u_Tc uWXz o55Hg/ m-!ȓٕ' N (NVG `، J0s'TW 2-zDL\ngا `AMEîA [J- (ZpS`ep-V#'tAۑGvREt-Da ipVL|0^0LM’yfKLteҧ#(=>R;mSld&MߎT5e.dt", 7\F&%H &3ǒyވ!&8tו8=E@='ƀO8t1ש*ux~{B.iؿ$3GmVC陴]!+g+A@U(`gX6kIPZTU` 8y6p벰^bhp'HԀa(dq8}@8ZG|e TѨl" ChhLObllܵ8n& {31]vM]p+4 Dm 4z(N$r |.C~@Akt REG%@A&e^ iA(id!E5sI9*Y !(na^-ƽGAN*qS]f1ͦk5 qPt@Sh_- wbJQ'"FDzSpyS ..,xaiq~^o$}۱؆k%dYHiB+]_6?DIv;+'ڽCVʶp.b{yc^[n_v!\zeq=OzyW[_|s0f {B cSr+dF-7i71V6˚buabsDҐ#&Q,5S(oc>7 8ٜzgdI!Ԑ`1:1Q xރj|-j<=i<ve۽'`{Y?B7J4ռrgW\uMMn[{4};;V*Z)˥zL+a ;vUW]E,-,ZvS,0:>magO_)$K&bݬP_ Q+$q(p1`(!bPBg@6ǚ"cKœ'izIs EM7]lm !Rd,R k`*A+dMDž|?J)w Lt EctVʕHm<7a^'JLoc.QP3s64( b)*O3\YX\ijO> ;ڕ@7mZcʁvQ\%ڶƋ[_‚c$9mCp}oy;19 Jidf;?xSSj`H=YE$zd쪂±-F8'r/&QHLy4؊"c> JVGRPTdVrJh껹 _UV{ Z ׼Q\JXvi ~L%E)y ~%+Y(~U @"@^j50v:n bY0TV,q8sqF̫v}tKO9/˖QSI6*E:6=#'(B u.S󖙢*?:^p5i"^Wȗ౵ Q1TaJ mwDPBRA@^Qb'I`ڠ8acoXDkky,ͣu`;Tx{Pahp,aMP&XT8ʍ? *Yg⼶x_N{ϝлnj.=_k{4}{E:+ڴRBs/J~XR xp5WcBԇF-Z9~Jk7hc?|Cشi3\O4q9}t$6) _QȒB2.> Kb{QmGl9o1)"Dێ)V@[y'wA45 T4,R(elBvʆp6 $آ37 A\Dg 'V,w54<h,7F*tj'KEYON^3 <_Zؾk̲ IZ;R4ŕJ7MV Wb?|wH~/:I-=0:$L g叺*Z\Bia{SXQ 8ps/: 9h`{Mێ4H?&5l(-ṣ䂜ܮP ADR{]2H{Xɐ`>( Xk1l&BqeQdQM# d&Tg??a0,.5ነ{=l9tPO1HҀTBy&˜:_On_S{xwۭhw, xB@7ږ&ICvqiY_"#Tnz\Օh A),O Ʋ3/Ut 7\ٹ9tvٍ>ϸ떛pAth&ˠ*JW5s4l6b͹,S ]_HeIc# ib` ,BK~'Jj)jƺikE eFi|_tHWE~Jb2 2otovNFFGȐ'-B( M ״]_O,Cse,3ykxI(sY$;3ck>ZƕJ$Jz=O@)-;vy 90[ZZQ!61q 5V~!֝ ]N{wԶ)Z<݃9֋% RGqw".Xq\4/V[NL(^fI(`gLz BH+Jh̔T L]FP(\G[-Jp9/y4Q9kmFS߰$߃-/ɧ{e]|a'Ih)P`)@` d- ngFgO?RkB4=Gci/, 4U\s sȌEu?>7IUP=x+N*uN=>:<]y>&rQ;uXY><ÂSf=@!=cM 9(gΕx6+H0D5WEATɢKql0lVP&N&t\T.4VU*TmY3<uNi B) 4_ < >Keefm9>!k-=}ETo>Ǘwʅ4vl_29ۨoDC㜄1dAcT ^?aaa!Odk#h Lb?~lz7:<y6L֘!K22tT cxSbk0NT!"N5ƭY!lfdқ  0bV `ı-tb`؉=s.mBEc &Bkz%p%5_y4@I;o8+mApe'>‹?EXl5Ǹ[߂-_P)yw7sdLZQ'}k ~g>'}ԅς0D843X8A?ɘ XdAf|πsH$E<3;axXܣ X'h"u]kgYFm43dK9!.)2CM 6QC:5,8!j g%]ff?r֠V*(3)Y%y<*^7F׳Zn۱?i|zoqzSCC r }/|Kj-5WahhXR,g{_{5]=Cpη4'Ql 3O>mh.-mSÆuؓO`a N=VG'1d d,|:'wNxmXBt&-yTQl}0G"sl%ZkVz`4k!GL1EXCY$*FH n"z#k/ V7Ԟ{[a (O i47$ZNkG_xׁyo ,5ٙͅ4>u} зRSZōol C@׽Mcl6jc:`~+N>ZI #'l4Z;YS=k-g⹭/l =K {Z&rIp&iaPH)J<ӫa'"lݾnkXw#؛ 1 y𺼹"x^I`8uQXnGAU'W;yߊ,{0bsuWoy{C0 3@+A10(ς4S^DyJYm/{Cuؽ_rR@;{ J9 \l-?j.SY_RgO=4} (-"3pVę. y)Tܻ͊h}ߋ6ؤ)ԬO3hPꊊ|B, V6?4[A&z* teۑsVEK2YYzV54E-!_0İ1ںpֺ}.ݰ>trip?EB['Zp= JUIAo͍f pCv5o>MWbcڜ_TV v w{W;f.ti-vR28 &zŢɈOhZ0zkN7]-Z a^g4}7YX֘\p T-ㅧq9N^"3UoĹ{P]G^'*Ю/1f=} U1 P-`A)~I0]uL%tY8-wzKc9GejҢ{fɾV;_"`)2H @] QT^%cVlj\@*d(b<zG`2Cshu{Nbܳ2^*s0,>5TZ9_%˛wl, pyTfWǿu4}i|Hk]k^*U~QI 7߈G~) 8߆zr!Nud"X41^CM,AD@5(l% gY k2 8YO ,X Ԧ e @䍵E4!3Fo8c]6+A ndu T ~t xx?dD5QVi9/>*qغ{;F>vVAAtyf,9VeeU4>U{!V.icd"rK4^f2\}غtZ{6{4}ZxξlsػoOǑi\~W^{ssh s&`Vֳ-<7Y*CcoY;{w2ѣTE.Fkc:>@T`@*Y]z _wxw\=܂O܇[hFaѶ]^dlH'kNz-dx, V)zCGŤy!\3ŰY0Ҁb'L9De-Oiœ}n"1XCj@ 8-jHGhp!-_@*zIZ4ju:2]^H$,>z$%|5A~ DO%[m\sW]K,K.-CoF,MZ^ k^vکx܋/о}lcprE]oɷy%5C'.#,??>q7vy5Žې,- =t6` BD!F1tI\FcʼnPJcӶf 9 -ƹ9JF>(,Xֺ|2oLf,. U@KFQ6 90r]ѣ♬gb}9@b{9@IdQr4{lZ۶m۶m۶5AۥDӱ'Oktx߭{ȶ9L=F{!2#[X8fTQiIzߤpbldHvi9GpS+]24͜}規>~zA8|P6g)Bx"n <eC_G@kʗ|aHp?R" $ qJ}|ӌg~gr%lݴ_V8y{!$U1:X>`0:{g;ҹ%Wݎy=א)5LgCݾ/enʩ7.{)w}js|WiH!/MZNs@_ՊU@5@` dQy =|Xp bd&cuǧs@@3n"Z{0gs!f-^Mq*\x="s3'>I\M>Iܽҹ.i.)@" FͮIH+:5G?TTmHwGL,~V'sB~KM_pY'16d=wKo! b}ɖ?XS.5|s%w%nFAqelO\D2!3\wlgQ; o}I'Ç)OVC}STNU#t틅hb]ɪL !r[, "5A6&@70v6 8N$\Bc֍tbO >яQzkrt13u6~kQ 6{E#*_z%i'>@Muk>$xiSoNfM% #y+vLbaPYE%Kyel\xKr8.gXvrj%o7l`ѥˉ2dh2/`jev;PyKAz8GCbt՜K |簙䷸w+s#ǍhspsjG3NK90C1Q # 4T,Xet:r##~G~jt-vp+"EҗD’d^ `Yxݘ|=A~ul/e-8@h\HV)ސi8dYs4<_Vħ>Ȑժ{?B+3o%B/ )7 |!yk^5W^>'q)ysWrGQ?)nSqK)oFJel)k`<g7XAZ$A? Zg~Ή/#cT Al w{PZqrUJ40] $G}IX$p~NVQ=_?@18$gTAjOy %}O3qfZ|0$#xd L:m-WRZl|_l<%l:Ees>W01dawj[B['#G5M+i+r%n4I&}&M͎5Lt@77E/x18zz'`Șhf"L0\AHRQױٹw7_DZe(/ .IS 铹NmYu=ޯ~pt֑MR_|Kz]“(" 3 :$ᡅdma;+XIiAgǨ Lul7Vfh/KnXɢоY;\1՝Ȃ-׆ND:& ԃ*Y2~FH}Cl5</Kw\$>C<W -Zr)~} 9cllG[?m[Ym!NX˺$gm梖Ցkdߺ^8 䃜>yR J2>y;gf& Bκ;}9WCsl`[F0tROaeӿ6lwpjA+{y-> 0,p` (eF<ばAKl«Ruw^G 2>B\ `0 ^IU!pvUܶ}vU#p\;ʼno|=#x@V^1L&C# ݗcٝV~kY~U̻j XĜs)oaVO;]uvq'sO׶9dfb5ZK29 0H:IAKV<?}!Id;å !'mM?/3}>WV)Ɍ)d3l޴HI.칿nMܔHܴЦp4t!|щV{?lhffH븧PjYsAr [M?1#*Mcw}0ˡ/o|`]d;lo=,h1W@""l2G)$11Zh 6a%ʂobQ(䢥_1yt O`hĩC-s|O5q'!@S4:lāHp1ӜΧ8!keMP-[)еmۯ`)-lA0JYж&W@d%^'Jۦ/+2ﺅmj#UuuZ)"4ҼeȬ8UW5XL8.]ǩ92H/K6Wej.fٲe+ DsgOhINzLTiSh8|# JM53Ggf&B!0_e])U*ϝCJ=߽e߅\[dD"HpH\/BkC'#3S B foϼ^@Y@JX $\@1v[E`<`oYe)~)ԉqtqXg+i~eY|hxNw&/s?v2ߥx̚No%,`0c D[Itd-ӱq6ZdJT|LgDiCTEkM{!b ){\) NAq%|phXIhlJJ :&j*٨Fb0ah֨o~PH.%'A+6G0fog>m^OTZ} 1ãǴwt-'G*B}xA t l׼K,(Au|xgAώf&g,$4{XDͤŬivvyvohv4ZfbH x~}naaUiu=ZgpIĮ`PKЈ5hlnT嶲|J yzdO"EQ$3u}}qf1=5y+P '0(_Q $`^:-ÀS5"""& `yZ`$SlR-XVԨ`ت5rT m>dJ]~x ٹgTzؽmv߳AnғEC{<}WJS" H* { %@++x]3sE:!^{[f#>rLf#6g*<.JhkkNB4eKC6dffxp4^6XZ lJ;&y1zomD?X_^>͎Y\ws峄;c䍨8\D8Bn'Q^ĻxgdQ}!Zquaj412Dmh&Lb4C:6T  boxvݶJ *& =m҄J.뽐Q@{ TCcب_\z/p?٧m7Xf20 PQ{R& xA2@OLiqdJ|`v /^gں!&VN?C}j>LF'[k$AfgcvA77 TUEjM0}. /C9t <λWmY /ەsl2kfFnCXm?<$< Y ]}2y'Bmd@P!ϸxW=ݳH]cjG8=]]b@[lY^`r:utS&T,c=f歔61*aiz ^Z@֤Tk}6cM6ln۴BlpʅIpgA NfcUw0 Rޛ^kʩBhRlsED ɪ{^ǀ~W%[i` a- ֻkH9Yh9}?LyY|w,o2vؔgSY6Gf{8I;\;XD`8@Q7{_wG5[,s,̾SCU yUa05+fDDX >䤤dRs?cuL<6c@,䨫7Xo0T`hKч*<@%"țz$MZ=H1VAQdVlvJؿl袍[Mˈ.^/ [ac}}cSUx4;r">΢GUL=H3w:j=s/?gpyT@&7α?U̾$ϐS)#xh_Mv yIEgIp(D0Pq8[pD'u)_Il |"A ${<IY5(o3L\u0W>x/.aJyf@"ekM<ړj5(- U/<{hR1 ŀ/v7!}F0iߥp\m pc0X7|fMҿdyV[̍4 T`kO8~Ol'tc}i~@ݎXy1+@1$Spg~_{~A3V+UG :RhFm`T8k|#ֵNGy+W|q4;,M{a\>k"#26ƛ{Gg9֟$[@oeUXH$j93)Z5 r <Ӝ~z.y(J81R$\L)|N:Ѕh,}V{o?pC0=3LnkD.us-G9+[?T).s}ƵsxA:ddx4i"HYngΟ:~ɷq&ì'<#+ o?< $Ow!X#e,/ӕ8(+F&F&4{c Zm$&-7 (̀ <|u[iwȵw_BS`K"A]2Tc*HUSqUv4l`'L8N}f7U23g|;Ma<_szx*Z<d lRe6RH,uPFAōOWW?_xS鐱 <)e@2j(us+qN4 }D)_]jo&yu& 6g_ahaͳc]"tțBw-Bbb),'#I7R>{<+sy:Q w=Fkn/o2,܆xjG`)!g3t$r ٸzqw2ǧy|=OKP%"T$1t?}s/rk7X]'Ӕlۤ?ÂA޷uYXū,-,/,p5^J5= *R2Ph9עIgCq#'Q;HڳĽИ c&gds.~7BTIf˧ N'$CtyV`ZNYZnyb]3n[ڲlF>3?ÓO>AE1!ǝ<s"(Ev;xׁ-DM9sܻz֊4HZ f^u-.):vGR1W/_Cz?W[Hjb`7%ζ@QpjD@Bf!5D bBdO~ͱ5}GQO%D1ϾŮ%VA4PPV){2Ͼ6ZbDsnQfmzlVlM|c6n ~o2pӣ=;~l"vf~_Q@N8N~]~cd}ܟfa[__C'B5§/]8qr1&LXH> EwÊ󖛁\ h#YClRh1ݷ+jN0's,HG;8dO_Ep$QLԭGWn;,)a`Y^z{#L6bͦI7M0/%8wskoעxt<+ o Lhc9s(9+x1A0e=^Iz0Ki5ۄB_|7G4;0Z!Dcz{Эc>.HWΡDsmvcpqR RKZΉ;oc|rn HMp5x@Tф\K}p{x6 {S2K6cfϱ*]o}! ܲ^:̩g}GbVN?I4x0v^Ę[/>Ƚ? VߢK`ϟ/r56kT+mfI%e[>1@A8q-E@M:Hg3s4# l,lXSVMDXNM+khA,,=@Qِk8|+.Qle:*vVZT5GOL[ۇt}_ɮwc3m^WZ 0UAG1qS!\xͅӧEC!ZB[g_;9~s2o;'Ѡe#o"jMlV׭:>GOpdr, RX`Sa{n9y~g>)Bq aaMPx0(CgEȎ,Y^}B Yc\&iL W eO'[ZKx+oJ}^ؽ'ny^ $suu_&O'Ԕ"/ 9b9`<)MY{qlYͥC4Z{g**gS2JF WPfl72}sysL>?Mԑy.2@;oR#/TMUyČ{gdenX[[ghTFvDƦ>~I>¹aTx$Lq r_aK~c<ٻ>/^BXNA ,@ MA,Q.FxMZz5="끏}6 (_p!{ 6|a3g W#c{kčl-g"E,yew'}H Qq-ͯ _ԒwogbyuqcN~=EG]2KOҍ%ݾT˞6ExuĀ\qm ǨaPZT!AԊ "vf8dz|ٱYjBO?XD{oڐh=Kd` 0٬1v>>eשZHO/hF&Y)hNV>O=+-/5/(5bP~Oz+W.b̀@l0Z^N xAr1)\DbAy By? a^c$[xCFnN*-:s"::þ^}: rG#ł~u4P%Q ,00Z);PZhI6mʞٽLLcO"$oa6 Ao؃`Z d%,Ө5׉zaăzhvD̫zo8v*:G6} \UN_x{+T)РU6DarOwɗ2 A6K#5&A^#s5g [kjF("ctgbn?ySS3#E/#rY}՗-3jF8}|~?_zdwm-{VhzرïkcBg;q%+y=߮ŵW^x/ WHW)J)8^s{41!>I¡=\kߊaL \\!j*PMr6,6T(MBQۓlUXh,6{ixB1ɋ<uVƀ+ŲhmSCRp*pʼ3ϟ'zRÉ069ξoڸi+7V8{%:&Clqxo2;Ifi;Qu۽O$ƕ=z$v3I(6n8ԎP+\E:=:1ƗF_±m!cOp-GO5@:fp6$ZUEJm3_9TkbE9Ƕ!9]vc`T*SpYe۶ ?bu2?d߸|㟜믭7Z.}g>uӟ ;Уٱ~讯jlzzX\9MF| FtrC:|#A!N#ӍU:p[&%MYET챐B dH4PgHy *Jj:vQ nhI?G/@0b&d%脮%8l\@c?|`ۚP|`ceZD~t}cd&|, z 1/rT a2M:&r10+`3ϝ4-{1 ?<-w!dB( ZdN2JnήUU3,/ L3)kKǏ`jdXr%;V[ jƙ$@/zQASY0rb[NҘj9*&򛥐f\# ;iرۄnj߹}2Io|e~m9>^9n,8s?p_ΰcr4;vu'|"/Ѽm?i\Bك>(QRG`RL:ܞԉB4 3 Na4@OyB*5Fm J<68:E?+\/cO ({|3Չ\0bBس]v-Vcpe֖VذN33>ddD,‹&n!H[vc?)0!ƒ aΓ&9Y0Z= )vSޣ{Bڦo8y@FE2W1ȩK4j0rIC:$Au?`yJLT]MW-J~K8A`ΙlAn lVgʵR8s dNsO=_|^dv]ahQ?qAjcm ZWB%FEkjY,@DxP Z5HhlPMG71tmס\$3/s+-i"YP0xg}DXI {=tIsj}bC-pű̅ΙbJ(]ɷ~~cODlCdKT5%g+DgL@z5Gj@P>c޾;P(.Bn2\ DL3^qs hy;W742Sd+&Aݪ,6JϧZ_9=7'єM-bTFmda4}IfmZ @O>sJ.{gCTUjI=-uWCK#V)8K^|5 ;~*K7!";|lL^ .DžY"u 8y #:( @F=럹KV#zOwu֖B)eCJ/]ӄx\UT UP 2^ofB GSOr吧Cj\!K^|hN!Eq973Pxqb2/T⊑u6&hAV @Duj#mMuP xE>&Thj`Y$YŠ#]a'Y]Z =K +p Lt`d^J?cP(޿,-u<$=;2F s:YP/Z{1\_9G48'Ӱa480R_ SN0pj׼)s<Ћîe"ܳqˡLPP љDMWꔔyn04knSFy;vݿ72CQ.瘺u$s0,W53)bj+s^QY{uf5E~[ &?6$\m۬XӹO}Ӽ )Yy# aeyy~> +>yOfhvLCh"G2B] *AgⰩk!::D4dy+6,k8JTR*eM2,+PU .32y'c2ѝ 'VYy_.\_ ' ['im_*-&{n>G/sO1J^St)c<Ƽ'STh;Ϙ|D+^ 5pq~kWwn ̭Q) [WdE-Zsy|Ő`Hޢs,e/o\&ӜC 6KTu5lUƀR88:LنƬPd%O|c\q^ MŠ? z3pkϳcg@cu૜soȳnqfYDrj֬Avs+ R&̀tKU̪fZR8AHq 6CE{ 5pǘpJ-5ah@4yd*-GQ1ϫ(l|*+K"{h6/sx߉Ռ8qTqü rYKFzjJ }Y(HoXrb_Xz JuO|/y[hQli1O06L&EQ}}t4kL9Kk8KQjjQB| L;##URʶ9`7*F7מWDLbSf-ے?c3<ӼY[?WVGQqRLsvhv,n%[[?_qI-Q,i4xD-?c\K"(` Twz_=j FeFQdtL173k_iDh$ 61X=Q ,9yfcZ$uCV:+.Dy7J%ƕe)׮\ccuzRg週]Sw+q:^o|[{bblL3RҤJ-()(XP'`mE+D}Dt~ZU675b+Db+E>7gj͋Te-g4^Yw{!g2j܋L~u?"5>="yU/SVo,RxV{]AAϯv:M`4ΕH( C.]2gϞׯ]e}m8E3nI:4cO@c $IgY,5blj ( 7@.Cy8iw&- i0j2&7Ɓ6ɢ谨XinL+|ve/𼁗f&xyr AQUMK+J@Ĥl 6ʮByDKl$ \  ^0OP(`x XW~}|@Py5"Uk7|C))G=,ogzh<|Oc b|\`cg6d@S7Zy#\kol4&#-)CwT"6:~7gN=eᐵ5la}}v:`p)⧣(f:Pc@Cck}Os"b/48Gn|3!MBi>F A6K)rYUq^SLݻ0X}֛Hbj"P)<h$ P =BgºgA6D]֖V,ot#ؑ !\~ҢVomύӚ K.t~H]bb!-jbS2|Tp`3Kcj v (.pew胏275K\(EJfzi9gYX3 @ !BhaO2  -W&4z_vmamz,P0 > |- Кp 6@Aɳzy-^e~as/իWX\^*z)×t/-.^'9x`vfǒ(C'p޷Q#Sk8nOd2*OAn@Urժ`lD@Pa)*[f{$D jIe;'9p ΟAZkR b"L*zKVhT5V^׺_:('XɆ"gL"Ͱg h֛4 (VW`}Z̑}Hl;)(0<6 H1{8;2 yrQd9W04;@8 Jp a2XDj?|+d9Y5{or(uc*ҬtYf)ZD&c2qQd/bt# { D"TM@ufc4q @\ט_]d)B)XY]a~q%.?oյ5.??!>^2  i6"2ݨ7?y]4;Vj78jk {=@u`-@Q)kGF54-P)Es6FӀP-mRJM-+7Vɏ榏J:qMJJQzPjWVkzE"mx}ɩ]/`!3Q:M:,џfCϊP064k,AXٰ jz-O bb\v}aJ 1R,?be Ad"#\iYH-n&h eae$Fd8' \y"}2SvK[pA⒯gk$',}5,^s<܎/CT2@@ y[o*gKWĂ+mn 8;ϵNbSO~K|dK }\nqdob|W]RUY*}ٱFT;,"Gcι( zc_O[XR8E 3H革 @ 6y QSIHlR2lYnjP.;ϙ]{vRՌ)?/6ˀD [i@ (8@f XҜfMEB6l#߷e>s d^7EVkJ܌OY_$[O YQMî]I _u^}oO, ck94Aj% ,@R&_c'+1>0p/ut"E7`FUEJ&HI> _.6 "]u[w~"n(V+  f4ͪmy4`^ `^s#@wc=ltB0v$cq4:of,gG8Jv-< Fj級H jqe+F+u+'V; CE^)[.@4Ydü/Op8l;5]YI)+D iNaT@yxv!ez~DE%ۤ 1۾Qf&<X{gT^clq}akW.vI{`b|Fn<c}d\=?T(!:4;֊j B\o1u+yOc3{MHHlLE1Һ=TOus& ,N\NC9U`ykZwxޞ旮/[H#4 AThi*Z|+53ج% `8!`yKyn^GbDZ%8$}":~̀Vٽ* jؤ]AL*?RZ/5δqP#=@c!8EѡS3`J&B@P 9lG'PѴ2^phd`m ;yP-3 FnohE"d48p9*i*њz_\a&W!j=*=N21Š=͋I BИM~="#iO_[m~E"4g%,GI^|-뒻~+ ٱٱ i%"x|wqחLbK|Cӱ&fy(PHZP!T-NZVRB3 hvW՝-_8,c?N.^4 eqkڛu8dgؓx(+E4 ҠQo,|491ey !t:e B7Q _XB03DQTաvIԮEy?CĵSk&8z 08#:)&5 A9@}a"yX/O13|`pt 58#Fy!yc-&4~8Ge=9nVZxO움j ƏE2J]gcgvcz(Eo5t`=\{cH2=1N$jɺG:Z^Y_n6}^vfBcS' mE`M)hR& 0PQW<dZid9!Hxjڎ&6L-K{X>NP*DFUM]fe4׺ねE Pf#hյU1g:đѶkh8 a:Q<-4; sFZ(iMr+Gpq0l*G*jĞ ۓ ȡ4KJjÀ TO@.y\mۖYȨ&51V7j4z^]=b KӽP7;wsхD4'[X_oӻgVK^sA.*+[ >W\NF鮣%Lhl6 (D2镌%PP&i 8Y0Ќ13pwAJUĘGJ I}AF6#LgT \9 P|3)tz{REԣR6Ǔ:3,)p5*6tE6&ugتT9 ˆزUJuUZ !esfcEup"l$"_y]D~UdbL\ojjz+Jp1G~I*٢!(j#(Kf2 Js #>.̀Ai[fWw~9!;+c eƮ;Ԣslh\iv<r. 7ȳܒ͉&Z1ۯ57TZoԍjX[_nZ&R hо]J3N)\ajUi p∠07<T{Q+(z&g-sNv<(PH%"PQ=Yr:"JqQ͌O _k VtL-I?M}iF7VU}G[GW ` n._]c3ȉu='c_?}mgV a}}}Ke8|MQ__$ٱZkQuvejM~۟/HؼoU\ Z,?dx~d"A)?)p$Dڠj*:%S̻U{E12 ŚRlR2oQhT$XcőiSCtu >eEQZn{ɽ4lZ\cfjcܧ ފ"iOO#)Cz8c& x0@A,8zlL4,bSn5wǧ xVjp !zBp% M@jFMk*@ˑT0O=[.2O`^#*DMmn XzE/Fި/ P Ey)G7"sરU_a]V@C /*@Kz :2 fRU%2ҳRQ@):1@tJk.+}^<|{E0agve~i;O#8|!q}~k"\'GU kNP lҤ]ŪQFlcb+QA03],uVtefMmamSy6Rt SO LZFR# 04[DI7rv?[6͸ڍ5ŚD!sD:*(ƶ,z=(@k| ^ZVxkH\Y67:lrC9`suT6{/񣎷 dtZ$a}s{Z/ܠEZdjjzjbbB_z6E^v3X|y|ԣ_]_cԛ*-`#`moLϑL΂6nŠOdҳ'U@ŵƏDsKh0Q*౼Xq#:,9 K4a0"AM pPD xQu# +mտv`–,^B555`OV0Z,f9yH^ yըFwNcyGOa.XG dvL26Inap}9P`*AS"dsDN kk5[2(XMQK/K R}Ȭ0@1dGkZvTg6$č6(,g?HeCIח]P҂_ 6^D]G :mcSxAO$T վH lT>.3}.?p~۾RkuV OuHox[?;hֈرo@c3cZŇkzdu-UA~!l}ўCh!\0,+0y#flA|Nluk(T Z0eka"P{M!5 颈'o\հ0*FD2VcnfϬЌi6Z;yߔhȳx񤃡Iǧɂ] l]:`r򑏑a-̅ O`LB(&1w3BD>.XC ;WPb| ]J}BM J򂔵K`^u: MtH&'q. QBa &s8+WL6m6&TmlfRF6tUHmrQみ?F}fQ\CZLO$w۷ЭQO??iKqcٱ?!"h5{"綛@ .Jr"?pWkXGHcb-u^[3 8Zp b-yD$'q`u\(,+:#PuV p0'@IQlΡM"sVa4wIl/OG!}ԗOY2Q-WPVͫRAbzr$4Xc\u-FYf `#pG(fG|CV{3 -263fFٽ۱ 0 E> In~ZSw!Soc]Hzbc 1^Z&!sw?Jr/DDPNEXo)gSJ 6Nn9q-7rӜ=&z1SIμ%I۰Gn={w!~#&1,Wwg`c(B\KxOA,hiM ˹ rT$l͛@C xEV` 1.+\Y ,D&mjDjyJGT#!`eT͈b2>5SG Y^&I % QHM7Z=n00C7B=#-RrH_6W@E9I9@ &\h9{9/? u!s{u8]{cSZ䭃tAi͎ň_3kF}Rg5_-~a˦nUM?,5!nD}sJ9@dž@rG0~˝HՊTE$@ڛpxf'6aH%VQZu=O3ip:OS66GcU>~Lŗ|͗Qk|s[kw 4KCjyY7֚`^BfRrT>{ Ў"(Š!Em*~}Ô.K-12χ>hϒ`=!MSSsYaBT $269"܎mǦ'pm@wrJ&g*,ܐ9ynSi1qv *s}TpGVc&lÁjg*-Q/ G}G8e_˭?};>VV`8$}j굙? U(x^od7gٱ;8VxRkbl/30∤Ғ?B!i4Yz `r93[8<@ILmr<ꚝT2 WʺJջP#:o̓@5p:C,m\9a~)^gHo_F-%Uvz<ē,,YS,ܘx4Oտ'G5"" w{\v5>*gD$SmVWC"}Lrx `A 8@'&sw "T;Q$it3% ;;Ns [5xMR">Vv5_7_DZֹ*cyJ'GTw;_͑9Bom "GDrsZ^G6W\|fjNڻ~-*;@cW6UkF@{rMILd/㶷|#c;_.@F QErYg jx]-"jRU*lLcl\9)oQA8WPA%o2H:PXcU1q<0vQa >pEhszOYC3QGjƨ\ӌg~ g[znN?0z \0>=؄ _XY76q6TaŰI*Z pe7N eqV+X^(v ^.D5mXpqA.c>Y@2f`f>tV!8PC^TęlR+;C"R8@kaT ] QL@ӚcZ2"/#By<{&*`@ƏhRsGSTO suC{M %l,%y癈sjZ~,GI+(v|Љ#[}3aocvBg;ۚ GSE"(+҃f) /}΂c|'Q,O_4`TTGd{,'S&o[×1 #1ʪT8@eNFVib}y_JYrō˗FMɦ$qL{W|W{^ 11SThN19=eMNgWo`(b1̅K}JlF}fE(2Vvjc:~,A.w܎y1h/%: )VSI}W(VJA+۵zK&DBm$RP2 ]ra]"_!yahmz egVVd(_G~S+"E JD77IנߪLRf 9AF0oI5zA^)Sop}yc{/;W}kc&fK;Y0{Vstc<[18Vo&!L/`'#|#אַ̯䂙z#o߳`emRYN){ "`y2y >$ C܄8zCqx&(?#ޣW׭l r%@JU1`+{A#öm ɈJVK@sTgnܑ*!@ [v3JVӭbm!gjM&W})Pk (7T0k[ZdZdAwre}eY|jhvemZ$I4np2nEdvlucRf1ڻr q{>g ^~,.Пrj5 =Q&$*ejT!V Eʧ{<ıngvn7uWg8sE>r湧O;N>;I[ߙFΉ;qXYG-wR*8q&O|退")> _lE(8,YBV:z"ͫ5I, }l@Q-HM@ P,/שÉիHIˎդԌ`3) A(*YT檼c|bZPJGMJQk2]'=365}ߏ^y=눵kn5Z ɱ[HVV`yU.p0$u5ŮpuMLslMLP0//cVז T :#O {-q jY\WK@$ǼCuj4jr~Y]ӕ3Q!׃[_k7hN:*ٰXB?rPv]U rXyY6>xaga[w!шRlF @bIcЦM.^ty'gf9xxcdXQ%:X&d61ž*W\sg9)Ny+WY]=䊢&/sxN:g7x=Erʮ.{^$Z^#BELLTGgy&]~ Sh5pVP:kkƾ &y`,4:ZI/.N*LL0AD(@ՎY9gb\6iS1gTD {gۜ6֗V @{qڳA0Pi-&@ egJ4-_4Cqd>es^D<ˈuT`Qgq@Du ?DW+ hTPsl*Ӷ4|mTW"D6~j7ѮD38ĵ; L:eYnrᅪ[n5ʫ lj*3 "4gX /.QD4RaMX<\Yr9|0]ccc}`hRu.=͙gxS9/ai+7kJߘXJZ#+e-<>Ҁv:7*`SݴZ=WfxyQ?3\x%COL=,/,v`A+r$wL5FS C y1f29bKiXOuW(gpG% Aչ53 s}b!5eW=A2xa-.]EQXn%j5+y"FazYz DyN=+GI"W9:Po5W56S+zìU{[VR5$=J.LGr<26>mPZ *bc4밾̵g-u'O3Nܟ>ӏf~'c%5)$avzʪ2+MlW c 7d6<C*}ԧ}T #Q3N2z`]' qh dQ@ܤO<ƾXZXg5 j]ɓ7L)|[J9C4fg~4'^2ў4zhm}p;q@*2QffgyU©,27OdW~;Ω~dds1GS"X9^ϣ0p9_8mcwN5,qcNqe.T0Wfz"ioh4c)c&Y ||)er5hYgc[FPWˇ`@(kk֬Ljgn{̟(K3h'[%]yB5,T՜a%ߔgt0`+[MG̳$<6ն-g!h3l4ҩ#sMPaM+0E(kL|)NgC5+ֲSZ8W5RXC!'UtP6o,>Eнvlc~R}#$uI$M-{h#<ߨ4:D \@ǽɝ̵gٵwGo;jI]c^Moc1iYŎկ,Y.r/-XW?7}7q1Ւ*U>tH0: x豇y[Ms8Q* =nUf~%G]+|][9&X(}2o8ŵkqug8) }ʵDIu8/镩ǀ^c"Bh?NfAؑٱ^C^1|W0w}F.٠\{8__\|/t?gCQu<irpqׇF5쫾ɺ/,r{n pQ8`!ZCp %ދ'rB D1N\e7Pd7jv[~;ڏE٪cQ*j!l_bCsXmPg7&3:.ZG{c,/QACE}zSn4Zq${dl|j74ݳ" ( P"W7'0ùrTD%{jqԞi$xrĈ;cudFI'hAz~w> dLjgDDJ Vl[E0G$H6bh7Vy,Z D&UGη9=-\ 6[)݁ʪm&6p?`3陵Xq0~xk"zp>vݷއ(i3,+Lw&/U!ֈ"e;EVLLoʙe柙s7xOq^ҧ3Xmbz|)ƽc[jj*jtPjb TscuuN=cz>wCP=ezrezݞy@wmW/2HQobzl3#P9uDI5HLb3`٬QQ`}n:-fz=g)e4sݬ~Fyo%8ʣިOO_AqAY%z/E/g<KD=fDP=Y1d LBZ{!(ff&e9=XҠu *?}@~C2n %@aE< -[wKL`!"\GWҹFٳsjfKBc.K]/ǼwLJ0<ޝ1?Q nӞzcl`ѩ6 RQ8.[Q@sTHUԟx %0[3q>G,5 Hg ntQ"!Ilq? D uiH07Nw|Z3Z1$&BVyKE2F{E&`;JJeHWk{WGwB39^{#>U1?{Ȯ>K46Dŋb^6baJfQ@<~* 4TR (}S0FM^9ɚ)\|ե\뉚e9YK& l-F0ǔ]y 8RJ)su CB~i3|/a#H* _~>^lOLҖ7Qk(Q&HCC\@+Gwo?}Mk&3i# 䡣4U#m6*A4DISLːw*2J5Ę Щb,?#1B[mY3>Ē NS@ i3f䧛޶'g%zW1Z)e  Nvǩ+ FnCh:Lx /2-qkZ\5[|ܔL-%-;P!48jdxȸ7sNLJ*_/ C]1$M?d/I&BQK,W(I"HMDǀtqa͙S<]_ds5O9AdS ypö// Yz*RWشq(*$kRl\BL L6k:ɵY}r٬_}&:SJւY*IA8{J^#ug)o4RHtp/f &#YN`}  t_|  XȖ tuyhDGQgt!touDgʈ g3g~Cv3b}Q7פ(|'EQ%v) 6Rn&L Ŋ9Kbr/ͱ!wGG/?W4LE\ w~cs@+,5qen_ܒ $5=k>|K\ 4i)adF96?%I=L( ڏDw@]gi(fZ?CDD8vwl( ifH~QcVXLz$Xz],&|v/S:~wQ5y˞[flƜE1#fI8=xx}5F^MЪ#4'weOִVjc{!ZM ^ؔĶnZs~&״8[ټnZK K,~rVp5yY<|TH s FhCR:jI/[j/ LjԘq̤ZHWTjs06Oƶp"s˔eP $@:(;7m1HS_F%W1S1WDcũO>A~I9}^Ug`S_mlQd>z4Bg39$$F$|ԅMd&3@?쥅RP,U6eV| Filq[p;O}x^e ZGpljcHk[+;!"B}Q~z2[O3釙xp?i(dTi:~= 詯QcWޏBGUwXW39ĝF"80ь܅Zl-n LF%n}FPskSlè‰Z\aC|i$3.mc8t( 4aJ߮Ѵ4N)?|&AHn߳-wa:5$vk{2G~5rykҪ8O[71Zw.u*ND>LƴP6i [Ap~ `a)2paǖ| _gl6;JD*4[ܛS]2"` `#ŦslbԨ(&1BP)JӤK~7䑐2 ۘI2&mfܷ8Jt. 3&0JZ8<aȒA+S7$KOc_O\`7>B<|>8RּOudt[maṣ֐چicʶfz%#_a(;Y'e} GӈJ#;CNN&H#3,RVfQ0قvfA8?\!~T*eE-w7=Lwc> V<Ƶ|XfW,C8;mZ%%ەVo1p_q Lf'= ,[{%sRp2~ w">ꀹ9˭fBNYPQf%tvS֙0rK<8rW8:a|gL1 CxhLz+R1111Da3CAfq P$^\E >1=`FdkZ?r9K&T=%$ 4Has@yJ}ٹIӔ)I̪^T_;)[iZ&ʴ-1!بC)G#lZ9nC}i1m#yg,3 u&}:)ii. Tzۗ_"L5ÃcE5&\.Wl.wАofܧ z/+~LF3^6(x-2u:58s4:͒KL# ;ʉ@JO'ݲqL(1{^:}p;Q]s CF8U\lOr',:UWx022ӧMZ*L 8YC"A`LL_ + 'Ő"Oy\c VmMLZC-Z7kypi&J^Hn<240m~g!fdF)tpΓ^ȒgE8trW?&d Ҕx ?r~iC6?b`+6oFu P5~C \#PܔZڪg (UֆFwsAE&N"|KVyJo=M1KgF֎ޞgHtC%p%lO9Ӥ6;PLشN"~G$qMz3\w57'MrI5x…VںRԠlwc 5Ķ`'[vzq,gp v43OjMiTh4j") tC}̽fy,ўt%eRLMK,^hDRkTw2qcGlcgLL&Mk%̓ !4L$eRAg19%e?ds(${\MPqnLd\R"a1O١kNDRY+3usQHZ;Z+{|fdfHci=#qG4mDz$IAT.Sy=\GJs#c:yt-^Icf&0֬wά)̣|%;^3)?9}Z9 U6K\-R=ZnͤmL>i;+@ak9=b؁gB ljl`pN6֦?$~E8穯'lW?7^Fu iune\{7@Qr陜Xᦾ\WdnZ4]lnrf %G{ѣ}+WN@JGuB+F|0@m|u~6UW1+'|tqY;i7Yw@0`R!ʾ⨌>b\a5Ub}EVM%0۞`Z5ݜR+!^1p|-7>5؄`ʔLsG/).-wN J1* 9"""PY#2i4nc{` ESS )ҡ;&dcQH\/b7ӱ2.\kǒpdNmqp}i9l'L54Eiv5`c85!s_"؝_;7FJL|./a9Zvth9 1#=qV {3 Qiœue7>3)eZܒC;%uS8Q6}ON%IcI[r#ޔi/)݈Sk13 2[P2s@Cqj%Iic ]]21طcġ>~DqLY14fO - L"ː ?!)g iA.fR("3LC(666b ?Gg6лxyuɍ-2 O%|$Ih9'>SN/cF9Ӄb_nvs: {U!Rf[Tבp HoY{#?jEq y0 /#dG6utu?;_,(Ds7Pe! RJ38(?noME6ߠU|cǢeAؾk\rTCqf%׻UI+lce#6hֱw;CFF0 )T' IЈdL$&^kFM006Y]7*0j"@P6^eyYpuw%b* ]? HbuKXw'r![ElcQ- 9e?g1k|qjE%}xtR* C$t=fZ s8c#!ިf83:e($cT"dY\e<3ק߳FF"Bi2.lwQF4i/S۵fJ2J6fTkLدkNjIuf8#MSXS!/?L&Xb%i}gx(<%e량3'Dn:;{:>sTnz La=̦$8YMp_ۺT6S1=o/ۿ38ԛuўX(RTN6ꭡ?REg5JA6Bn*L0/z rSMQ/8 S t]]C``?CTʵz{덳dc)N3C^}nŸʶ"J3=}f_>3]2RBj$mj9$ JkJPi(x|s/)FGFZO&_.hIFlbgSG=grޛ4ÔZ@*3 [\Q9e,N} (bG8)6+$ KQv'#LaTh ͆bzzs/mٸ[BD/Y$=ɒ\QWx+Y7 Z⽧RNG{yO} ڊ?*/Dw =[?I6lVI%o|5/{k1rAgό+k>R@\%pwfLDb%/|^[Uq'dTԟ1fWu&>x}[#J9WU4Gxl_ftčJ]e~+e_Zg<<Á9NS1 ͆%HǍ?AmE2xho2qzH22Yh[>w("r( vjuq{o _Miz'CvϿM21JE7MB'y֢;w >^{rg/~ӫx^b]+odak-N(u&q80VƦ:S{h\pIy}Ԛ %8TSś!zLZvxy| ݽ>A缞/yZGۿw+n,IX#P9m'> Z 3i2"(9Rܷ^k#<Nj3l0dqQ˗ENNOAkp׹[bf_*\oS|gOrži[dp_8F3dPs x4ꡩT5yY=GNnYlj~Dɞ{5k^fLlk 6};g㥩1Ck\!uҸ8 2KH8Ӛ=gu$8!?ی|G7mؑG@i Wr`*Kޭ};imJdp"#iv3[e:)f䳻.t9933Ӵkab̤~[<:@d}ѹ`rI9GG' ~Y|KRv+\eJ.$ !#!VA6Fcx5H0=?fH[w.AvJb)j3^^s\C'㡓'uhxЬ֐V%cF#4A&FG'I9o6c@6zh*՞|P~V-͜{+[J(ׂ_)Nz:bL,YqcqkfT8WlJRyk3e#xƀ@{Htǧq53o4 epf!Kӿ%NZHI{wc\G7O^sگ)2 EYK?AdB}*X$I]:iih&ftFj#A>H}(ǎg|BBبjB,Bd2i9@&CilG7},#ऻÞI2J!Ф~o+^,:l+Hl0&fPٷ8 s&N9&Fh$Ib%RAbg1 L FݐI"FҚTHZx@zhVeq֣ ގRZ)m"׿~)c7.pJ)켋_}D(\u)OOл|PmLRs1q;#w<Osx>'/Js9b5?%{7l1J5>^Ŝ ,\ ̬C8E v;_/'r$) | XiL| $n Z)լכZ`PD~0JgCM6jE$uϲgZp(gN!H뜔0;~/$F\uniQD娏"!$erhP.qb=mh |!On^pW9CBe{cIr Hӄ4 ɔz8a &Z:BݬorejRc 42]{g\"j :9w|黄&"G^3z3Dgs\ѽx5Iu woѨR( O:.K0Ӹi2(aƚ dyܷ}{6>N|X^VZVYs\Pj6hϓ֨I,IA_$!wJi 툦ND6k/&7OxI.: %n5<ORf=g CQ7wL۾u;?la'*LdVe8kzxeW^և:9!V'[*s}>c)QL`Sc^0Rw,=44,[Ge'ڈQy@Q(ё\G98&6ER&7kQ{z$b5|b;'>b1I  V@5}\?piu,)ʛGᵙ Փy~8[)m#?+'BQthsVq^e=d{/ƾn'\yu\|ds13q3s}fYlR;cl*c?(һp>z+xҳMX5sl JF#5*|Lg. WYX؎gQ3oJ֪DW2 ml=A)HJg DFH^"]'8lTԬȉ7'N gK N+ŪFB&y6gq8&,]ꪧ'x%e<ߛLq- Gn;$!|+z7bH=ϛ`J;-čϬ.eٵgٰ!d b"BXg> /ޞΊ=L3cy$#!#uZNw~R7`׭%3rpl}!WL)~8+&6(ܤOZ//4D 2uw~H-4V Qe ,ВƤqP PJP0l1u+EDn<2J y0k^t.$KϧP1bd:8fiYV6C~z 3X JU[w@G޹\l^Ó.RŢq@8~0 )&FF8u|{ICQǑhϽLq6QnoE-nO(I1E  `}CzMjCC^98K&6"dѪt- 3˚m `lQ2>+z\֞LH>< Nヒ1&i*TM,𲁡E !VJgҪ0w} VpIr虃֞9-撐Pm=OsjҰ`f/rBkDQ5 Hȍn{RfQc]i cdxO5u9>9`:%  ٨2בz~{6=+/j)ldb6Z[I,Z [s2äMRJBgDF`hȞeϘ紜8zn%(2{-U7BDMܼeOԆՓ$M#%Bxr!0eTr_g\qӋ';i?K0cl~O1|p(I9wxxKo_6$%ϙuA/ᐑi48)o{|;w 6:NZ1*b97Nb¨Ҋ/_*'LzʞFP~oz: kmΛ0F䡃&4kD7|lC3{5FޝI{^ty=rrzC1v2H>w!wo`룿dߤYFĆhS\twuk6O,^::hA`Q)5^G)t+j6B:aqi=8%l|1lA"bH$@(l: "֮GkmrFq5]=DQhSzNP,pѵ7?w>}LZP1O32: &Mgj:Jb"R! ձQc91>< TD}963v0=nFO%cfif(Y6Ѵ!%IrH$ sA&Fpzh/Kk!V JqFHD9TV^jwޡꃻͲ`ٵL&#Mc##~E>ÏxEW>Y Il}{1NN2 TRT\&㣣d:9}b}7ߐ.fS(rBiy6CX 1ȎO r`^KTm*LP4181 R*HUB^4I&gIQƤIj@ ",YJ CG/i/~s-pR+c&bl}IBOiZ6erH֖bX W*Zl <5 w;>KR8jxD0mi#><=Om>I4Vl'.y\D,T Z{,U\r[d \Ls>{fp(V=Eh=LuVJ39Sv=9GЏoLN<G'>a#PVRZ+oFU(q3ZFZΚE CVJkTNy-Ciɋ#(1Z#`j1M?4" T͕!]H F1J [,3ͣ9g+8}Q5DAwӭ] `dqDּ~m"rU_Ŗ!m2E~2Œ!L.o@L6: s;HEk]fAu6* R +$6EQ3 Ʈ%khFblERS?:m*͚D{)ΝGKs7ca16:DZ2>NbrrRDURB+(hY`Tc'clˮ46C6&"1d M[5%ayj|=$Pctzf͝KBl{ 1)q*l?L`ll DŽC4O #Z+6Z $e=fQ~bf}TҨgt/ZȮ |_*:z9aMe8:ȑyҒAs+oІoV8O}@){Щ#OۍhQ# NgtAB&g~eFL~W,r2^ENnLO`|jx82*z?g9M_'!$I(?vV![,s7y1jC2=c~"ij=mKCf, ;:b>5yU7;;kG1Z ea4щ"9~s YWvl:1NZ1(N7Dcd՞Rʜ%1cc#T*#^) 6e^k#?ADXNh R\viz0V&|g xZj0QhfidڊFL'<$RXG&  ͤUbQؔ"6҆ȳTSS;22oVjzش%bHߐlhCGO[Qr'AThj$qDg=Dƾܹ JFmbr?5O+LIBN^=R7ݛogeT?.5~|BLXqEI郈d̯鎹X}-4?}Ӥ)iWRxSANYOج(I_F߼ (wC-ڹJ!9.udO۔-7J㓆5YsovvyDb1k$IPH`XMVnE!L~>%{*Z;a+$9l_D$Qw5F ]?333333gc%Үiz81"[[VIK )̪6Wsk WZѹ6agĻwg! ѱ )ʘ! XqF%tToeH3[zp׿y2S,0:\*X%@!B^ VCnl!xD]:Cx޿e{?=޻>evui5z4:^6m i2>,v^ōgPu[ge>DQO>Mp\eQeUF X#i?M4"YF\B <@@UXtN"8eIg4QCépܗEK/3̊gR8I OgTNpt;^[Y7H1.tQ(__y LVmxT;5KP,IOJȵ)>G+qRLg=5ؼ:o[cvaW懾vg1|®l?cFxACX$|r|{w (BYkLYjh.@PJqe$zu JiP?@#l@ 8Y3Pz]*q"pХaƉ|_M:\q!7 1_ IW&ux~)$Xp,5tkaWG>f)@60eӟwNos{^p'3J~AFn9+&Oh\.t2fdP;.j#o"IfVw|4 †/|&SE9}MEuE*t>1ܳ\~ 厲74;&#,`z  ěL(g{arW޿dEB2I)Xlq .VdQ17N(2{ybD(x#-\3"dngDT*]FSj{wQqheg1VZط%< o9\؇at!56gIaOirH?,[8l%] تFC,L~mGG!xF 4u)<ز<K^ʦȦ Z|?4 !`3c@f||3=0H-F a,K*N:,-$]8 X$[DلlK$Ha%0̝CcŗjdWCcl:ft>!lw|\uKP?NV4'\ %=!]N'X7TǏ|hnAhmpY %а`^ 0`* P)K }:fө.4wt@Sv}6I/qz-% ȝG )'"Z  2܀q|x>d$-$ i?#_eh65lOcXE#߿FRS7=QMuX+ˢ0ǣR ? hS`fX$/?rwJ?bD/l/rv.Tt:A`jX` NƮ3} X8񔂨tqX7'>F[(pjmRW\t Tcz6Egεk+QaȾ_C>=vbeNȶ5QoeAc8u{I:NF 4uQeԺPYtMd[~m ZoTMk,[0UHv7@5+`p\[kZM >U.l/T a@F^]]#nX͸TJEC5Ees q__^,p.%l: %mW:K*; 5 h%sgF|:ɵ5ǨdMu!k^zQ Ϧ;k8Eҕ%땢$R3@a-Lu?Z8!S9n#s1FC5$RJ"UxS',CJʞxXzΨʢp7:آ($Seyˡ9Ya0^[Q,M~(5ƛF+WB_$og9j{wp/}b]g? A Q\!snDֈ$I* hNTuFSG'd1K!d|13ׇBg;&m؊3y@0x#xlU@Fw>$0@x^^Np>7jzt[Tɫ;XAG5_2Ve |T 5ZKC0cu3qS*XX<X!%5mΧSOn\ ;Ce|WӥgލN$)ϴ|>Gynx4:BeQX @ dho \S _ǵon="#z @+3QMuFj1GD`U+8Y1-[XQotpeËw*s!]o+q-EQ$)x"Rp6guN Yzs`׳H ehFkt` Q `fs ʊ` $βt ݵ g~~]׾oyzp{=/bJ6C ~oTb{5tDJ" Pɨ:F0If:c LeTQKRܲ$_.G泈0$<,+3̫0oBovǵ.q@[eX >CBܭ,I2V&8, J)ZU<gSL:<Xiy8,m 5]xg}-{(F:؏<~w`/[LJG$y(l/^,tMX]"O3QL.cq2jc)~Xč&Y8bU;/YϘ?qdƹ}HMxdWRprD'0Э ʁ+E>H (:Pe1J5~Νw12(IN d* X n~$ A3!B,c{X!_TJdF1ݹsp68t5k_Bv,PZ\Gouf$zhbn`k\YeB*m2M|m|ϧ_.4_L [W[!dƝ$ 9&=iX逋[PkxKw=4- hg% ΎTH5Wl]|5F>e|I Mϓte@1ڝ+,il&iERh=? jL& R޿@o5Qɋ|o.9Lipw6+K`~ݺ+pI3eMJʘX>l ϓ@)$T.ȵ ?$Xs ؇ 93BfvDyQF#3=~%`Kx 9ϣLaS+†D'/J.SUӴy폎V04ϮQMuXc2 Იkga^mT6 rՍMmIh{(Ӕ籰6RV3! 8;Yz@#jjn]]o }΢(,f@2Š,\o0)!x"S0}GpY0S`^pJ zf GG|oe?S<3A#EQG˹"[.i:%s B9+/J4[N78x8aI2D*Y6]MUfܰɯ#~COFtm(H@Y ;㜛)hg)kKFh7[>ĩ$ W,:LtK@qG g3z^,I6 QL5QG* ^Vat:VO'm_?}w>" t6d'*7#uI9LX1vu$ɜAA1Rvtl}BepF"XQY )u"4:mdF37$`Ơ,rGx?"M1xtYcc鬎:Ejt^$na>B;楇1N%%<~_JM [.G?mHmqSgeyY4^U`Jk`sŜ2% 3I'|uv:Fk͢z}*\y`'[@"9w)Z4EdeBGQ2=ZE^б9 ^+>t:"nưBBIءl9jIJ5y2g}.=$>kQtIW@tHC|:RYq{:g8e+eH v^XY2)$sXX  $rd2i@:.ܥr*/Ĺ3BІ\PN)&rS޼(F2%$f1d)Qf) ~>5%o߂)K4:]ڙ"_|F(<A=Nd]S;;[Co5,Ƞ9&GG8}/3ht:hM'`tY1Lк*WQG?@:~nN P%> VkՂe9ߧc:BNv*!S9pd=U^IYDERmk"q9tk:>VϠ_P2 .IG!ݏ-RAt}OAt=hO.41eIG(&7 vܽO[TV50ԓXI#a9Dv("܂:  k_}!>[N|FҦĖ柴۝?jKgujegyЈ q<bHj-|g 4A꠽F 2tvJM4swA{llRGju"(dz3: !,3qL6!XLƈGЂ 5|iZ\AZdkYb? >SPYGEu!EV*mBLX9_UIm. 0R|~w!HT}7-NEx>a"[nԥy,mU%@Lb';((F;-Ko9>a$ٌE,q,p|:A>|/pNFHK-}Q|[ >ɽk6gvk|O Om6NW\z-(Hsud36\(ϼ)9g3! py@e\eڸRpӧRk<]l..72X3Pȹw*tc>_ t.\jCs3wdE18AQ Fȅq>f^h;8n2/ h#õmw}FRR/L }罘*5(J?nB ;6M,v324֢j!@A$A51n'hm 4Xr YqCO9 冕k@YkEٔS Xhmڣ0` URP9/tzj EYwk\\ZxaKϾde.]L=QO'QG#:aoA <=.ǓYī\^ E5J`e!ۻ S,N 7fvefJUZ3d^3 ],npFU#MPPvxR ",8kX\0H#7,I6AT2r`V'.94Kc5G? *0fK]P5QL 2$tߝ p!`jHaQ9n6j^%YG)륔1h#n4*c.4B#LSy"˲6thX=4&ɣ)b 8#(LITq3&<'p$e 1/=ْw ~ 3̻me z$$d u2{j}VE贻Tu/. W^&c۷6tdE@F+ g l[At4/mqc^4 ΂wi";A0Ʋ `C,*qǸu ̦#r3^|4,` TRqFsq@O"`_j%ꨣ:~2M?sܹ3Z` VvN/|!%ׯasc4]cܜ~),VJ5"پ7U 4e(hZ 2[F \$+Ϥt)C@A`0׺teX}pױׯuy Ȱ<3Sq]L01_v |gq 'VH%S.z> )| i{_:ɌhX~zO޸/ H6<<@>;ׯpڛxCB M0𸜥V-dFI]నr$A1 Q|`An4 8Lò4:I!YFY[|,v@.#Z}xq.c,K* eUuɌpUa>W{6<˨l6Li,$-9+ : :'?fm_!01,_v܋or0Z4,ڑei`|y斊 <7PP, 0 Pv#@?Rݾٚ$h@S'Ǎ_/ѣo )ˤh;d&y8pD3oط^\[VC"aeKl~>+;rx+[r#D0`-NJy#fm 8a3t{r L5Kj}\ngs:IZqj,q.ך% \) cTSx?gB֚p |ryvB Z}-4WW12o_.蠎:~QMV_|ߛ&酻W*lYG?sJ,% EbpZt#  AP{s5p 7xr/2HP(E4`^esb4s5âHbN G?"O0d^ʼ<lOIO#ZG'СqKO>1 B K6-Aglj<̳)aReEO@Sb6]i熧W$dcp穤<n#f[7ptZ.`i#@DXyIe -)!f#Iۏ>frfC@`'VfA`fj0Z)ΜNd"*2ෑKQ.؊ڌ30ca3$ c^B MP! NS)I9.0@fI< IQA5o:4 a-FmA3dwX*-q_3)rvx2(iu/#c#搲4lI;h0<< B*Y]]0|Q h1̙.^|YaI/3iJUtvtÛ7xzPA3R\ 64f9IML/ÆKY|D&W^kLW`n -YvHQVv8Fp zIPpcݧ'{y<G xR sj$K0)^?0ɲLPIE%`v>n+B ˄+1jC3g}%4l=f<95WFq,QG50[۫[ >w"Q+U>ٟ O?M(PUAYZJyc̑YYhkrk`e*ȲfCzzLT *oL,YviYi#l0RS1L㍙KuMf0hݽz[bp,TwV*+T6ܡ٠7 ~gnQF߹浤`vC#X,˃?4dĩDVR~mYPGo56=i/~#O=s_7jHwedf[_G0=:$ o\Rky9+s0" ~* Be3&n<|a4-V ,)hq80t%XXdc$A4R)v@uz=!PQPT3+sp&[:/ p p.ԣuI.~4Q&D mX4Ξl(r`<?5M*QG5jCggn~oΏFIA`@_a`c޼vEa4 ;(&Dp13AOs8Dl.58 3 nBLSfTl T3hC~Pn2 \RW<t Ƈ߾;阆x.-D@-)Y$s*C 0>EY"YRrmrImFl]s}Ea7la4jG 4uO8_zWD67adEt/G<)U[ W?>$cq K~Mt8#E舚4s WVI h gcd`PȪd9Ϥq_﨎 ; >A3pp49 ܸNlܣTgE),gD+)f!2ŋE!R6_Q,o4M77W8SI!{zZ:jc1pzj̗ܹ~=!}t{}*.}ڧ<EJظ> ??vl3`QP1f:-S@TzRAq5DgQvXJ .lj>n*:c`%oB0cPydG$/_e#ZD{,ȷ&6UYd:!GH ]q$}^]"'swE~/q PG?{_?~7~+4K&xCz7Iѕͨff3z,рn1{tMa7llhdy}v0ldQÏeF`)2$K~[s4suPl_SdE4N =t'}/ sEu:tsf{I$-B (G6.8Zxr]c-e3W8:ڧMp<c!"&R%–@+M#l=%  ǖʢS VWm]Q.x6ʲoyJVCY/; Z!Jc+fਗ਼8a26#*0:Ag%KISK)y-J ..О>x4l1B =yܹ۫/ZM:4uw=7_8N<m>Qx+x>J#pmJ>)n MK]bЍ.L؆t'!@\.vQ>XI:# ~ns8/Ê/^ܙu P0y1a+ 怸Fv> A xyn?$s;K)o 7)l zyQȨAqRph*HJKώcY\l"CNI93[18uurttS"X!H 7ϝ2Z=@S>ofdODnH yf QQp 0]JJtZG#؇i,QZm-幹TRPɛ5wІJuR GR#lxXe*p&4:Ţe 5c>AL{z\Q ζ߹,d`Y2,voހ4ySvq ) v,:!gtv7D`c ~:@UM =$?9aGՕ5/Xdk=wz >}s+š@u|M^aΟ~ي/޾6uXhhde ~%ڽ>Lr/ b&nM0&~,K(^oH܈!_b i4'u: p#KEz 4,˩|?p6lVINZWʗNTt-46un!lIYT-f7HCn$q\NA^w@c \Aw0jc$8ǸsM{xI`[F@e-#K j`%<᠐ܻLFGHTcnCx-'^ g?C Wbwi <4?m/u$Yk)!76uP\ }7 8 KwC=$iBk}!|x~nI`^DD<֠ldh1|naxϦ_-}LMya黲2Do_,@{~iΈLA`xi}z*z:™ʕre y/#4op7Ƹ9F穂Q;s:("I 3V=҆& LFF-0Vú~ch4' vy>L%t`vkޤɍ(C ,;5J0wVIX s_ ߜ9P9PbNF]w^*ı6`lcn ѹc߼&DK#@ARJ281 |f,+CVh8x!e,Y cGs^pprp`8<͋wx*V3i5jZ2@SGf{? yeY\ܹ{Enhb26T.뮬$ka+i6ZX[Dۃ?qSZ+Cd6lZ6:* WvzT}ᐇJJX\J p2\&$$oxƿ^*d`,ˑKwU߹|>. jq^wA|ˀ  +0\YChڈj\J@JA?7x*w|e* j6yҍA~Gb4Fu󁦎Tb||g{oɹxyN}"N6?s/4ڝ%rdjF3~w˸}YDaD #K˒Gwdvzh x:f)4xED)GXL' 2fZ6> c*YJ?H'S}4ub#g4}lmƒ@)yg? OgJE%3",&ƫ$. 7J7V`,HM,. /'"(SbmkFc ,*ހ]ON4g.AZ 0]êpVϙL`[)jМAII%YZ@ [5Uv5ʍ{Ȳ3B0J!jұ'\吞O֚g@,bu'G&5 ʂ!Kx4Q;h+tʒ$ e^ogМ%)0-. )rcn6D`~c3MɴDz сxsw}$DZYA)@f&$]CF!ne ፝7f4Ág!]w?-X)*@,%tl+ù$287$VE$6|פ)x (ap%tyx -*)Zhu (X]Eeō&I˲ sAfW쐭q!õ l>] i2 u:tqF3BiNl?O@wuX%y/Sh67|ݼ[q,VkxGmT.^ 1ɍ:o>|6}/4u/VO{zIQP语A6[dzp0l@+Tqe'Z7$\dlFs0]˖Bjv [م/&ΞStc6<9xC$`29-Gq ˕ dKKcXLYGeH4@rɼ.6 ݒG[rbZ>$uקm=;O~˿"Զ:xMb~HݺyexmLGsu/8/.Ay2X_۠A HLJߑBg7`] a+n0TTeAĿ|(!1Oz4%+<ܨbhj$ͼ ]qIR%N(\[L[˙g?Z~^^hPadHGE\ bykF]gң0(r(O$͊prFT*#510@sXΤ{ =߬l. v͸rHϣ:". mΆ~W[ֶ۟Zc:[]s_C`PW%ZY4p|wa|) =aڄWpWn%QGb|tD)CEHȊK:s2d0g}VUmT8ۋˇa` -mMqvdsOpx6LI"6: NeM V)7~2H)tcS< nnWmKfJ"/K6ҸJG՚KjI?n y47p,l5R`^.:eFg׏-sO֚(<~ eIKk}՘~z+d-"0}߇}viCSq;Y^TV٘ qnCyk ma3.}r#*.,(\ ˏLPQ S3f{:>gbvyioiOb~tRŤ@8<ҒyZ <(ٻ ֩<0v<Ya lscztYЕyX=L  . ctڽJ6d@_ kl!O,|-MQۯtVGDq7ܙ >lw "n3g}h4-ޔxCvi}&-}u^(tiA\C?6`3B L- ` v`60jؒ,x }4?wG@n{u)9Q hh5~õ/}+Ujr|p9tΜų$,a^DQLnW],@ҸkOY4 cghx2&mPƂe>L"K[Tȋ us =n߸ߧ_YA~lPRh2X2veeYsN Ct|LVP@ 1y4{WhMoVjJq<:F[v<̛6 2Q'PR8vctI2l1fhsGHaa|0su &#ܹ};ҥX%uqX;sThffyn[k?Gu )FAb:v误#lm¹w? 2rYm]K7=e/GdOR%5Unq*/B(FݘaKK3K)JCIvw/H&3n7}Ao{ ۭVM{O%/h]PL '@m[0c:Reί_s9IWut.\Zhč-*-cV6p2he];gh'Dpܙ}ߧR*I?͔ ԨjIqH%ٌ$1ɣI*po\%]Sc\Yrңعr;C%KO}-0POk փ$YͦEV7f#8ͳR[4 dÝIfhkohwyүN1gARs?x˩Ta{W_l>ERD(y|6r]٦(y( XFu l:I}]3ifBACh=Rّd80s@5$`3x4qsh@Hܻ~~42g@6Ԡi9gQHNJ/zY]dd^z>f)eMe vw' bQy>Bk˂o wN%2Vjdh"}I,LưQtõOOk;QOo@S|1 qJX!d#z(glI~lIF0?'w幺ңG?Bʊd͒~ ,DRg†d2eH$pj16©xڊSݑR_QLTZ'vvF) ,R1#;F gnqQ$fFg.\f+pIvw d\A{G4ϟ S¿ WW꣯$?Y.1jN{ČI(ܚ& ʍ̯}kX3 ,8oz+Y x(x0'~G78Qn,=Ʒʡ'HTuӴ8|W%F)…Y~EUpO8YjEƨ.K%Ʈnon~,˿{(9)]rnnuhP^fH#Qg%NDžIG:F>/G~+v钬͖ =f"<: v;y9JÑ}!mvI,Bߋ,ύ1V%p_fCb:x.x7o}]N73RKkn^:*RNJ_޿bkLd ?s$1d**[S(Gݣp\EeV0a"q"z+_ǿ+ӂN-m{=XF3ra#7Tծ&ILiϝo|sKKxwPb7ڷDps'Y?$rQ |t~U*x&JTo@]3|h_&xǖɺ2s 1y3Rh YCY0 Q >< b(eX3qKLȍ* TY_[{/`-bB;O'S8\9Νw6ݹyN}6dLeȺs4*ԩfQP;'fulȷ7pXKZy o4 $Tԭ J ҥVIam-A J9Q Q8xB*^9s5 6@#kF_j?Q7 9f3U[F$Mi/׾,ޡb;^./Hzk#r_&5FeiuAf+']dYENyK%e/!P66XL4TU,>$>A=j5Ҝ?̲Ը,drf{ [ mq^v^vO`ibzlmm.0jtI ! _<1׽'o)EU"uew&I殬~C;-UUٺd"9|׽ߟE3e,,,ʱʳ^zGk//֯\t_1g?nsm:_SWd=1691GImy9zFy`q鑲sH9d߾OK~_WL@ 0Y+4&'uظ&l6ud4RɾX\7UY3PӺ<'8&,@BP2# DZ%4cTQl$LiYzGk\9o3޼s9dhwA3*];5)7:(\z*x,kX=`ilmmtQ9|XF,D͖݈@sCˁ{,qҋQ"(u<|k_]J"0NpsHyאm1Vx)(\X #:t&\W.WoF[(4$gG~~EL5 Pu@FQRgK3 tUq&ik ΃fmffxfP?:Tk0]pqeRfJia6;;sd U?Feջ%Sj?' `B]>5@Wo0)&Aޯ([|%v8̆R_L6bTWKp`YXsC{kb-t1wC퀇*ulcQRn|l2g`= H Sʂ$r}xk b<$== 3|c$q3H0iPMk7ujF#fW۝Yel]|7Gn]'hD) H"_>Q>O~#}e!)4z qw vhb-h3kaI8"$f:~Gᴬ:8^*(#.i3[TK\Ky1,hfARQ)&cR[hsFn%㯟Y ٟá;X~*LTG F2F4M)y_'ŀH /ih6tE>vgeE\]T)2ӬO_fxig&Qe"ّI G}UiaB%面Ԑ&:+jFx*,dى 7H0 5*4i߄ Mw"֒il]=Β>Oy KY+"BVeZcdy(C }[oc$%, 4QB}s̴P׵lk&d"@Eݮׂ F-ܭտ;`]pe%}hfi~}6߼0oLQg7LkmȑL'khw4ƭMhd]λ;ڝ6^=o2ޓ&jrc\u;ƤFv}d&uCuKgO|#g?9꠹ 3h9xm ~mN\WƱ:B44fj(+PApisWNOd~ *h2"}uhPN+'G]"hU]Ϋ6:a&Z -G"@^/&r BriyLf;hA[`LW/RmV(Ԯfmu8ylIqU%3- (I;ۭPvL| )"X*cwZ8>[׮ĎszuYMk 3fd;1#ͲwɅťCUUsٹ67˲lo8V\ݻqku:&$.Ob˺|1v;V#o9Ux5{ωBGElk}G7f{Q475( d˃,h EnWLh$!Ͳ9C˲̽]y_ٴɘf;~gnq nT_/ =O}1k+r}M -'侱?R9%+(/Q$F!E6f!iEM}̏ %qI3*=Io tڧmbv{D3Mc1^o وOYVz\ ׾WuP`Bqf><ڀBPj:f{Cn+.zuoRf/8Rd8R[ܳOGTn4M3h4sB@INi4U@"4?Å'&ptza:ip/ao6OjI̎ICkf^7Tm~vNkaWөE] YuYUtj1ov/G}{45za!7 X`x4_aN~Ei&I`t"Mj/.EHR Zq v:QVm,!HUl`DaVkdmrVKi tQdh/ʌ`LL -rG3@]sV*L91SZ@Od&#!d'h9ea[mC._o['>&{{y]&>!YzY {;9q=\rdB6;>aєmii?6l )HBD8p,kR6s7;䌤Cǹt4?O{Z[_ewIEYFf9&ٟa Nɇ_˞E7F46=GfZ\|At.r}:+x>䏟NEYL/Uwm`h4/i5j<{bx~w~k}ckJ fg昝]c]x`_pVȠb- ys@933ρ|/)6.g<boV7xιr6737`7eys0 r쾇y=α;#owIMK]1Ih!ihmXp+?/'F7x4ZqUSS/loL]e] JkBbr#v-df` wc余]<dfBT,ղ~&hQ^QC>"/F% S Xh+T8-4*[K0Ϣ, VP+_j !`h$vq9OND١{vh{XEuX{)߾ݷ^gavLjkQU7aFׯ1i%C /9v+%g=x_s{tr PtA]fr gM0nʮ#8kbqyVWW@n賉9A嫟$?yrUpVscx/avo%I;޲iRWF+3GO *A)!I(CMF6n f~;o?wsg^LIb,m{cuɢMS^ʛxx::N8ɑ /˗,l޸/!1epYց;hqf[gՕt&H;#-55-9H_Ժ>&7/͟nۿiw4;}0}wJ*: b&`ZN/M~hIg<󡟤ȥQ%ɤ,&x}ܷ{$$'Jms╯3k8ӜʗY9u Q~XMJ/iUS5gI ]YF5IL3i{Su A,| uHݙLyJKc!R5m4k~VKq1o(2Ȧ*ec aBH ; W;AAs݊;msˑ!'0ES5>qa5O1}afa+ 1ï9[Hks"n9Vs Y\E@Гq5M-~;wU9Sԣ cvn^ԣ3|_SeqJpcj󉵿8vɦģ FY8juL]ל|;yߍl&o4"iGnӻAc<NC%@+nJL:M3W}h(Yj1SW}&wm:k`6{gϡC[>huUuR c,f;i%;t'#F"a◉,KSM6a n7G])H倠e" F1FᶄfQ.E}I@1 P jөk=?5Z^x¤*IL^΅1ZQpeT@N$e"Wľ){x%od-8ydf.%5?qkk U%7[y^`<v@LU^(7ZH0A;dTUY?i&e:hС!~Z>ùgյ(@;'h,ϲe.VT{(Y$u)?T\Uw$7?R 63;Nbni4i/r駘BXT @!Gv>fnAL^D Lv ,*3jx9jis>d*h  $"=*t_F{B*`O5Q  |EQLtZH$6Q\İftb4H=NYrݏ\ᝯAo1xJ+&_cFȸBܸL槪. xՍLmwXEQw-L9'HFC4LƠk@ Fʫȫh6c"0\4_(C7[!vjw8r\x:ib&oکXYՏL 7Mz?Y.Ky2qd ,^3iQbvs-젩^nk[*{Oٝ%4M9t8ij_ Iil\]}Khx疫|mTUorY#RJ'cZyLƑh~|!Q7ފ{|YUwzJRaG$W5H>C>vQe TKɏA*@9z,cS;/!)ա (-*h1ǵff5"K4vx?E&gDuqs$-k40P2X'ud"TPԁ~ZnViTC.y֠JAP(MFNG!kοRr YYVŝ޻/CR@37u6KsYm7]3>jez+6KcgZ$(/l")ѐ૞yEqW}{;,;W ttZ0 yb|&q6z4ƁrpʢjeWS6E)-(Ȧ4E )0zx¹:iFUaZn~li~o176׮<Ԙ/n'>O=dmV^2{IqKf*+겑[$b֐=қLL;@qh Vf5@EATOJ`i :)6NoooE%ݟS'y[t9x$VqET#@Meb:=PN'/ɑ &7$E\!`-eF"62lBiFf7U&|?NFxi:0ɲc6a췚|bCrN~okׯR-:''w qƲ Q~wV':X|YVBC^}I.6S'杬9>sa;4Vok4(@E#đE"A"H2b5kytif?FN;yP\9[- *;_v>p Y=79viIY&͌‹5.ųxSo//H?9@vo-Ra%Z+XHEbtzFޔP8$mr49[(!"ZJ.<r9Ċ\'k{"X?H}L~Gw?|RtÚM+M15<*GwZ19b~kƘ^噟[Б7qá{&oulmK p03'D,֥3/p'i{ً&6쩚oVD3kbTd?f,E Z@~_Hc%xjT:ʞv++n܀p 6^iK *&Q{hj: t{Y]לx8~,5Z{]ӿN:-htoWIOp+;1l/P5i,Dg}3rN.\g))UYVeq{p .w4Zz4 ,aYȻ$*$P{XMvsPc"F=*H?3I6a8?@=DHZt_]ǺZRN̩gvB3t]VG$_0iTwfnuG\U}'h*X'g'oцMCk:5$$zg*YQWuib͔C]nHo`"ڢ?c{c5`s3,t36tBQ|-#gv]"qO@[&4r_{u崴JchFtHS1nRZB(ȳq;`8vj7@R{tįv qެͽNrdaί\M6׮IIz.v/<~ 6ZذGzL]퀪{UX:~"_t%(<)+ހv!`1o❞ݣl\< 6ΝN :*(V0VZE2 vpR>8t&&"3-DBLOt*1Ċ$=.[f!_7I;d]}}PL&k ,7HZA3;!xkģ÷㋒P^sU'\];7OW7ؼ z!WWuAÎޚLSS,Z Ɓ!N aNj[1!T^0s?VUճA9^ځf_L|U?vg87pv0\^WV"1o E\:DՌV谵-kA4eqi=ԡ䠼$ l1=U'ӨMW'}D#>1RY'˃}Y` *~OG2움4#[<i@n:f'ȶ.ndz0{<_{~9v4d{:3-Lkb.5${ۺ{:dI:+rAlj%\4ozd0L٪lD|ayʥ3g8r]bV4 *euXQh#HHppM9uJ "4!$6YF'^✚fFvI,*ny[@gIq.x_IHŽgo0KX0HJx:Rz )650/sٝݠ; $QA%_|/|gXL'd4zÿg/]M~&^T=kolKă M/bܭGcIɖMdץ~ocfEq_YNzfϵcIMtrC??i橵,_=LM[$$IovZy3o%-Gk>q,%7|G ]pc-Jz$fUxL]$)E 1 6z+GG5*t a 4*[_6[tryFbIʥ;h.$r gN8gkq^jg%sv^y0|b (Y0-*9h)QՃ1$M@#M9^Gvs]^cµU! xl.K!yS9 j~\2@EvF1 +A','#mb\=K}p ΟzA~KhĥbYǐ:NWpCi1UB"Zflnb"a,‚ (`<ſAImod,O\{'ė6Rs6ux:>|*rG2%EPu5hـwX}}l xDuog]Fa xp,^jj fH2XfdIB`nftt`ߓymeQa#Mci%KwiefwPKPrY(s1+g\e=fwobiq~7ypm,:M (ݖwV2pA>c 49xW㌾f4ڇ¸ I*Apk2TJ>8VDaGn!#r**~ tl$j!,~H0mL'Sz.xOSbEGՄ?6}܊^ٛh,ټk tgf/..f,>|XmQM{DDؼ4:Cʡ%4 Oo |0Skؒڑ& m_ K xWWì_*V[0X>`!^m?yջ|wW=w>#rq/<>U<^hp._O3ojiąw= l {kni&t׷m &=zM׼'E&ar DV2X$Un-=Eiq{v "9{*$*d=@ ɡfLȆT ɮ4w4C'ުe-5`̎S;(OT 2"PՂ],[dڶ4m۶m{mKozd;5guVTX5Vw߿GVfe}vz`]i6$-< ?Y{+`hm~rѓ(&fDOd[wOu;?0~c@Ы3Tj~_Io䤈i>zvFo^NYQ*H)I3#NSmU#dZ?0moN 4KuEe*?5ķBblEhVmH?n 9&S^f 0D"@pSgMrt&ו]h9Y͸DpN > 0!CvvSDob?*I -MU;cfë{>fpdbɫEa5-Le@E{QKc ,/0'' )`PBVq_gL=A "vA)G4Mq'= Bj!j&~Y+Tӈou]v Daֈћb{ig8Л^Ǝnxʭ-lpFhHۀV R*}^z:}LVIENp8ജzrN*QL&~H~}Μ^WX=04EYt, R5GT.!lRe ḣP MN1I*{ݨ\xh7ͣ!t1mkO)5b68*RBI [:T0Hc0;5SxmQ8A#`܊i$OY)h A#TR zanC@>kFf2j뎢undvndX^llI;`(@PȆӈH:Sx|%s&pkNNnˆXSoe@ma\v$. Q$6:p{Q^ؒQAbyjuuJscAuȆ >R 9"!xMd]w 4:wg8{n*AD]J-QܜW\ކ )gMiChPƲL 5H d7Zq "6P^:F4hG}> &< 8 "rfs`Z^o @ZICf $h @R%K+G',!<^h?&SdPKSi RtA ]m8Qpqť{~fUMD >V]`J==GPHC>OEdǏ\-z;~ `X8X0(EMŅeSڋb[Cw?wz[,d ?CE4qJJUV~o}))&FV A!z1/{ι'iJ)CSH:s`{p:@IJK#8rDg8HOO.&}B(<#&RiE\{]iҧw$T!(7.yBDLR槿?)  0A!Вƈé+& R/~^>L8@ gj=8C9ba0?wLEaM'/L538`+_3 4-@`xsi ̹1mth/,IdRw.?קh(|x9 V*Lκ H)333EG]D n&Νzۜ pˀiRk>2rs'mc!5!RCt%7.F q-] p(.?!S2ħѰEvqw lsO} !;1)3phyOkL yG*1fh AViN C9').Jr VpWfr_p&Ũۢda8n4#Gۥ,m!X:UVO>e5!0?y*_\gOVi(XГt>OlViJqE@+ ~{6O(v3'-'N1sb$`cS3w{V0p?x֋_& kDԢiFKezskz;" r,{p $M-.9KtU UZ)C`8(5uZ L({"jT{5Q !}鲍gW#g"zNHI^Q)gff>Z<{>8xzj-s$Fxh8xfm <9 <w Y{) GԀ0OBQM2UZKԚtq@- EZLʷ!o=m/4H)(Q)v QnD17;+K[S!H(Ϭ4^ZDTu7'=?EQ$9DbG LM<3Y**>% pؾVv2mgBic,j3U{9я!ˡ $@L*2:SJ D3ş@%HD%Yַ k@ ,\q: t;h_yyx PZ"/geY c !I m<ɗlolGAMͤju,UNw&f:2x0(TE>^vyK"/ q P-,Hm5k4Lbni)z)k(ҪAq72kNe}!D-NH!pNH+)ʼ Rqm1Љ!S 3;@a!a Mb+׀L8!!kBQdvg2f8)`N[^KxxmeB#Ps:sҳ] u .l4pM|v@QLjEi}zQJ˃pZMރc8VFBk KkYCbR=W=%E\j>⿥`@K`4³t"5))8&3@UeM\+/v%CU 烐n2D՜? V`:}|dqYlF.#j@XEIMuG> DY@~(J ӐCK"pzCGpNny)fm:/YhVu`+u_r{=A^\饭6 бp I(iLYg)MO~6_- 㨇?|]cNF8՟U^؉m'J!ݳȱ"aqAЄDE|1ha5tXp1ip-.@8_=3=o}˰Iڿ5R$lb\X7j I0}(T wwbc͍BV#^xM7HwvmQsE +FA$ZҤf@~IAEǀ[a(8~ƣw}G!sdY,10 p0 ѿD>.񸝏Rz]4Mt (Y?RijV\}'<+,1d?GeD#,J$mVH Ȍ; qTHPP, NAB HCT ˍl])D$SsʈI C|IkݔIr6PjiO=w5qj!}fAꐟ: N8]&^l+ XpTEP:WmFM0 >^RQE 4'9pc(\EIT~URB 7Ql<#B}_#yL9* @kȆNΦS0*|ܯ6w:ӳMZ h Ѣ+QJl-B@r5}-vR=Ԙ'.I9,ZA 8nPu>'#(IML--}h.-T U*=U#({޸9̍ñ]?䲫z|w7?Z=zWT V:*)2>reWC@Q*pL~q{QdJC<뿂ropMx㟈”r}c#8x%20xUQ6ϲ`4tn=){sWAmGaITakgB@zjq ,™4 ,y; r ¦R$oa?78DTq$)G#TLsW_W]}RnP"Y?Aq*ta,KȰNE1,fX4GJ i-@ n T$Mdv =@b5{0&Dj)<0:nTbi( Ka cJZZlmmP 5`"R_9=ሗC)CzfkX=0~':hфܔ6ǐ@gu@( MvvfSEt~.F(Jf[B|[n 9JQӡ @E.gqyRl1hǣ1mS%$($2RKtCp꘲>u"(˒H,A`;z yc( p׽ .ylmoZ]9wϟx|p0xtfg"ܟe{)QIpm5). 9Rp,j+ L'1שhZşQwF. Iq}@eL)tkb3?KxSƮ h@Sv!c>MQw#0έa|`I-7Fգ l6.b4:u@gCa,8K"4B ?PEuqBye%}U_lo/~[q_a uργ Kpݘ,T䪜Gh<r3> `F0}<6ĢuU@AJڗU00ruU@cܠԄI嘮,aO~[9R45g PO%w:2}ު?dT xǻpnʩS. +Н"vIA@ڜpe$>U }G"bx«wx:ƣ!f #=~2&]SVVs}Eӥe'077ۻZa{}B =(:]XokMBJSV#3 4퍔py-eF(0,tȽE)BHKuN0Z~dX!]e4sr EvV%Fӵ;X[=G of=|8} QmcxO|̾ކnFP>>! 4#,HXI b"Wh# ?@"X G0ɢ $ KKT jR& O ׇ1nM'< ޏ`aezN!FeՇ'Zh`M棐6UqƱ.5<{Ja&}#pDe pt`d S 'dhP\KM?ush *`@mY *c2\P pt*/G* >GOxm|{{yQ؜~aw׽X=SwIBH{~ۅߥ"aL9'B&qY ֓cBe66OFKcANa,Tиj,C.N?} *u 0N:z IJNt1Bztu7u_aP;T+'pCCHld5ј&aSx})\C?>#_ThoQކKV^pTCeA=5|Wg|6Þ##?nJAH y۾&XW%R9}]oz;'a`WWέ~_jHү/

    ]Bb Ɋ8vqvpH␁Ћbr=;%r_ G$C9 D8݊=]UR8;hhNg $ RiR" "HsNѢADCD>Gª В3%JWZ +܍rpMVRԃ3 t »v׷)5KYڈi! &5Iv'_C@6X#8F]iI/ae4@t/YmF[LR{{KrsQ1F|ĻF l!%<@ yJI)25`csWP RY+ {{;HNS @.&x0FfG)E,drD!!v@Ah^+"*4EO6`QG=puwii G.7pi hJp̤$ӳ,II=k>0i]d0RlXX18uB-$#EҁB >fgI9vbQȬ$sCnm1HOk61 cнf.aI%u ;{*(wG!eT_0/X[]`8+_ ofx)momEOGFcnJD٣hXq}17/xrڼ5~Ӟ.>/=ݷߦtsfs]l߱u+<Ј&*j;3S qnnu!(~bJw@Z]cS0''wcyi;Uh]8P$a-->R # N{1]فG.EQM-ky[{\T{ٗRkNىQkSޣT%U7#mVұMZAR8 sVslJHg𜧼IQBPt !B>Hzن2yU]^RJM8KҲk+h@ ~68K/(fI2 1n7 tM}R5E~Gwпi;c. %ƿ|F ЙEhL*sK#:ϙ_bw.Q*t D\TO` Sh&+,1= ~0SPHC)!G L& 8jJ{p ]ܕm\ap QljRV6ϐ> L'}Cx7)t dhq "P 4& p>B! )n77U5o: > gא􆀣*ѸxEܿ qAۭ7QaC,I#?WK0shږAnckg%<Vwh{Ls ׈Ҫ G)OGZ$9;$Z3]$Zp`2Tt&3Iq]2旰tzč,\4%@q}`7EBxPqLFsAnl!4ӱfF ".  % zhET )8 BV"E8"DB =.]"_pubx{ޘa\|җ[^kx4<%I ʲ[h1WW( n& 2)i؍\)Y&@՞}1Ul4}m9C(I͚z>$Ƀx~ pUo7wbp0љjgwca}ĔŇ5A`(4[T)MդE3u(^Pbp.IJi€KB:pp Fhɔ4lawRD#BŽ>8yYɞc\xx[DP)F5 :a 3%߄̧Q#Ê7=mw,%<b>Aj쇣dwNGfɓ |0@'|S(,Pz4*q4HN͒%.pٍݔlA $]p‡[ 'dxJ,uA ;P"Yvi5D" &C4B@(R:"5 `T P$CT|<9_>rs *8[/G#tS[g;܊x9Y&< fۖqZcJS^O:QH\!ve]~И>~EA\xQf#k3l">;7Kcq߽awcKBvQ֝w^ Ǡ93o34vw5˻K| 1^,k>'}sRHYgiRmYwεl?~R5 &uT߫2 =^S_AoEDQ.3JY8JRdG@gJ5%(:Tku:7:dxC@WUc/T%B@%/jMiɪNXzn"nRDIItfqE`1El)\ӡT^R`^-7eikH~i!AA(}T$b)DB0[@URpXI+Y`PJ7SBb*ZT0 @ u`&"M9?lLCE?AI@@4M1oAI#AuL %0=Gր/Y)Z)t )\})ˮŗm|t#`[psG) ؀ 7E9~"7aDF^orlL7iz.ӕ*Å{H .}oڤ*^&V}5Av9F[u=|\u3omAN K!zjJJIR/FJaLYjMDDp@{ 5eOZkTy %)@I2Q W&֭r{ 7@,u P͙.p0@E~/p%nwg[0P+-t,p{=HFJhLOj{_V}:N\p a&O9Aogf[> -4q80 !Ej<ˑS'] \ !o|uYо.4W;(>& l<}4^4*n4p'>LVlpCW*'d~Fkg;dGM @w;&5{Z~22A~I%`dRk01&Qpg\[)!m@VfO5?K =S?W%agURb&q<@`Iē=F?qi,م80'wvKp݈؀Q{E:D"d@4@C0n nK'hE͎&og{ϨqtJtIQQsw@L%{;LOq19e+ @ 6n:ln~ ,x 0E g{pÖ0,KwMx?O}_t:ЕW@` ~[C$r4P=شAv:5=} z|w F$\zy]09s Sh S,םeRM<4| Ii/J=L޴0e e$9 3mG-4\&j{g)B P )2c_դ.=]mjE"p).Є.XUh9QG LA>CĒ |~)h^ ^}o/3:'=~Z,R?ɇ؆!)UͿY)6#/ PG)h1R 4Oo`Cf8{# w8($[(&6Zx(j!J‰, i;}χ>tSn}w<@H)8_*<OxH] Ib#ap~%^5@69Q*ց0en5eYxD6K\/Mxf x+Ȭ; ^FQ'|waq+G=.~p4x1Kn?hUANDQ2%u_Y+~|8q \~*z[) d95_ֺ|8κ@aC+qH%K1&*8e5܇+/j1 8G?UqJ}[Bv#|䭯š.<8&SN 7ku#rit3S[=dI ot8nrF' xа wHnRܷr :[G&'vnD_}o(c"6Ψ@7@?&Y2!sJS ITs0 xu Blw/5 ~nV(pt'?vOKF1$k8 y!! 60tgV4lYks.+|J7$-u5M/V(d#/,cb-rB-{~2/b'O{[[u2/REQF=q *:< u\)Τ>J&NgqeEMZ@9(6 'P: Fw ' 5O:QA~:j`zf<׀fd %)<1O^AW\(; -d сYE[=3Pefm TNa@KP#xj6GKaBH, =v"H]bCdՀb0?rQ jSa̠t7z )}vϣ ||Q7)x\OMg0t ~GV70{ZPɖ@O}7z>Rg<:xY:`$bč<湟!aHZכL~axLvu,TqpmJч|FB+2):ۤ٠4ْN9 5V &M7#ܠ##dKxc㻗czfS3mkHo-=n~,4JV9(yO&Kp6z8y=XAwXY=M|L>րyի)K?)OMv(;BB=+kpgI<Iݩ;@jRuiK:BG1NctQ:,YB^?͇9yN8i!-<@sB)Vq"x;37 mJѣ)/IhժFmtl%h_b%v2ٲA3=]@! "LhĕXgEtpjpZd}$qpBCԃcf?h҆ۗ ڙ3:羜:?=<億sl&p'KߍVG8` tkݙN,ӑR(K FZh.37W\ (F#et0@c 3K1ȉK16,-)llXEocp c Sݝ"O~_0:! Q@GJ!u {##/-$,@YJ RwB"j51mfiܸ$=G<SG5=  :WC:y ?}xɬ0&GZP.aR€’-2->td0m{YSL<КMpJ9Qmv7'L (Hж{/o>h4 /q/<Xg&SC}^ +NCz}͋;IeQWNz+ 9|?!᜹@@# 3 go[NNp\|:j2#g2Bl~Q͍pX߬ $n$Z"0NF7}1KS !8dGL,-o BčMya4@}h ),ueQ0>íG8-NٳYccho.9hBS>z‘KBȘHda`hhOPz*U# b {Rl}ܡG@'SGjPj5I,lLtY@/Rq@4;R9z2veQJ+a:(v2 W5IlepYNyRZG\qy{SWAwype]sAM睅F/-,6jmF p f˒&x0 P%,%<Մ.PI)hQ LZ ƘF5HM%F1 4f;X $0 +T1!i#x!yGKUOf;\vumX'hM%j ]4]^(;&O_#^ h8(eu`,mИSW fwކ;Py++YUޏO/qp?f~pt}hx?ooؤY× !xOCiݕZ'NG !Bk3eci-8֚9<ϊ(+({XGLQBHe(W˲ص֎@xXJnM)]7l;L:ܷ8 ::[pLT/]p2)^s v{[Za>1f!&4P 5`c<(AQTh[,M?yOHqɿȢ$ (D $iD;Di4D#^OLgG",}h(*{i8r>5 fAѪ@kUB5T)м?iuV#& IR)\_|wx>BJDsdՃCeMFQ_CگwͯMo{^G2J}i _7si=zh&G쌹\i= ܓ_, <϶Ev6B!AYij8GI:N){2 r8w9;^½TZ\EsKoĥ3 G.LYkޮzXi#JSHgֈVvm7]BZZJAXJ!Ne&=,g IHyznjf#EY"ZF1r+ "]L02T ˖lO_xN7orƩ3T՚BDPRpFnrc:)71Y:Lmx.! Bt==/EjLJϠP ړE25qnC9;p|60Ϣa]⺨_ef?0@{c[Hq+b! .?t%Jhgwg~=Ja3(A9CdE>+ :ӽFܧx!>y-G|w*!Vw{;vJ%IRF+:pčLRɔIX)B-t#ƘGǣE~8d]J.:Ji)HzDwSȭ58YfK+HX|VT7A-^=*>K)&@RSb5 Д9R DV%֙]"$!:- U1V[۶m۶m۶m6U]WLI ι1믽o '=>(f$At]J{g+x%e몂 heidi?io25^p>-86r45&Ӝv']`[%`sW"31v}P&.eG f !q%cLbQ~R,lǡ\,1؎K0p-+x^0N $cBvcN39=Ľ$1'(+)+$IzxbmyN (-\@_rj]xGE4&S1s|H)Cވ[?mzRֈȘ\-inRrVrY29Ÿ ~EY/E4ୄ13L@|` XZ*[JŜ@w x>)ɹm^y-a<< k&-DqTu}CZ)Ib0d6Z=$cTJ%24 ,ajzcxH@(DH @|8Ry*D8,@hS2iv""nwE4Fu= F,L-h"s߾Nڲ ^Qib QtI&I( ^qm- ѝ$qhY6Bܼ:u|Ԅ~Il&'s3;2zf ԁt< |  0C@!D2 R&##m{N' IZꑍR% |*6oev\2b-U\my @7 sZz^Xq-טOtsgfs1]Z\:ڨ?.~sF#6gղOU EUB$#*7DIWdl= O"`Mʿ Ne~ g  GsZB塕jge;L1B)W,dpЖ BзtHRcRAiMW=CkITq=\C6dmu~@7i0wp)QIi撒$DqeT:Q_#>xy} 9E?0촤.Su errNsdIJYJimYX.Ku9/Ilzt&X4I@*- 4Xa ( R׬.R @6G}|8Ij8Akh:%0IJldftJu?&o|gz{+s#[s ҦMC}ʲ,P8RJKJ X6H!e)qG RrͫߐaĶRqDl PJ-MĸKWmXE,َyK"M90|Y>/+%y,'gXX\ JٖcJ)ȘT 8Rk9#x9ryuv}\ˡZ!qEaZ[J%abc; 8"F2׌}9>9>yãc?`ϯ fɹErrpI*3(ʄiP)!'4RI)em[*bR&R퐦4`[6Τ㱩qqt.EZքgpU:vt9ifu4rrNK?m  tΎB0N`;jG!CT-w]0y$ Фi])}3>X4ȅ&''!cH qP!8E#Ē2S-qy11IENDB`PrismLauncher-11.0.3/launcher/resources/backgrounds/kitteh-spooky.png0000644000175100017510000022510215224505336025422 0ustar runnerrunnerPNG  IHDRT֩F* IDATx}q#Ϣ(~޿vv.33ScګMIp2w xjcmZkj7+J%,ex3H>FdJ3KB9r$ 2Ǚǡd I*9|Xy)HNcTJRuGnK"2"UHQr|fzr[:2t厴#ܒ.YґK=v4-y>^~e8@U&eFe1}lHYrC:RLHUJIN!z(pr[n9%5)m$3>h/~VЅU!{dlEґ$2U KPXzu48E9,2#@יy>^ LV9$dC*y1Ef85r5uQΦN 9'3@>dx9GˀɆ]%dVJxVawt#7\SrXI9/W1 =y/*2/vYD<Y"CK[ny9*ߘ\nH?>bx=ÕF%n%3V0;3w!y>^p %JKJ<@ OǷ^(\ׇW>IdhQX$Wy>^piy|A+s=hP*zNnBxF˸*%1.Fa"w^/?<َ1 s~D`b(턋k7ç`xh.Y^(:sbC=rE&ߑɿ0#cpyՆkI>&ߒ?p) 1;czj@jeym+bi8[Zl٨ϫ̖frQpeJF77yOu|*{*rQCQZՐU#ܫ_i"cU)ݽJ,2M9-Qvl.<QQtcdDɧ>ЀEx=ZdJx/gT˦++Os(3uAۃaG~Տ V][GXdq~Y66-s<˜.3R 72 U`C>f9`0|,3WFxFjtQC1R'y]ӅЍKsʂih zѿEw/DbI#:(6dH8Ӂ^uFKYF&3(:Qhlj><˿?+O~{*z-@hL֨ke,\hi֖rlZiu ?!E&9b>O'&ppfƕal`IIVy9hXcsy iAT[#\Q`ް{4s!<1WnSxĢ$)*Suj,/Y^i 4*4WG. J(2 c8|RGhODf5[b3|fXao.̲$\\ q~vXVz5$OZGC'##Xt\sf;;opomtQ`7lȄ(9K7"'"9nhg_|P2OR%MHXk G1a l V6ٷ`A~fBxH.Yn5bcsFC%pg}@ 1?t~{' !^:s Wg݅ c8rtFtz#Fc402N7h6-@ =T vmv[#ڝ,0ƯyM25D~@2gN>w|FEEEE%"_~8 |@dNiw27wYY15+:(`aEb 4HCIK~L}}jѐm֝@D k 0$8e. 'NyyGEEEE%^~ x:n qs=-vP &i2siq{+7:PFt47pJPHn('xo&}5E?+{f:=*2Q~yy:J^<<׹^pYOxJVkI2=WM_;\x)݇LĖeYp] PSS< StNX:=͵k7bT2G8"yĩӽοTTTTT!W%!0acl{6"K::!t|ͥ+=JjC\ $.JcrqrvuZ!sSHbc[9ڼ})a 4T;;ԩӃ7*UTTT0QdPki/#0;i'mu'a}1C\<`&w%)l(n:L{P 3_E?5-S G(Xk`o4B`4Xq_ҒF\G{%0-H 1d84CT)wGn@ g.<|s  }D^%<B%`Bɥ`mK\\GoࡘGw8Kʶ>6N~ީ˅{/_}k+i`=23!i " O "YCsڈ.Q 7 c@0pVa@Sgάq\+|p_:߇^\-dRd@Ԁ#RIYЃ[M׽cpT> T1Z~sftDB`A_@#y:9ʼRp}MW (K."Su7  ~"𠂒lOD!,/GnÝ 9%o=`yiF1&򰄁`D @ =s)?OTTTTe_. p<8 t׽~ًoV| !P+EhS^ZA/f@y Av=Ls¬!tȑe}#G-2,9y|ClM!I0aEPa?ȗzO>DUTTTiY!uԁy+M/˺gw8y,ˌY/O_E` ˁ򞂛0[H)Ti," Xow=#;b4.QA0Ph#=ēzS ȸfY\^DR/- )>)/\$^f=@;mDn  F \qZV]ǚ,3k 7dV7-eeEX[cvv8h3Ep7 r/wy1J% Pp QQQQ1 _,!+^R1)e `-/3=)k^d#@YDɉWYf'B K‘ zT BN]R^{ȕk}!.s֢NsMx*?~om92BD\PlL˳}A0#! +Nyqz ҇ʩcC)_>`/>uwV!ހ0ӂvַ8=20k9W8}<`nZXt⸳ %Dk1)/o!q n.!n9}%s2 tQ.ǁV_{͔L\@A_*~{A3@!Εָ}TCJ02߷*mjCCfo]y鵌ɝQsSB !kpPӧn؉j;\J D4kqkXZVkRosq̍Wn$\&0;;{[b+' !QͲk]jb.\BP"xˇ28& 2\Xj呹 s\T0jAƘ ܴn =ַD"m ZX "Vt@&՗AV***y}$MMY/@3,}Lp@Fx L Ϝ<ǎ.25 TA8ox6Kw#Zٳ@"#!q7o H z/(7 ԑ$)iI,9\f_~ I^_ )@ Dl͉ [jl *bٕWGmwo)>T,p<40 d\_zc>p>$Z<{NMmD9EŤɐFƙSYby>ܸsvS'F0 %MI3h42=K-/`p)IfTL;>0" A B.>3^["D̞ߕ1zcThG3CEE%n'}@+@K'b` 8Ov82gVloFXYY**־Tsq$!sdYJ;^;;)7VP 25|@bXp%.SpE X _Ψ` TGl4^^CYz/)us****y}4CJ@\~͋@ۋ&6@޹\OYe~6Q \xr8N”ͭP}-! 4,鱽A;hPWoxzѬA)$-GPt\v( j EBFU&+UƩk'9ࠪ<~^>eD}*"ʤ\SՌRQQC?y`/ ckG6`=?yą@l^dq"$2XmWo[fmSia2P{4'ߑTYh33PA0ABb\fEמxI UUQ\,=zvm8<9yJsys.R *}sk~՗Q&l뼠%02GI\c^#URI w3]NWzs5~N`c.^Itp'I`ވ3'fuwԜ"_ 1H#H- !8EPǀC1uX,**gji>"2բh>;(M-}ԀGhM *IM#@ ȏ}m?9ҵ ?!hJ^Zד`~ˏ}dv9G^ig9'spvx7M PxǡՆ/9݂$%bN;aiLȮƯEA4b]qul!I(8u82D/<~>8x/2Tq8@i?6E/{ ,YUTuf="0~s9 )U>6} 1㇏ $[ܗ+>4|@#[\JyP嵍 ɜpUe+c{dG)΁S?TC NI3A&d]%w!Xl!:6YVq8-EܘBY\ꊹjbQ-Fg 8-}8."ր? U\?j>s⤨nXz>N{1~\jy>ts)Ci&D :[__xIK/)(n58-@kLZ";ϰ N?~yA1~x5EXʎ`Lb)}'  K Q+tXdYDUEq϶ir$<2WIiJ{PJUy9Q8 URmy)-Gz8MR愪! H^"'"$E$Acvu&׃H3+v_VUe=\x qׁ̋?ElA ԁvȔ₲`L\@ ݁O}`0 ,*e iZBTP1 >@'8#Ư~9P<pXDQ4>'GS ʑ$c&>B|PFDPPT#cy+aLHsHl`"*^\o?W?|?v~ֺj^Z3-H l4p,.#/,j#Q!A! 2MeñO)P/)SR ňa-^b*eET!En6z M`8 pJiDDH޹ v)Yʽ`-Vԋʇcxy4 0y໪>֫樨0 )A3 5rٔbo7|)CaXZ}a}c "2&&/"KZ >< px X?:}`ËqEx&fW2q^B XW%h c ayiy1&"AaLLE-BW85%DM'uK"zZh†ORsFi3 G% kaaVxDO7E5=n  ,!rWRYD0>sBvp`. C&² 'X+Gg:OCRđq .̔!0?"$h 9vg?Ϸxtuԩf.ioR U9,iXsǁ<^D,O**<,{U7 9.^|%7{( ${k$,! &kPޕ$ ?""G|Ф=D՗|" C_sʍ PQh9u:3"!sqyO)(C O X03e\0eݞct30D@LQYуO YYnpy53 16Hee1SWx"⣠i9VE6RQQ`ג^֯k'DX,#Q&ܿ***yռM MU' A+:p dkMDH!݈0"^B $2[ʨxA!(QB (ךX.CK\Ʈ$Qe)-"|nv7=5uHI/SSB"/NQ:=3 KMlbLc)eyШel<UBADSjÝMeeEh&C:v[ئىS9s|4)Ӛ$RNP E{Yq;U^T$ iXd\hT9* oo( 2ax9'}yiU~?^ PFPdFf6Q]j6Y!r/ E \s`=aDTXE6wY'X1^Z&1%ʍ;&vOesǝ.]ڝ(f^V,9<T'"TTT/J[~ mߠ D n.0% LtRASd,|"2^JeqD7.QDA%dQ@uP֎)R~מj ~q APqT[ )(m(5HN{T$%BF.s[W;+BVL lkS.K ܸ`]Ymg:ݔ5mXcpe::뾻r.O.G Z?i ,Ʒ!L G(T'dQ㥽88_tEFK1 a)(˕!D$~SGP#e #avZ6F!a¥m~Ɛ%,>Ӣ)I17@݈,fgo%*'"FL?j-ZWE%~ 0[kxW(HxƊfKH >_ɍrG$M@龗7)Nʵ/#%?b% AUPEE?*Ш" 26۬Ŏ{{pF'XL=}81 297t<*A`7sA߈p{qnޭnso***yem.' i`F4M1( .3PE}LDcc\9%Ո%奨("cb2 b,*XC@1Brd)k7ZK؆(&O!I2z W+ʌ^Xzb-xtY Fax)+EEE%/lv{w_t{,ű C)V<&Tn,$B$ɔP{򻨿 $!6c2kR R.lb"Pr I;]pjBuQ$ld/$ҴX+(KQE,0v# sjJ^v8(?'unauo;u/%izr4[0F@A?=ŏ f1?N~I*~ 0%Ee2TR1-|z ~~.\DP% I^~*ZW6E4 CiFa0QZn;xA50 .AJ`[._rTTTz0)?lmn:1i4q""{$H*o»G/r~"ʿ$Ɉs'q!3!(TޕYSTJxφBHR!SCyt7 0)×Ҡ#i1 (^ 5cmW5b5q !/Z*J(eRo%ai1:D5ubczLR2a!HM>J#R&BǽS6ѻώ^\!͠;\mF#1I|c ۋ|߳^2***yU<10>QeA,V< cP|+/$?`@wu 5'Bߋ?g2;" y Hm'1Ο4HS k ! `1TJ1VB\ra!Y+JE3)΁*凬'}#Zv726?,GF'ˆ~/B8 I L j6x#oYc޴$모U!49 "h"IJƊkK3,!K2!>E  Y0"daq)w 獉58~S95iwhw;Iƽ""c iٖ",DZkw Xkƕ3J^U0fy,2֖/K@)_bKKKp R:3|F1Z .za pgS?P\ނ `*ɨɄ,/>d>Sz}.]rM݃117UUٖ_*泭ԈZkqARQQɫŠȔ  Li<{A9̻bp^QB"8p `RgZ> #0X3y} ӪÉû'%+ܸ!o_Xt)N̶lP -D;de"϶<8ߘiy+kWW-E cx\w3)^X!BX֗K*]4 .LQ& 3<бe\¬&avvv֐c6I*:D|ca<vBXss+b̯ k7o8***yU<`䘜 @ϦԜ/2bbPu80jQ&lD(0j0+ FMHJ[骾z*>ʻo>:YN+k ܌`ffVix ʿBF%ScUrBVoVu@8 {n߹TTTP@S@M^^^Z8v/F 'CC&2 xqQq9iUy@P( k60hօ|J*Cػph^7l ac^|EO?ŕ+WvZ}oqes{3aUaAD@@ynim˩R!D EC(lQD|Tx<4^bΑA0l6ػ8Cuvַ!ٙˆNL*(W (ʄQv3/_lc;88>U 4D$R1t7/X1@萈b0ֲҚm c;7 ;=AEu/-J#Aq@ZЩh$)(4Z\FK dea.`yKRԥDZ-C+ҼpLcn;%e%i:|T!IUG=LD)a)9|Ԛ "B^_ܹsܸqw26 #k!˔n_8 El8<LMM޽{ǣvجi6V(w*qUTeÊ#"O'EXRe?`=!A!zN-û'NV2'~/r鐲ƛ850#,%ᅳ3={E6vgpK]~5!ͼ$vqܼ+__(W^y.?oqH kLKt^W ~Z{%;W.+UUap  :p4sbf&2BP֚" ;AZgZ-K{ Q467V2 * ( Rʼ$UV7m)N&^YrT^\8~A;5G{B);nPʶ=JcY\6~{#Ggg,!Nt]]J򹻻[uu߸ #32w.K.a:c4~[nAa yiTTU gΜQE!nu[u6wڥ?{XbkbA ¾Y eAxA$88tJ18T6Y" H$`;䭽'ueahFFו6#@Zԧ>ŝwީRt.E0C"r] .T^ !pcC1I gKQq|KoXbkE$ e#aLsQEB<WkÙ~ji<=BooJ,[qՖn֮(* [@Yi>E* K#dY;.1<ńd pE+VPZ%ȥ XqSި8Ψ]9~⠅ 響8pۊ+T˄ J:` 5Fٔaf *h8! 804 O{TV"X9TđӒ9>ʄckucc81]-[($wl_ٟtgu\ (u[-κ34}[ba\.0(峌fy0dYCf4,5i H{ݴǖfz|/귎싾k(c,̹Ǔ`,5_JA.{8ٶm@KK ֫GʁcLz f+v: )S Xbxf.PVs 9`ܱ@%lx\:(L(pe5+U5.<7yK߬m k\ ,Xr'6m'kES*.%N:\/-mXׇmLBi@q9xZgm々VXb-{Qf+v5P $vY`3[x\d?oq/}݇SpRZ3NX K~F.Ky-.UBMNF{oyVO$ ڌ1aayli 3-gZbdB(eh- 96Ī#1UT%n`Æ *˲C_#ۿۼ(Jggg`(8략k/tdžVLbŊC@-`U،lflG5+3d"H2ƀ9V7\Tp!ɳBP¿PY\֒%K'K|뭷s.[vq@ !jd԰ubXƘgc~?-)RX1bhE}ZU\fRq1Rį)gM}eNW8Y^G+f_Q,TNBW??\ƞ֯_ϢE3gҚyP~L\%G*?sj׮]-ȺׁLbŊcp@E议BUϭ25+<7ci:z1$^ +rݷnjwx]R~J.jhN5sLA=s'_Ѥ \j<ϓ{2mݖرþBn]*V^1<, n :`.PY5^WL{0~zMM_0 `F9I4Gv*''UUU@/T.HG UnH9CLӦ ZĞ6NޅbŊ ! /.󁢨O`<ݝ O N1)e-u\{.$q )t )**{.fI&Phx29]=p^Ü@vt]W.R0NOw7t_Ċ+$}`&P\:r pUg(+AQI%iV,顽3hʡ@pIS"EmcAq\\O1C#ʀ %8i\>ax\Zo477gJwv օ\4uq #\R'C;l\3Y3YExR/!52Lg :Id7\a:eX7߄ɦMQz饗8|0HRK ^l)lQ%ゕn+ħSK[P`⶗h8D"'[6fyy140{yiZ(Wۼy3S4Lf\^^. +NlYb@! I s@X1rCp=V}-ݖ!K%K$Xrf/30ơt:EoW3xknmb"`y.)V#R.H9[o95(/dK7ɞ={P13Q`$s= ` ^rM &((xuėwJKooȵ߀U_W/- u4amW_CP&)~' %Zx s_;+BhImX<Z@K+4+_^0KijGtf2id8ٖ 0BpzavBy,*.+YϢaʣA7oWVrdTE^^ֻzD?ߜl>j#[)(-dm~%zZ0W/)%8*$yQC#<sm[Fxi!@rmH|ʂ,*J#d~(Q__ ہ3ہQb&UaN`x7M20xd \fUSXh%fPUYȜm4.?ge_dR %vYib- h*)sѧJ;T0…% VFP),G8($ o]u||߷ҥK nW'p,p}v <`Bo~F*;j{& 'H =R1Pcc*,(;^!Ul/+\#涀,VS\Kwg'M0* hnMR AjAzކV,LWy90Iz4֬Yr+THGQU1sUы~:HC wK۩M!+W’|^f2ZbqVa'n!x FlD᭿}gn"Ba>}m3AOIBh*O?'ܪN龏"DnZ6.^ϕW^yLPW\+{1v#0U<+;Y{yJ'O|~: `va21 \&=j:m:PWwhڌ鲲:z-OF ~E\0NW鼩(%&H ҁ„]ʂU+G`b8Gy.sܲ.wa) 3tV>5|ǿUrrǵ`+ĊÆiJ+350LxQ텶hfP1jPeEUw!&(l7t!Β̞OGy7nDMO(]Zz.{705veB+*sTXY]릛nRᅜs9FMJL4We(&}\1:0Jkw\:Q|Ok7TWT3qymq % s}E]DPiӦjFݕn=J²s=e>&=EY+bg $lm+1$|ƺ[}3|H.@evbEc1_աVhqrL0/@BZ\r-_^w#IIqؖu 9C* mW6 -HA<ۢ@=u5@PsF Ieٙ?8צ _aI!s`SgO)mX{۶yWٍ7`{ꗿN}g^sg!7ʗ18eh)!<b*s4Y1r2Am֋nA3>$SE6'u.acyMNf0++ _xfCp`Ճ9>chaŢaD25)ù s-Wq&$w($pcv[|mVPR`G͙my0uV/r_ޟa5KWΪڎ3 l-mR5'\P%a޸LBy.bkh]{y=י{l;M'|dUeaqTkwg7pk4e;zWgK>_XW1_a7r")[T 38Zc b}K7G}@pd@SDʎ8,zĈa% WXBචTǘ)~UdEL=uI8h.^p;wI"u\ ̋t.uRv99lcޫ;Hy-Z1:p`; YNQꫯe]f_e5EŕW^pc=+sdbc{.(e‘.Og*>jڣ[ 6suok5Zu8F*Iv+K/5~abxB222be L` %jPS8tи*#B )r3s88*I9|LAT6(c )]Y_: Z/`jԑ5I!;Ńy]Q = ˝@7 aGzVH_sqadHNM;w\]qTUUUX4j- &c(1 &$FZ2)5ku#wgW\{TzO @ٔOX6v[`UPYnp!,F滊`=ʪ[煄#3|t6nOaG%K$>5h`+k jUDs*wF"G|,/N83vPFZ#mJ.W"LkN9^338P"VR#W}EƶXs:Z1 !O77tRVy7!oB},={68;( k y0_Tj0ԑ>' 52rw D*XW|c>0 oj"Ԏe:kݖj*HFt %}QX1gkҽY䘩&8Px;[}QR*ppn^xuwR .$"X?=iRF -G\kh %H ՝C"R 씣NC7@һM-Q`~Ԍ75XIR+fvZ>Q}Wg^7 @dai^ u} ) XP(9us|^~e[|yt$v\(Q 0 ?엿%Y`jOSV9u>,vNJed+ADHc >9r!VHݮzw|낭;t`DهGYKMgo7熭Y U Mbz`R-MNRa!LXd%8`7|3 y>Xp݂mp40$-pl޼yL7M{#S5Ш2­ 1pxWձJn =:+o +z%&u +HX.T =FvjcrT4 . uzB R7 A%lq7@qr˦kƩeۭkӋ?8  4Z ! > kRg6J|* ⮆;W=QedaFxAl(Y5}<Z`Ը=35gKd4 ynxJܪE|tE >FDmP0 W724)Ȱޠ>JuS;f6OXU="f")\6]w$/3U,X&EX*cq̙ighiR# @ ǷP(#05H g?V=.K,s\b3Poqmul4V yCVl-.֕ȲNNШмsP&W]uL0:XVkmҳPj}i7k>CZu kC5(о,@ʰPuqTEG Ͻ]tb,TnY,,b D~g(' VJ) CiXB=FZ>š '&3֚ў!<@V pqq(kGޟxcM뮻Bp=`? YE]&ACX%T=+w$`ZbX"VUAlE ? :Vx;a-2,12 ;@2r vu tRV ^(x ;|tO{4L Q)"#WS܆9@ow9Hj:ZctMýYRa;g=KQYأDϧkit;q{zuXACnd=/XUU8DCW`Ŕ*`` X֣VJp YyC{&}ʆ1DD}ط Ioiaau7Q(`\<. Mg }2-dѩ&E,`,`ǿd:_];+d=m}M?"H7,',v+D!Jʔ)tag\@ZNX~/fO$k^Ӝ 2Te!\ejԺjM)}<`>SWTsL0(ƶeH]V,o. QʲeJ`Dl8s95bޝƚ`.`Z:S&˹@*t(#aG (cŧfK;.pw*F=*0 Q=¾y;( H@.ʟ6 U'{ a,2!cG|2:oP8xǸ. > \NA|ί|1\fyG)`!A '7g@5 Fٴ* %N\5b|<Úuąc .hٓnfsJ`8~b<-S{9Ú[&%sty+G@PǓ KwO9n/RP\R{V $ !w;Kűb\2p!lHҥ y|ꩧw7m`Ga;j/0 `LǤLS,W b,nI bn 8"sK5>n@ޛ.őSWzgZ8ؔݤ=Qsw/w81 L}׋rcu_.~g2Liݪ1eDE;62&ؖss+#0O3]^/. llmҗ#oX {'jt@g2w ݙmH,#^R&pH p<))&n"So@%|%{sX됻Ӹ\Gj&Ŏ93e04(C0܂)R)#L6V!=.&%N(V 7X xe&582OIߧ q>~`]ҽO#r ƾo}"{}kE?nZYˤ<IҚy_k}*J1L ܂.bº8~I-Y_|$NF;[t %K" Hݡ8CJ˰p41JIB*sP5>Jngkwrl^+z>F m0ltCӳ>F{q $mO8/%gD P9 9L\~WEI]'R SPĹQ<&(SܽсŌk'&2*LxyvTj`Yp:;T| nۆV0.-U1D̞61 B.V5ì@.2'j_tEW6f~ T 3o)E{n6{ws=pqw $p*(Eb\4Mv}A(tGS_J/@D(\vy֐X$͛x15cBp砘SAbs2/B@{K"Y*)5g$I~4R5si ?h J{ܳ:$ T7P[ N"D6Z1YGΌn-#`}!FoeMk@. sF.B>H@sQp(?d_@ՙRXلn2 Z-2P틃awN@rnZ¥E./N HRfH$@-G](tN@-MZ`.f[xZ,H L+Rc=3*ϙ#-)Z)C]!. tX˵3 )g?k/b=%M7݄D }9L?ըi\K/'.Iu-rA FXc.Qc"$Rq)OJG֣=WB)ww"n|`'`ygHA, !@-My@:j$c!\:|o%5qd+rz3-Hh3!̱>٤w`I]2u^2_0͟P/*L}P Cvғ<%?hم=qzW]RV x twFk&)n pDMqH'+ڬ]q 2 Fe"7B w-者=}lG~f:Wqw +ushN~efqUШͿLun9 ucD|_fY@I4֩V`GZ*֞߸13 q vUz>PauN(~F!d}K}jPp"c!@THg)/#$ˈNb^5\ԇk/)R:d0P}mK1Lv,~=[[o_:\''%2i"͜b1,YX3ei}Q5S@O704ڢpXV]N4gET~oMCccysk 9*E:^JQ/)$d"Lg5D(aX`G %:: ffkұ.\\( d p]ve12>X'Wn2Nή,MZ}ѝ3Bwk$MY3+.l~0mk~"popd^Y-Cr(Gh$l[[br[͝'bFbZ6_K2޳@-R9hPX`U%,2N`V9#n!W*X5/'0i-[E58+nv}qGazVƿh110dܹ:x*^y{qP -j0`_iТxM8F/Jb(xzDŽv~;q ? hL,@FF$c8qh\/rW/@m2*,Zc^G}uIOI$Yrs~ҁ&%cZb9K$kjj7\hfG9%Ɠ{d=Nu`q]"5PVwEL < Iw LC=0,(Khy=Vt3IZ>֭Y5^ l*@<\vY W5֨{#H^U y *`h}If,&[#X"PŸVʍQlA-b_kV'cdo5&M{<%4V,a tIvOE \Rjė l0/=aT~v^tc讽FdV 5]d` 3fřSpI7 f0t~- LIh-- W V؃xa826J>16t|\~:ùk:C[52zXA+$wFxިȌ@s7F8ƂGNU%sLBc3g\8hHSpPc녪xRX}U]Npp Tz<^f30P`( Ĉj= Jp;\ Wʰ ?+m`<U8t_TIHJe}c1E0cLبQ &߉,McL7"l86E8a@ֹts8򁓓k-B$DCC! NLLtL9/M8Oon@aic32X\._@*{7&,!Ӻ oį-xĆrUU^kWbfg&M269b 8Pp:999N_={9yq쌍3rfe@m6JCM!999 zP.h( AhKNVrtv.%RPƥt{챜*hhs6$'<-t.''p(ma"LwᅀPt7̃-|0|OnQIjzA {ɼNۡKؾ}{c!A70bNJ߁/ 711W : GcJ~jgbɼTy-Cʩ.f'|OU݀!Qu9990&@+ȗDk`,xjڴiAhĠd8qɼZj]dZj#jKc6ֺWjd|A999Ep'9v ! 0g :9wAɼZӺ ?~|އq2fRs7뮮rrr:̓?`$IX\)hۆ2)=1 IV^Pz9v5'sk-OQ… Ҩ5prrr$I .ƱZRpCXN3Vmuɩ $<ֽFatv œ*yBեxgc0ZOZ`DCsk_ .4-Zd$C-J[IM.1''''O샍p| H+sQ(rt䉌kԩ~ڠHjk}t.!rCdybNNNOq]NmŋM^DN|UgZš5 PΫbeb7<ڝyx >AbZP^(xoωwv1qV~~&}ɼEߙlʝ"$tY3U0579Tnx~s&.:Wڵ8tZgq'oo?>gWy6f"?vu2Ϻ7>/~Z[.)@7 '7X P^; 8|2`gfn9|8sɸNCl6K[jK[*?aӿYeʼM{99(7< 3a9]:;wS`Gl } ~:QVFfj-R颼GeYy! |jGWD$Y xȟMU^?ք/g[fĜq`2,xH MT׳۞={)Y9HYmv>mt> rx ;q@Nnkݺuߴ'xk]tI֌T63-coazMqb_8y& 4ŋkS|D￯69haNC"@>rf9!&%?33qǜ:.:@D6|θ^#nڋ='S4^~{O; -]5| !b\Bk\jղ?:dty>}6fl`a \aμVL1,,99Y-3gz&'W\qdꂀ..MU0lww=d{*6l Wqf}!*>p ҕ dM*]Aѣַъ+nڗچ&Jy-=*%KUtN.B:W?`NNgj*Qtx{GAp I?krxɸ.~E^Zaiw=7J`  P"ɸ"0T^vS;vsͷd`VaO+4Ur))BkQ ;3%@׃*3"EGn;v0iΝ6j(-OQ[˗+qZ׮́PQmm0*h ll=8~M7ik]}Պ4999eU;w֦cݔI luq-P<;CS2kx}I?L|᪴h>vU׌uZFS#) `d8FxbUSNK ԱcqWH I{4L}Ǻ+Z5lD3[AzO& |tonݺ20$ L_/U;;hG73/n9gYK[\P }ݧ(p~u:X-ltS}Vui}KJZ{LPa!)0 A"aϫ Efi_(0"50̚5dlwCYmr^|~MlBhѢڋYnTۋefӅYQfBM"ðv!Loa}YSIR۾=7TZڷ%=nRi-&B }݃oLNVhanom۶t'x:q2ڏ&<]1EnX3-xO +!HQʗsr7ol&LGynvec$ JP'Dt3tVƃvʒqU~=Sh|˷f5{cχ X]mAG-RCHROa)S2UL嘲ю/4smi LooԨp23Nt~FH @%BꇗCymH{ѴAZ-ɓ'kWE)YqS0@a ikKE}]g~nq s Z X '=Me\@v1[u#}HR%z'g} ˷HUVMt64#MT%^ZqxMLI\\ɔTUi}JRMB̞Ջ}||EӣPkfTR_G պF35(r8 R X'Fv $1q]qFBF*Dw\dwtv?c]Z]rg7#`*t;[,wNSq1ςhg%ZTVO ªҲ ~Ɉ+?6"kZZG7 i9]aWzޚI=sUi2Ue hb6 kSKy.ӨдҺۘu $#S;4sV~i31=Ujj-Y*`7l%|bx#婊kb\n?Wp-\g:CѵV:<*\Ǖ+aML\\]TL&&ɸNoC2l.D*VS JmXh?Gt❞%ppa v`3U0NO5|[͚5Mk\۷oQvZFYeZQmٲESZZ~7UMUf[ [`{ f1ATMp5J)Lm7n9%&%ۚL&v4Gk8HQY}\톇ߔaeFU_T^a{9%N EꊤiI6$2doRBZNa<HO>K4"-$ְqXV* Mhh;ZLUl^Զ$OlF i# j=혠"E0 B>8* C`{Re^;m<gv>{}tOVceZ0kR`VgD=3Re%Rť Uc .ٌ#vf*R<5$UwZ4 ,r*_Hcizw(|ELy..oh#^K; 9⩋ O,ٵlRdf*=i E\-Xڵ4i_T楔ٳM{D=cɰ4H)IwKTmpdJLKƤSE"Eɰ2ˮ+rnc%[k++p`!  պ7Bk+#1b~ pb@>OnlTkaT]q?Є:eJzw##zh6թ C{dbj38 6ZX (_ 0j.RSFTjZ? ^9)-meRItg^T!F9*D/*v}݄<ʴIg0D(xrE]m0̙ġ+#qn2:ABm+2o JUȫĤ[fg^B:p! Ꝅ/4gd˖-kOT%'~(رE0 B}.͢"h|za͍Rޯ:6x7)֠sl5 Ufhdj ʬ8dZ/(Ur1V&t\ *5W T ++N>X)$&KLԢz]pIſmOReJIkXYI ʔLG&(j)zFVZ67cdj8>ךuz{ZGɐv!]UO3mrMd=]v -tRuVLjz{%U|qd9'9`NaL`%,b`Y\q͊9- #Q0+98C2ѵUǠX1Nqu:RG\.Z ~j}|'4ip"U yRala?%=Z;kz z@oyHuu:|ɍ=pQR#ԩP #HMx4AxDJiPtEíyŢ3CF ҄C"~ _TS3n jDѼ!LU. xgHF{>Oz} b{T ެĽuEݫjj&:B  V' &[MWPDa7ih+Rl ?%Fܐ!ChATĊ *}<͵WVĔ ҂sc ư$󛍩m9= y8 I6M~<An<) Vɘ gWb%E;.];OdQ !-]2`&!mpƁ jXŔ kkJCqDӄ_t4b7M5NW@bLaRǎI揂 4&H׮]믿Fie~:6?Ìz;XR0sPF(+eIR<!u6Dƴd8.k$VyxOqi-kKE/W1S4")9hZqu1- 5mM}Pzo&X3 =h]}Ֆg*+opBNni>$M{|TI!=AQ{\(Tx!0ݔ)Cj`5A\iCg0Y ~8D)}/Qu;Wk^ _iP=e}_Crؘ3IMфvk3B5-f3w팇 B݆:x.!0YTF4"dKH$,Q'ρx8Mx4ƪe ,Gd?*Fd#/˝^clp;䕇śA3R!B09Q& !T ?!C(פ|2B}HBfAl0rYjk,N~nec\L BX TâǙЙMt[B^jsC Q^j[!? 杩JiAT֡CT$\I%jVtOTy2f.QI٦>l0aV!7gnB(y~zs 㚄 Sހ=nfyGqQӇq,_}DũA+3E lݫwOA`џ>5g;7b6 _;Zԭjl… I'BB,1qCnMTA ,icmW_kjiNÅX,~+Ԭ̅b}HϏn}ZZC\Z>M< JȊ-$ax%(1NE&Dlk (~qM8Bm#VȫYU<:qT4'-52'B4UVvS U|PQSUX rUW :k *IMH>`7m[c!  -Lz?5AL0Bp(m!]HҗLIQa\HkPl\盢$-hC]deꌯ 5ГքA^՚F,w6+v!_ w_r [!(>z-wUA]ܣGY`%#.R;yAX(.֤4"Ocƌ!BTVVYq 5yǦc@2MA~7(=KC}^DF] =>āhz;7営;ˈڍ$u0 QA^2w@cA^|wE+Z7q@BOGn}Xs*(F}S<0*]B}|IY>GczpK}!}+܄7jRwHaZn|#5烸8M0X1 q <%B2(<# 㙺kAYs8# QNP:f)GO`VcEy[ny eWbLWj~ a|;QUir{9Kʹcm TᕺpR{_.K'M!B̘bjZnZ# R\q\#XҀ-\>DᠴPLFPJa#QwԺ hR%=}AfhQ=ŸJmc+NT4UǨ`>@ld DbsE'wb0Kŋ,dxq;][]%nY}҅,ӿ)$H -+>(̶dm5̟Aڍ?ėh;7rIߦk3s@%җQ]IWB;>A9AP&t#FJטDm\M9+ﻕ*)Z)n~LR~jg4>D0rxK#V:|&S3P_Կ*Mx־rYKm shu:H/Ќq֬Yd0z?jZ7w*R!"* -:9Tl,ee %gbI@.6"ml&!ZW,x g0y"ǵsM>]oQ\C'ao|(^UܩZ?T@4]3;^I) YEbS` [n=駟^l2fb:\DJ32ρ,Yh)zYa {-?s ua.u*ԇ:TD롁_`.M^EQ,qySPܠJ,F)pnT,јP('n=KhχLyICV_㼶R3Hf)wh`F]qXugP?[dPb䅱BwM~ǨN~? J UtEܪU]XX( 3gΔaÆ je%Bcɦ()6ԭ{h M'W $Dt_ހ!n(%ρ4P5܈RgF 5KAqS7k:5E=d50 5Ĺσd}3djTČd.P']ȵAEQr<GP_62zl_e+r.CZʎ)Mν)Dqb#)b9c ITSO=Ťzҥ [IBņZ7RVq3im-k0CAq5sb20  ${EYսxlVrY,F3f5Kjf#! '׭U٫I #Dfnr\4 ypcQD/[a}\q=)ԭ뮻V~cǎklK0;X:0+@fH9gsaƣְœt 9 oy ) yT@P-ux#-/da'mfyjk%iRmǵ(mQ A{/(TW)NVV!jYNTH )$Wm3 ϝ:uA$!gԆMˆG,>u38~s KHR0r5BjՓ!L!F^ ȋ;'%7~nFlG]oѷSwK3%0 8zUD%HzzVbqPgc$v˭*ӦM5u::daƊB WUaN?_R'pm`6D$S4W}_,jeϥ#H;cBa٬GHwgF+X⑆+DpD;2g(Q5U]uN۷!0P{y|61f 5A 3V*cAԾ=\Ff%Df-H!r&a퍟TM1kFZyaހ<4'G򺟾'#rE+DkWKQQtRcG a.Cj5vWc 3 b@uq\*|( reO%j 򢯌d~Mڐُ{^JZwy=7b'PM#%JS+y睥s2i$H#ʜٯ ]~P^&PJI|1SI5aw {N44b *%#h+AF`MB\o!toڑ'qSMŸN:I^}FJ ƨaHm$w6Fs-/3uuA4FkJ!sa@əᾐ-$uޒ %|\9Au0rC!TVLṖSAQvU]qln$eMn0 )H 0G݇qíW@LrG=~~+)JDJ+a}kE6?VivFA^E-p} "DkGX)Htm]~m6TbOFNdkIAd, fz6y,Sߧl8'CX|@A|fQW۞/ZyP =!B4h7>~7U!0P^'2CL CrHF$Fy"& #Okf`# >hɜEBȃ>(li9(fN㶊K\*7>+F*h=lnq^R]!; b)8l`.Fu:l;=jn6Rm OT/c{;yU2[)=7:W2t{_Ss믊 +Tk,b\MVxR,F̷s-l i[Myֻwo~m{#W 1Dc2|~LQ3С QFB2i栳.COk8LҘ\نz 9%ByS6IDG:-R<)/]d% 6ETUC R's E *ז[tv 6򩞧S@m[4ue…>6m6Wq_cmdbg͊Oc҈|_t 'LYÕ L ! 1@dXIԇ@Pȍ2ggJdχՒ(~w93q)&!B4 G(u )V@`Nްݺur n LH21n8 ^o*-Jr yԯH%mv&I]k[uGԌXNBYIq8 cBF!Blqg; vX1[ZZ*!>5{l:T5&0¡NF  'SSs2e4d924bkYr,ؖOnR}yU`\ɯ&tLv5e.Xrw}C*1B?@Jua,W j #(C oV2IcJ7'Hrw.5UX>6XgO}UGPisKFD&DyUZ dP@Hl-Ph(*;.d a 8W[M TQT!jl`xoW,!B;_);WJ}ݗ?:C1ur9Ǣ:Ȃ4 $_/H2a2eoʘA37鵾CR(;L|̢54 X-c]\+iTԨڤ:Q5^oYˡmXI,!B8S1P񓫃-׃2~J_u˓?/* qƦlP]Lak!.uE$(s79d0+?X 2r P`AڀP5w}unnNMWM2hv<^!m+b2AUBK_uD , ל- @ћ?0c?$z4Ҳn~NMwK#6f nfF^3Ł6D.!8 E~ZX5(H'| k!uR_]iǹ)sCΈoC!uռ $(FHԚNm蔍*kWΝ;[la3jb;E$Bcq_9^;.r F/_69'EJ$@d> ҊZ(}ʥ|m mmVV#LyƭHD!6R\FKiVva9䓥gϞ2m4!D7ZRt6pbVF\Pp]8;U?~uQuJ7tD"DQLPd+*ТE aR'(;V%Dh,!&%Tzmb"6={#{a3m)r 9;\Q"Dy1j4ҋʄ>.Lq-b)oMK mHfb U AFχ qQKd:? 6F WIIWD۾=F֢J3S/!Bl8Lb~ry)* Es5%ӻw:1=*CH12[ ^Ӆ~Jj~ ی!:F\`HOF^M\4 F^8ns>бlwM7ݔyG+BQ*Q7KY6Xol1egwX&Zc ÇQMw~VKvŔW,wH^Ur-62P4 \cېR"Dbc)7~Saf!rp KKK1EqI9;H # 3cniL麄 MѼ?pxOMS^*q)p&6(y+$=Q0,՚PiւmڴaUo-XKi6)3T2Hvwqc &ރ)@[U)k=Q|]9ώWgUݥm۶l+1ߊ!BuQXY[%~؈DVR$]C "3͟_Վ߶RQ% !!qA<'EU8_AZ|' 6`) )]r26R\vsE!qmbE7 KFb61،e#+zJŜ{Ȱ}{EmqO߁jش]m$fk6K[EEErS8xU\"D@b~C7+^kZ²mFѣ|_PAe2Gu.R y<]ajR| ?y} 熺j̮VT<m9c;rm0x)4-9n D,LǼ9#/"TgocXQPA:? QargXQ@!qџ#a,v 1k~>Yq(t*PTDW_F1c+9Ä:G]"D&3(5LiSjAA4`{oyۍ'jhqH br^`eGQw|b'`uq<q!.i}]6M#*37d}mݖR ܇[!Bhߩ*lu! oBkFa):KAj5tex-cPT6kҢvICR<7MD6J`SEցZB1Ph >#:dd3u5\#."D ᮎS_<=c^~e䏳0iXT~)L?&o$l΅x*^?8eEHԚy\4 OtWkŶkOgSb]bذar '`D;!Bdh4#}6aS^IѺukڠw~PJnDrXu a`tJ 2KdF(h862ԠT.x]҅Sg4YsV+ С0Ԛzz΅aE!24*^e@Tqi B&SaWX'!Fer[w:!ftZ j0G*ҎF(F`13iV7)DmYފTF/XY#W]uUZ-:ubTK\D_0DV^zt҆Ʊ+;vryTn~ʕ={^#0c|8p>2apW)Tb#O6}W5mw!{߁lgP"D 1BhŔ Ǝ+˖-.],6m*L%`N٫ iQ2Ճ)2DqW32~$UzOc EO^ؗ,Y" !H2 ˜밭"D(&(y`]v+VӧN*+Թn*Ov[WeH&0ao`')H`;&\ըyI2 VU|AX[le>#IW|w4{F*U"4$%n{!+N gt(]"y>ֳgm/,.M]WAj}GuNPCLs ~_teΝdML9 ovqe.oVIS=|#/0m6"DFW,M[=%\"_|X(Fk?\ +R~؁(>(o&5Bd6:S)lMWSA6wkV珌BIr)XS8F^?*.R_?ˌD,MM|}QOf0Zv|3*͡ȷyMĊ'N/XyE#:C套^^&7>0#ZM=;?q#<!NL(6#yW8QѼfwG )G^+)D4|QDӆF^1ryɗ_~).r@f0D;zP{jj']d1x W+ l6!χ*9ϣl^"*11םz= rط4k}Ds4kSUF^Ylv^EUEkE!2l< %hKkYflŖ2͘#8B>?f?f,o}uY32KFI=A*ðA}=3&{}c<-!9FPq)ցwH=.-MkƂ](6YQu~g꫅tt\>U"FC}֤.o`8 -J` 7P`L1R :V.CB ƃL՘ AjYKF݋PY=TGJL@eAz6&gF jTI]/&-p7 RR6~71n!M_{*Ba5 ]hEQ̈́yH*I@`o`o 1gQ3CV ֎?J1Apq;m7ԥ^4HP6~1VW}'$]qo/l\A!BdhO 5uXڵk'Y}WH馛)āS) a'^@dwQf#os1`2R#/S?+.O]9(FU!BȰU6.2a()҇3e( 6K!>RRR"%+NOs3)t70`JR-X'@a{"'_' q(y1mH =Ə!BȰ8ԥFf( !Bk-ϗ0&ta*zzkrM-bA!Ȩـ^j^>CJZ$}.52!/UwG\-=]Ҙ6$C`iC#,$)!BdXln4*r| vۍ-JC [n2²h b;cN51-ΑϏ` )56r،AZׁ(yqmHE2aI Aj{G^(yp~k^,󚢸O"D {XvJ# IΛ7/n*>/_aidV\)5P.|=Q.eR{+ #dFKAdyv~T Z2wv7*ީEkº&Ç/y}"Dӊ1Yj(63ϔqƭg SuD` ֹsg9s4()-A,WI+$|gqөmq& >D56~4ןzg%MsvA#5J"D j~^BmnCZaAcm =Dn=h0zh 4o˽~DjVxHՊFSF d$E^\WbMoH_~Ayw#!B8Qb~*\4)s=W0iz!/~p_+VG|O aYBS4j #? + q!BNb/"^F[Yj ſUŒ5.J1403F&L*!B[1\1C?ufTrR4܈c1~TcڴirUq -kwv'|rg.fTʜ83i=fHq03gc'3ӆu9 ?`y79f631Slǎ9&YҹSե:u꜑dȚy~sG:=zöxĬ_V܆$7|Q/^͞9YVtG0xq} wE\pбJ7q{%N 6;'ŋ+z+:ίT}Jk!r!ԫBk"dGyd-O]tQ佄,T8>%<,6oLNZ8˪ߤ&5E|"A? -c".^us^'?e 'ύNpr2eZGv[ă-uYѣKw ,ek;X 7%ƶż>Ł3F[=Mlӛ䫂lq!십;ԟ?~tFzjD_%뮻.2d$E)O'NWMI \t UM>| ӟ:Sx ͖ׄ+x3@D=cǎ%k_PCi!nƍ|ƫ6ᰳ.Dv^epwFW\5 ĉ&hbB0W_}ud:ׄJUPfN*_CͲi!>M~n 4uԨl2]w͏m~nG ~L%,\bE $-p,Y B퇤, qlʵ?AXT[k֬=ylxfʦqS6L- !J #Aw2\WqVs1WU,,SK*/Ѭk Aoa4!=#ٳQoaE k{6Bhv{ĒPߋ+l Wy%QJ~֥_~_Uvx@q݆B%9_1ے ˖-,˜-,50n`7pH Y#?^` X1շ2!͞^D!pe!D޽-,EDq3+{|S?/n x 1ˇ[^cۦlh"B:ʂCp|8a^{eor~ Ç]8i\t^H% ^ÒT" ]vE8x0/\Vx㍹;eŇ?^.:V؟`%Kٖ(֤;؜9sL4*WV?qii˰l͚ a ,aQ{ }+$^B*9wߵ,ӧO|+5Bƕ;3Zr!.kJΝK:gx %B϶ZE,G0yd.&D_WXy W x6[S/vuR(F^>dx w "k6f…&DSLF.)tsϵ7YJ1bsi$^B ^}U?|O4ɄH´P2k׎N;m\xb2x^:k%KU!{}4d!iީ}/^?d=#lW0`-Aui KhMԄ6j#Z -65&%D%DwX)p̙3- JrZa8|q{{KE^>`)0DJEaLVẴھY!3]Il2!r̘1EĪ [CV/)QG|!5e0&Dc9vm7*&>eJgME-|_Lx jW*8)6!w-):u*Jݛc%j/0Q6%;;/6n6ޅLpx^ 7Ip6w.Q2\B&D"*ohW/>_xe_"UشwUVY`(QN^Zc/!6XUcprfW+w}${fMTfh"+6ܘ<|lD<܈V/!t|U>=p"wu8s):tys"lEg^[BTgٝyC'״iL8;w6J~},"`r/צxe7!Hܚ唙Y{13#Fl \j\6x}d,K5\4ř$D̠ag}6Mcz'~!:zaʖzPx ]N>˴/2̢^sqqAxmٲXճɺ|K }U!`ӵ78֭[]Uco0Kl/!6{k[V0 P!rBՋ+%vy3G:p([6z4YãH +s9eEan]Po׆gQ%Do_(ߞ: Æ dBر#KzSKvڑٮu+fpU&^b7J!XgYI [{\H,]!^B׺X%PkB*0uæBvEiѳVnҨmu,\X6a|vG\V@܂v{>)fg]KC3Q1%dʛ,\cd_c`^( ٳ$ubBK nP_CJ7Be%YV<ʕ+ 5ۅQKbvVx ]2m[-N0(2aa}ڋɈEf \dN4Qq%]<=nVf5E%C6Es7$mFw5Qq%><ĆJnxD*K"kg7˓딬_k[/}cLDʼnQ7B,|˸˸EeBf+.^vʒlwaP>Jl㧚Xbia%|ɓ'CITbpȑ6qD^݆kw֨naixK6[71ore˖ɸQI -\w7$lG@'ËƮ?KOBy(E0n0EKT 帔3f̰gyƆn%ܔL$w$ָQcM&$^B&ϾؖRNԵ{`Qr B8w}wѣuޝMef5jy5*{7mjޟ/!X#Ԧ4ۆ-56(;4ӭdZXɤXF?~Z~c¢|'hklI~|"?2/K*{#&" [!^4) qfx|Q5%Cnt҅;O>#أ>;r2dkP)Znu\/!bLۑk7(#Q6m<+F f0]F2JJ\rA\p{{ !:pXKI^dk @<) 1:/!bD!Bm}} f(<"hާ.d3C :P:ʆ۷/K[viqȀ;U>9sSP49kx 7vg{ƍI%omO?4fD)b`Gmۚ/[Ȫ̉ˇdU58>>f_&("/$^Bwz q{CRJDQZ NO>ߩ1ۗN:Yv(Z 2.馛r'pBޏ 2̳\|diCRg]CCx u# H %~d/ɃH֭gϞdYv'[za8[˙ u1aKG#Y^eXؙ!J$^Bt jk1#JlHTqڵuQvYgYa{.%E9~acߟ5B٢@̤+K+ QBQ5f_9z`!2Iaի|˒U1X1c+{FQcY ɺKʔ|fkc6c MR:wl￿]|Ŷ~YlܠVxJVx /uUrq u5x0YH=8V PtȌ0P(MU_ ;K _bb_bXcA B gntHW.d!Vf @u>v&3駟nr}zx0/u CbNlqoyWi ?C n#F;餓wџy|%Eh[?902+x צ<XY Nz]pċ0xL)qcF 2\s]MH(qu)>%6v2"UPaٮ]ve5>=jظ&PC!b}|%#Nբ4Z/v|1[ Xl̰;W_}5z k5!f:(!` Vh u_p@CK5n zƄK+u⾯Ұk kʕ4Cd]ܚ ;*^l]'MH(0mB "C-ī_L)KR.\hV2^vW@l= ÖKBZ x )Ğ!Zd5-Nl ͷ%4H&dInv╧Xb_69҄K"8S9sf3UYBʄ>,7u-Edg. ʸܛOoNo}[V(<*xr e^BI:hx5aɞ|8_γ8LYƍG!H.lҸ뭀 PYFSd]* QLvFPp=`XA_~S&o1>wGڃ>2ނo{l|Ïu($^B=h1o_.۷};!S7b27Z!x~du {\,e},VQ,ðVK%Dix `aggq[5p;Ly􌰻#p C9N:$ɶUtpwywP%XbO~jw!Jxx6#n<-teG~ӆϨ;#090p?[3.~a֜W(+n2!%Nhg[5<,9Vb[onL"_#pyX){yBY+s9ֻwovw5,Dao E B!ê4;#Hy2_j]cLl(D݅cӒF<'gL0k4h ^5d9ՋL `m".p\p!ߺK~aÌ$dY3^kB%Dg`|a /23Ğ6 zr Zdf@0ΚDj͚5lb'gh3zaW9ZZgB%DA9C(Nʡ{qa:l`1Xs]!^3,'Bd@l`%9UhB%DNh,aX :_^vdn74xիmĉnDldM9 j7/!@?x(pg@[|/]K0#gw2xLH(C=te".X.Lи2wLe"?/~nWE5!r&$^B36΁Xm QC\`0axڵkm&!,""&$^Bv!2R̼.}qMW6⿯!AS%NîVE( 'ī37lI_/Y C d`"fphfyZK[c$xAC1_bL"SZ U^Kd][ {x 5{rB`>Ko\̏5D:!YŏjLH( \jS2cv*R:,)2FA=A[\62J!W"`<Ûkf V-ChB<$ʺ$^B'%Nw^.lo)۟3KK.gseɒ%Y%C~} ȼ|p#^{-˂ 7o8.^W  .zR3yoKtH[^ U&Xa`>:IujCp+Ͼ"x g]e5Jj,-Ae `RK-Zd3QF"Oɼ]Vǐuu)OH@,r%[)+7|3"zg#\8`FٳmԩW @C{B%$^!6e]ݻwۏmeyCC0m3sΥۃѯaE@H $gq(a%G>۷o_!=nX6|׌3%Cgٸ+qxog!`Vg\u(w(P/w:5䡌nĈ2Zdd6MH( bn(^2$A~Cyb漤t8iҤk6n]iqx Q+,a!;l2)1vQν>!~}0! y3 o+TvsWj9V_hE.XB>S($^B3 Nء+"NSOΝ;[X7n\ź M545ā!O%`/Q(T6B҆Q9Vp ̶aH<}:ș)SpّL<A!n*!JХ,bnHd_R:Ĵq2l:ĸx⊳#\ x W%^ *_1qK/ܸAߋѣG[%q7o |KFQKdNi{Y+fw_;ug_qk eVE!q ̼RF Y NC]daB=O>d_w|!Z&L@*"Nn! k B%DMڌ!BEzŅ뮳}'ոicʕ6rH##)wN8J1*C p+MHha#NClV4N>d;餓ب}qyaeDH6묁P0kdd 4* _~ՋCq!WP/c(*!CB͙_xEigXcǎ5&^Ig}p lO}#^g͚UŸDdB7e]*$^B-i,.Y[_&MKtXB4c zBz*q55yI8Z3}K9k0:/?dM%P8\;`/^lB/;3Cevmg 5 !cX#"bM+GoXń#;әUS]YG,׳XxoZ--ՈV , C)gih®ORc?9nAVV ,qft\O7e5΁x%[ƌceu_|A`+rذ@v /) ,:cwNh̸p-aw OH tp(P4Y) Uk([lȴ-(g A5ӭVW±x.U\/[ CSp{ Y)bXB}[Hn3ChfuVa_8WHfkJ@~  (ހsiS^^"5s]9+ʍ%^QaWTy9t<>Nd0ReرhtY^O畉*QsM n&^Jѯ~*o ~̜x9ƞJWSR3 IhN1)eNJy(xe*Us†V Uay`% t]ݹ1\<5jpdZ:OpbQbVsqyWtԿG55Y؀#Y @oy  e$/"8j} ;*Bm KM7ba["Q‰F XKajw-]SL#VL1/y+Dp$ >pEw%6Ȉ:j BqU 6)cºGmpcn.c=) o ,!5 ZD&L0ҷo_T@6Lkae ߛO#[8E\&JxW'^zz0v8 ̞z@3ⴰ=ud EOoElB^ml2ܗs^Ɂb t`bkI,Y˨Ľ lGytQ4HK:_'5Dˡ㻷̙-9x D9RÒ qToZr5a|ؠ\#COWQN6 fܸq(h1G"׌3̖[nXMrMF} ץvƵ,F7D!jgՆb:l@LK)npdMZ W/*u-/nѸ-,> f{}W(hbYΠY楙FH5v'TXƟe.;rux9tcۡm",EC72y"M%p8Sk`4p!ԅY^a\]qx_ᓸOC})/5ӼWBYlQq1Lȅj4\* 35qx#l7)'8u' ,`iu4a "WYYCA. 9%LWHaU4Z褢"֪"z ~ n>~-(v(,O6A WNf}i`JgoZy&9@8i8pA 07M*DiN!\M<֨YDl,"3 V.`%(vl6c""{Sry ?Ь\,h wxAA/*F˂s^Ca hBB4]笡-Q",nG?[k(fiVl?l>sW6OApG WbW7ZX%}Qa%q~)b Lk[O(DM^B݉W…#XS+{&}{oyaWP9)!<*WnU1Kj+xQ Tno\WVrN/V0Ώk,\ۏEk{ALxqƏ#!w_xhM4 %:s^HA xAu \AMrJ)B8_9>B::2O)}#uL lpR'/|"3YSh"g>+0q/099†p] ׌ÆI^ m8׋B3]dzQs=F31 J[_8@5ET 0߅O 7w TqJr_(@S٧~9/gobA:F^J(<=$|܋K| bXX6BՋ;{Qz떪"t}{rx3/ .Wm΅h/ M5\2w5υpKRXQT)&yi+B%C2-xE mB!'%$īaYsl>]#'֩=!!7(xs{i 8 L<%:GumOzHʅ 汐2%:N(Y('^@˄w%-S*YSGp .t fϞm{949j3{'# 4gƫ*Q+ qF ݌׉W*9 OqDtgSC=dMfr f=0oSv֬Yfĉrk5Fd 5G(8.!;]n&CGx _)Z1xH M>G+ lܿ'|Px$6Mpw>]xsjy<έ E!T  0d]vAEM@j~hI Uy@֪1Frc'Go<|YN(V;rslDz BXElHb`2JnV=P6W >뮻X,6ÞviV‡6'hO:$[^^G۬.+5pmSJYpK&Īhc[N~[5$7'`%%%vv/o 7nhgЋ{|;v va Wͺvu3PM)%J;~)wpPM=ē'hn.V8Uު8ywl6CBlV*3Z苊;hqk~ vP+U: ;G lXVRl:`CCP:fep(`{\V`oۧOzɕ1c뮻J Ր|}x J\J"pD-ʐ?95n\8KJu>R/"kC{۴Ѓr] iW/rx&^UJ ɑ(I5e vp£Y$jA؜m[):AKaìͷIBUۮL P9>´&=^Kt;;J1X' V&l:'V)A tAVYZ䈲ǏGnH;>.tMm4 xu`^+\UB.W;O>/8+Wa! ژ aě0bef)ff ,߿.ի4ѸO)HF;>-l0 8?%z3xLx GݤIB3Np^$y~Μ9v՞d0|;m`Tn>z)?LJnݺqY\RkWH> H[sz6(wW0ez ¶5LȂ,Oq.Yѣں[r3fj9 )_72ofbuOj"[!/ 1[5#+5H`HԵ g`ds/| \B’$v?|p??#^R.|v|-d z'p"/JI\A5j(o6m%+O<3((t<$wUyٮu|l]Bz>*2k`vZ <΢-B +0$ wKbg<I6l0߽{w3Epiy.&jҤG /9眼 6pgƖPhlϙԏ~#/IAc0<302&^\rg֋ (\a50|CvBD3ezӶ:xÖ"VxMѶ?~(LxB#${$`\p4|g=y^ gn/FURy1g<ܚ5khLxNipO-cUh+}_ YUݯas5+j ,Cz+1l`qK>bG .>袋&bЀA;m-D3_o߾[a3 _?vaЅx͘1DžQF56Tr5OP_ߛ`%lXȎC1qD:n!Xߦ%׭[>^]+^d Ν;7eܔ>B5ܭIj5\4j*\jn fr]Q3,9#FG;#bR}b|-ђ?{lh2V@L,R#UK]0 O Ixr!i'6Z+UGXTxTDes@9q;p`0.bOg6*b(ҥK|̓\cǎ ǶⅡ?>e*Aa0*[S01Xعsg~Ν>`O~i!踋|]ڣx}_7.,`*Y\W' C%,R+0F R|Rn4(/'gcpUIqpo}[xH^Vy2Kk  ξXUr5`]waϻp*^-[$"⥋$ N'j>z74LLM bsm…:LMr 9k o0sògϞ>2"9}Ľj':`壄' ; E8?"wf0%ЦKhUҶ­\׹?q#^i}PW' &Z SKl0U[Ҏb}>}0E%'m*^xW Vqp&?su`Z>n.:'W╈qb$-s.r R]Xߑă*b xxܭI4p1.SkcMYwL2H<;$e>NGv{-Zp2-]M { ETo#{`0j"AElo͵҂Gy$Z4#yXbY]5kG^.Zx㍴xkL@_4{uɺr7{2=k#<䓩d|׮]u~" rRċgOQW:`U)SW KqX :LRs\M;qBku⮝6m^5$ǠAeA*#QuI'j9d Tupu3ʖn*?\z&=m=n:.jHxQ꫑W̺a%+gQ7ӛ:`0 g mm`W]u1N֭[5.,Y?UV'7t= n?止w#aչ\3ʩ$7g `8TOեc Zed$6rvj}N0+ . X5Ɔn-G%o~3/&\T@oa Az'-N9*>JkϚ5ZZ3 |MDWmE(a&^ީṽWZ0 Eg 3gČQ)Vw҅,m۶T3Htxխ25+dj:D`>-vڕ<˗Sm{R#\X%\RR*JMOց`0 ? 2 G}ݷiÇ~:{L'  '-(o"HR*2]?@KUd0'iF0 ܗI`afso!T\:{p+ \/f;* 2)\$X(8Cjmg&?_&!pp_6Nj1}g\{ $;}r?۹  . 3WN`l2^wQ@wqC0J59/ėOѪ̊ 8 #^Nf:Yd˥;ҥKS)\O+[nĉT. H>'g^.(* PT"pq/Ha.,B"+1_'ISNo pHA۸q_>-pB' gϞfΜ2>쳝;Z?^V !Q!U Wmg-2˅bB'i' N掜h+9I|ĝp Nm.x{.U%cUoVClǎKdKZz7k.n:7צMz_LJ|?.\N[Bp4NT:>g.\j6V ZK/?wgqDhmRqa6K_&Mu>y$;IA U/rCCCy-\ c?4͙8H.do֚w$o\;(~͚5Q,$`P;-SSwGqmLG]T+ yz[E2l5Ȃ܏bv' &\`zsE]]GDO'אD~-rlڴNڔ 9SNEhz9/CpNJhշ^)SD'|0aga e_<e!HGTSEÝ8BڔE8*$.TY[a݋ 21H 2]G^"Z<JA0{/..2H6+^]]Wc3"$\m$q!\U뮻_jbBGyw>MꫯkӾD|'"|g&QxN?tzkaA??ryHV5J -.p$|?ӦM=G (qU{¹$.lTlw}73KF^?p@.CX!lmWVqlΝ;ׇ ~'XI\`U*OSQb`8Xxdk"BW^y%%MʽKċbuNA~_UUjLK|ΝD߻wo߶m\OR,\዗ oKݚʼe˖oxN5&zg0tm ~bmnݺ"aq#oVdo#\l(?|Ly0}t.M ^/( )ܰP5MGG^󭃝6 KW"h ^e餓;jHU Э][ <HD9 J.$t8DbHu$xKiQX0=F/[t=mѷ,א0lmYl^@8y_NοH'@[V@.3q|+>\ ??/w؃ eÛ`E" -CFaO)cw'_?ig\iC(rC:Pi_!DxTaG{*0 U]8~iww𘜄D;Ou`PB<CNq1jr -o3ؙa?Ɓ@N"<{<8k0WIBB7񍜄 1bڵ+Q1ϣ zA&kQ[d/P5j%Vj8LEpE`  |NէO'[0f ݽI03/K7 o׋ H/AL)KȉB 5s U^‹| Vl_N9sAA 14~0+FG3LpmU .;`8Hx»ekaxgB0Plܸ4Wзo_*/E2% n }A&f͚,6mg<J;S' $H+ #ZqE< )gq Kex|fwW+CrJ2Qsvڹ뮻uؑσ 8FYuՠ[`ީg[G:C"\R >&L`EMk޼9;t?[#bl6j z{>UYTU&\=`eq f4LLطo̘1TXUsZeaG&Nl2YOETx{!!IsZ|[, TgkY&^N«UvI7d#uE^ߟ-<׳gOReXe⮿z99f(2SO=TfΨFަY u/^M0pV\g!, qyWYT?SqAlY y5d0ppUȨ2ѸqcwG ~Z)7`8Lxt i1MGr%glDSEDhDe6 \μ+`&h+F0G6' X`(R7a[\>=٫"?88rm֮]A4Ɇ˗/. f4B |nW) ˝μ vǍVhjy`/#+gV8H{q*sWdV^q> ҥK%\ ]~o`e=:zm[qf\;zq6ٶuq4=w;m&'wl}Z5S  > Rgp(1>]4^RХG-lxtx\"zpxl,ltqp׃>8h4wirlfffzm[;-i,EMAI((ks[0%NڀiCf,[LO,uR2vV*sob\Kn.{F75> =-%K-!Ufdw&%hX8%:dk̀],x[+?|y#:._ .da]f:Їn!wlT|&{ߛ|;L-.'r _=}y|y:&(hMP8ea4UV \փN<3Sg+2KcX\SfK b3 {VVlN>g%hхIkff{4xe&o:D1xE+Q-͵\Q:2M1N2;+}>)=څ.-څf&aK`UV49#(ōPOxOWvejLrra ʔRڋ֢MtHG'#a]R ^HPOnIƾ0AB(h..JJ(1(ȷcO+1A6)kWq-r ec=uRWȩE.a RrVcT >ydD_z.p&IU8)(&9 YuzdkPO4BJRVDEQĕ97ynydLJ:[y#>x#3NBE:+psaY)]̱' q&n0tbFEl+'ps"Gq"LPB eJ0,g@`Dh1,uxgyʹ1Z((bS2ij$6whQI:ml+VcXEQM((((Z7}%N=IENDB`PrismLauncher-11.0.3/launcher/resources/backgrounds/rory-spooky.png0000644000175100017510000022615415224505336025135 0ustar runnerrunnerPNG  IHDR<,3IDATxsږZ{KjuMt affffffz?d}y.33an^k֨TNlLeg}n} ү$Ys9s9s9s9s]{>E/*-sιt(JkԷhmX[Y7XwX[YXGx]֦5c=֗[#u9.Yw%ge`0Z/jQ\UT3SMU]*R;[dmUTZVk3ִ#Ye}j9w85*H۫z_vVsnꜚu,Xdl86d @%+UDJnhU\E& U0)@P霛g$5 +ֻ7XY3K|H`mX֍OcYԋ7e},z@rnHsLe7"@! Hn."0TT1 'f0G-aV453͹t{9-9m`)=[kC klYG׬%"**À sU@@Q7"piw4&}ϱSq=[!bU"Pj֩g'Ϫjin#6?' +Cga=h}u@ăXWhȡv@ "' 1VXp`gġ;wpaE0ݘ&1N@Ul)*BUwZe=CȪٺb0 `3bQ( (8.Ν9"¥"nDD2r`>; "N"6(iLNTx-s@>$uE2j|`"ظǰAƅ9KXjݙJH &[/mBP5"ɜr;}Vι8 *^ۭ%"nGLqRH @sΙ0AN ZGR;mιCU[Xm=HuYoPܖ*9p}x pQmk B$a>9qv  8jQ[^ߨGY9w GV֭x_G|Lr@ Q Q`.&P$TR%n(B1@Q m;ojgQ'5se!qZj눵B%[1qdQ 寁 Ea?zdrj9;ڐtg(m3}o%P$~:5Oϥ|z 묥pν,>$eqWŻ)rɩpBA2%qB !Vih7u5-$9HzNRtGW6(9VٷamZuq1GC,Y%1V!bʵ(!.š`6=f"|]jH PH;'Ufuⵏs>$˵aҺDU,K!w? !"m" 7yltptc1sJ-DNng H{\szus>$α5jkk!#Ux[,/]cw))9cX\!f]RU5O& "dHj<wԼW%]U> `ι 4$Νj[mTU5`tHvsjb՝ev>9fݓ8Y nP$'3Q8PCN> ;Cq·99f!s(8?ׁh骥k@Qpt03{cAQUf]RIݙJ lgE_J4 ?(8G8't1sXn tQng[ݽU ~D쌤Df#ԍ @ݠ:'j9!8waY!x-=䈲^:]Xsk&Tt"EwDŽp0bF(Jr"Fv&_byD5:0f¹ ȇ9%U u 10@ȩ;&.4"B,k[@?1#us8"#@ﰟiՊHB5X.]__j&qZبKT/cTe5j;BT=7ȩ+ 9s5I9Z ^Ⱥq~6z1E p!jMrPIJؽRT{Jmu7R;7ov·Ĺ-ohu.ḏ{pa?*X4vckU2GmNY'ν">$ΑucQ6C0a#APTt3 U.n@l4i}=q<ζ"wxz:_ s#ݽ#xX B5Z>漏ݘp& @U,~XPI*Y|39Y/9Ⱥ#XB,sg%=bsDm?qؽגyw#?58Im\SWNZg,s/ɇĹE[_%۪Ѫ^huϳ$!nd.7*9'؈>W]ܜP~PZ8|Hc^돭[Bn(KKh̄q9RR׶~49RJ~?3yuj܁DJ֊E,XRQ{3pϡE,"3[U6@rzځs/ʇĹY*FDc|EDSu%.WDH&I-Trh"Sh}Jp@>$Ωe-@!9 T!BG$ .W*UKInIUk"* NY8V2wZ8xwm3"F;vgV!1J> `d}ޚù`=%?e}߼Kh@&*֭܋!q.[g'ϊײ@ b(nL}oQ8w ڱZSPpB9w*#*܁|HkǭJo9,C P/{ŸK@t܁|H ,|J$asv{Cq(s7 rvg&J|Wխܾ|HSkzzZ$?'HVGqޘ "z eЍH.K}"ۗsQJH9"a^QP!Tu=pEpn:c}V*vV(0ÆCe5Pc(, |HK笏 "-R!{ΰ5B$!!X|H+uF$O%'vQFD qk0Z*:+,¹}8X>"Ӝ63HN{>0J &PeX f;s:agUe&AjgSC,t8V0C-CA< !0PXRP@|mW\]U|H) XN4?V(J&Dx!Va9by?/Fpnm*i'5?ԪaC̨V.2BQCrb ʺ"8wZy듪6&Pp(p1 ( O5P(*7r(p̺ ΝLJĹjNAE,8̈ʞ3G0?໭/pni>㩝#53e+Aѝ}C @HDZb/ެ*(T& H j CYw-]/sbJwGyF1,K|1(hY("k Qȴ@s IC# D1"TqeNa 2GU) :A.T!U YXQdɄ~[C;kt| Ftpn=/3kثg"m^p 44|HE < M\IACYLCOMp,1Ε~Q6֒D3=jDZ.8$8vcM^+6Y9(pz5a{GNaR?aIC#uHdl6ւޗ'vzah^]JgxeZIrVUP-8ὲԉX_YF,D Z?+>A+d^ϽICprG_(Ʈg :`Y1_w:GO(fCsń&TXk<^/ 7VjH" AO nlW& Nt l/߀~DyT$ =kD!b *Ԩӑ'0tUS)K[rSc pn=]-5"3XxDfg ;e? 7)(TeZ(RE _fc9 =41_d_B^46Wok{HD`c9 Nfmϼvy*Ӯ*t\/\T fUƕcȤZH"T`. 9"F6JqG(rkT5&e"JZ9"\YOq9Ηyp xܨrxp4h44+ڗX6#AU!x_ݓ&A!<˰b|: rKs%FGϴYiMXjC5@-Eµvf绨*ߓwIQjyU*AJ} EQ@ūj\@JXDqLUI"R2cTؚ$EbL+I;*b𱱱A1#&cQz簧 (Jׄ[vql|e|޸'3P[Uj˕Dx8DjhH_%qڍ+H[}j1:X> $ZφA&('}KEJ9i#NH!&YtυT=sذpg|"HBcF19dve>s+GO)3]N8}kix]Dw(jʋsLo^qj%h`T!tGZI${" IйFLQq!h*Ƭ qiOpwBl\ϫ) 'N$ <vqY= + 779"8,VK\1”]!(1Z%ȚDz ;46J-"#@kq3'L&pc17s@,Tb," Հs9(x_\˧:9(Q8 PEՇ06u{m'Q,FXY A&7v{QZ/&#ihD0NהK ?";ŧw%Qa'ؕy8V@FIAW(YxVrD>d1d~(TbTbZ1M@# `PNH> tl]D;šy*|*Q]uG;zMYJpvJ/F$,W&WyLh*08^0:GgݻH ȴʂMq `m\ U*) & BduIhJ{Ral$e h8"ihHGض6N]wU +bՄx/ =y& K#m@[< O=j4eF=s̠xLn2A#xz\&h ~0T#W`ZKEZD q"_+lbaqn.dʼ(DqZҪ{LO Jl@8VN\AQQYY9]WiKFuA ۸r8YZWbΟO0ԕW$C+2qU|?{%95֌[7SriQܥD)Xad}+پ[ԕ&f^="i*F$_<,baw_y#X#!TrJcxlEY`^P "ǫF0 REtUqQq UBkSnXc$QvϿt8FRwT }TM`JICC3I\y^Gd 0 c)7u#bX%*8i?g6ڧ K`BÉICD+b]2g2PL"9\r~]'6tGUWc`.fn8@p!n$T$8ϢJP`U_'d0P ap50UwT_á}ʺ #hL˷-7mJ6>A"Vz`K:v|?30Da8#bRkcm;+ghz^Jzz(h1b<8 Z T RW! u$y(c@A,b"P0? q^C_^}PP;\IN7lp*/j>{W^}0*F$ K OrXMq~Ӟy˙s;ǘJ =PJHF(UӝJ FP_KwŀxEA*dҭq8Ot;bPftV,,c"ĕ3$+f) 44"i8D)116M.&)6O?Zo+Jrʓ{,-[D*׍vszaZZ}Vu~q*qOO ^9LgQBERJ J[#Q\>p\ey2#cGaE.!;7NA.p"ix #|{XِVG Yl,'|mz1#¼A\9i2[WOхPDEWVIsE럘@sc;4LL eN ig!,aU ek.?W=jF$ blWZ."]#'VGq5ʢBL$ŪF_rP晅m=JÑS6wI ť.S鰴~&4>^?$gtl{O:Sl4Y_c)Dց4S{}wp栘 oׁG`8@ihD O~󭈴}i83k܍â J Q }kc0u^G}q' Jc%⨴Qs5վXx1׶s&3 AtNi/xTÓ/{7_/hӹbj˓ ЈAW$ѹ$Bd6:CQB8EM)ǵ  ucz+t*I. :(x 0\يgɝ&ugQ  ͟H8ؾ+#WTm`5` H81_ilV:u8#-6r)ET1R]Ӆ.7\]2PU!+C3޼2 i!l\mݦ۠@0'Taob6 (g p2;C` xꅼ c5"ix0*oߜї3,oW?DnJ̇B {.|V/,TQqSAlTg0v-4aw`5BblDNI֧[m$R) ^\1?aQp.<\9z xOO+F$g1|g=ekP!p$RA Q4djvEbY2@-0ruj٘*eK֌ͽ"Q̓U°U'BYX8WiL;ǽ %@UǾ̇CUV?J4<`1໺-gZ}E:#|nd;+L%1 Q TۻX8$ udr0*yꄷNN`pVT㉒q%X$YJDQU\1 CWdtd{|MQ-Tޱ4+P >F$ ,$J>|v'O\"!)sQTYm i@וꬾA bk׷~{FeWpݑp}ĭ6i֫!ճ*i4TnQ&Q:JʼptrtIP @EMܡ-ZW3x_WeGCp-dVrs{o֥&pms0x$Y EB^ 8ԇy>j,fAdʼTwD}9TU*9\^6+QlWJ hP7˟~_sucRTp0T bP"%R KrX}49p?9)a؉`b)62DqFuBR JY"Rt+pqrfWWJ{7Vu>WU:pXb xx "44;~ (\$@jy)E{0a+l=nN} /+ !P]( )e.UC\9ESݱʁfh#$O-_I9>$i,ML#*xu5/BW V" tVECk{m͒!xt 6n&" 6NGު'VFtpeFp8 ȚMK~+|dSdqˈ@yUb8*q]Pp ]|དྷ8 vf\5e3&(K8i!6 GT1I#16zCoc6>d2 VG畸^9#P""ֈ,"k+64p^7l])?|)V#"ihZ?8m=*Qs3 {0c@=w%FH^n !:wFQ gsu~ o=P3lVKu[ b=л?+'Ns=0qe~, CA 0IlMZV0^,X8e}Cb_hp |.yզ4|tTUq}R,uc2V|(R@xdDgwQO/{8{[#.^;Q2e苄*BM'&mwC1xU?ZX?s,`{>JgӑJck8vuz6IHTN㵞o[͘?k?#lH$cid}gX| &dDdG1Po-XbPaVP]s}s orexʳ?,L+II8blbUHHO0FqEQ* 8/bBYL5[{ĭZVieq/K"mȲf&1 P:$~J%7^w5=y4"i@ b.0w;]#TR7!hc)ۿ&zlMǂD3z0+ 2(c^xW/red{H6\Y$&3ڝAb +A w6Џ;#+N;c*)AN؂=T!(' k]{fy`4t~O-agFք& M,44"iXVgӀ6.00F˝,vZQ$ἏKe:+he NwZ p6IdK񵟝O "k,f28Nil/KL˘@RI%I[< TP*!D8#iuO&Addͼwq9MR'H$#ӜYQYaG6xS{LR0@fh!>n9+@ 0\/8~'{/1(cvBmg(R Sx o_^T$# րWyXʁ0J#R\zHid QdQU5^ؚ0~w8b`x:#/38ƹBhVލxFY\ր߶YY/=,H.ޜqu+gd4uB9E:Q^J:$iZazmQm׈``$ aYA CXlxu=IY9v:Zlb̕n*V¹>O?\I~"h^@ {FS4zZ V_! *In]mecVkR8 FZg""Al EᔃqDT^U"835%+]ׂ$ m &@zmAPBQ8nSjި cFQ>M~QOqdɶ> Ŭ%3s3 ѽ3s]?f 0s(&*Tc SX˹O>@y5I>`NF|U//X秸Ls Fmy=\ߺjrS?ۏ $mLx6l,/+;\KJ=.́d|oе plۀ6\ %Lͯ`qR*k9PfNP4^n"Tn (4 X|UJ]VMxˏp!F PO75>*,cr^I-]RBw/0jy(xPB@m(zq"XC)Sޜ[?/Im}u@Ҷ614=xmUUsiC98Xˤbt2.+-`aI 5V1mЇ4TWqmjB1RlI5E;}s֚F`.ܡHG)_m),P(+.v 4|lHiq}٨Q67j5>oױux7 V(HE$ݱnESo\zOCmmH@ҶqS8OfU?.T߫GRi m}XRtyk"`L,q}zE":@GU΢ێ.tux`}^.&Gnuoϥxl4| T-R^Zn`)0 H`i &iKG"ɪt {2\v9;7>\qMG$mk!~{@cͣt f=O;Eb7S( 2~KX\Qv U,UTTp"mعSpBŴ=SJz Wе,4L/F<7 &Ma)vĴc)K[yݯx^Fea m۴v5*lCz;ӉdG:nJj wqx z:SFByֵF&[3,\oҘΚ_ar![e5`d4/ʤ?=F5f4Snj5rѬBi0eND0 ;ygA_X \nDn5&GxJ:WDEwo6-hRW?/>`[VaXoȾcd0NX!r< z<28 )Lܡ&S 1ZICGZ-)43`ՕI2C17) (\~X͂" {Hz:l و" D(fŶYge)a=о+)6om im#?VbΎpwQ{x=VJ cWr!H *Sa3yfDo KsTnUxn 'Gq J[ eda0"-rkȱ- 3ؔG]4rm*-G+eNIk-mZHڦLI"{-:+I t&06M ؒ)rhDkPJFY\+a!Dl3<~BggnsWϔ HFInAљ <߄1bM?u>Ar(  Q7ZD6y⚬<dZo ԡҿ"7gOZ6/QnTtt 'DoWZr $*aʹKh4=v_3٥56* Jo"]LaI}S38ڦ5f"a[_ P?LcIY#KFW*zK&|hf)O,EPpm4R,TF*D_-Ԩ4b'KRôr(#M4R`3i[Hڦ fh;:?8zXjC]JI lG#t:pQ(u@0LvZ_;)DRڦ5L K5+Хi4*qg@+GW&n7 å@Wjx9_FoGj6gxPd9WJo!8H+*tP:>Rڪ!MF rtEܫ}ff-QADx~Á֊(8sjcp[`/jx]ddaP#JUD8sv/G->9s+Z^֎Q(~m{vmc#}X;ם"SlS@cff n%=KetLHԩ/Z"NQSjum7h3FNj%4%Wv؝F qt#;f ]8DzL@K]`Z`b{S:J5G HJog ^͗s&8m(6sυNZcxd=_>*wrϝC,fn%=&疰,!)+J,TpPd_.,P'12BNm㬔x/3iTrg֫F1?ޖTj¼t} LH %,XŋE0дR`y("Կ"p454 xX+C5ՙ 瞇Tv 8xxJM.y61k-Rb^4:Ԙz4CϋJ!tވBgN3IS&BÌ忥zjc ޹x. |Mb Jwn9.V@÷G Z lF5o bအZyZk~ΩǕMF؀X(`Y|Zci=U洩La7t-F:}^ur .ɮbEQ(V$q91z&.^J=0(iq?e?p"QK;σh 8tu|3KǞ ױpy;MAUʽs'䨞I"l@ԚhzMx-ma?:l&U^wSՑ&|gKTs]͕1F Y,̛.@A]+C \Рf,7yY-оdѨYHR`R-u]cHدsDX$w4cH"*0 \.^qz^fRzB)|U66B9*'ezF@A uDM/f?y*;?,\G(eex_ T0g<YxwEo5t9'x*yԪf]∍ Ь^4Vkh#@R$Aim$G`4@f8Xetah69NBpbMmwkewN|E`*9G3O'$i2$l֤Byk"r%_O2BXa<dbcdS8v` J~(ۅeZMY0g`<5Y6hlpS^eQ..VuF$M(R":!.x&YS{I+Look.M#qA¤jGRʕ*B 5\Q"]Q-PNUnJoSkiNtУ5k6!pDͰ)-D"x$۝>7G޾&ZYjcn=K>U|˸>jiP -e0-)V06! \#r3b(ҁEyqp`._ :VbyP.WHTB Ǣ>Z3)جz0Eeɹޕ} @@k'mkI7䔸gA(^6={d`@*kQIg&?fwn\ixC$A*Mz wp BZM$>XZ(c!hb ./hVkȹeE ]C% O\-VV0`r^H0" TxDܺr{]4~ƣX#m=Rc|H$J~󥻎%9˿hqh{7ж$`M5DR{{to4|G:ٸ0KBww?#mܞ-6G?*3ꈆ(zyy.nXKMJiRt[ͅPtgD*[)UBh@c Qc[ /;拾DždDl d}'[}mܚ-:TW8ցH drY*E%QCDD@:ci(x]˷,\ 16c|Xo~8;S+X0\`bQ` +1k(6nu^XhW \;)^H8d4H"EQ"?OEk+'tu 4`DYs[*YUH1]5Y(!я"JȊ1ٶ6<ጬQ2$52GdG&Ķm8¨kL~_C$׿^J! pA2L7' N9jiͯm0=him@D#Ȣ8%L>6#c+G"T@^Ε06ԉޘNm WdžDO{g <>5J\TcSdp Nxzvi˻};e5sMIq"@ǀ e ׁRW= 8zJkFLeTOxINq $xH|ò;ڇ=]JsC~q(s!63w07p<&rfdS qJnqST!BOG =i u`";+<N6K)6`GNͭqa1"d6붼78Lwz<4mA1jaMikh݁ED ܿQY`۬^ol[!h&vڷfh8|z>O&H 1=5#0(x>Y¯Uc`}mm !pDs<'SGF+F,Y4D[͂Cp8z`"Mow q{j Bw8*㨝Ѩ8"JzYpV`?FPOp(:ID2mtg"8$^MU9,,L]’hmWo2*ކt@xYGngͶHh cA}M t:nj"Ld^fxmfok3ƈɤCa  ;j wzTD#lPVqӟߦIY .Rnog*ypK>Kac}$3U $M/~,{/>r9rkث,NL+9^S!5SWڈY*p"aїQPotHFe5)Wе* @ a<$أH@8ӎ 6i0mD(.@'@YH@^@0WG 6%XV8. ìٰyO#f«bBroo\% % lǂGNfhZ54ZA)rcjlz 0 £2񴇁ёKM!ZXlȤc&kcň"߼汒+Q2ZXn=n\r&l\P('{xG},J6p:[5pÞ-.#6F@qģnFIi<I6ȇOvJIL^< 8w8Bz'z_"@DD'az 3N:G*g=8{|/޸p?za8g5Dqh69V]zhO;A4=6 "tx M$>&XF_GcV%Ab]iD!ca0MGV}߿2բ e(Lo96z5,HO"ɎLRJD1:܋~獇(HZ6Kq XbGD~$4p hĤ a$g\0 6הRZ>w~S" Z3!07Ԣ?~D?6 l>|(KR^w."ߜm MF 7v}m(]UZHd?ż#]h r2WJ)L8 $S +{S?ZVP{>( p*&6ATAPFQ|_:bz0[ےQ)/bZ(c_A \d[ha[@PbH&דykb?3j=[di*B?s7:62$|:&aqE2 !/ 3Bx?sd.ʕ:[L^|h ނf6 񀇀yӻSր@t.,翐+VO!swj_k1"JG/k\kEVV -v<6޸9\"VW,K-XRV7#[B)nIL14ԕ w|Z )<22 . N2vh#QRB0 'DBL"ϰM?RJtaM"B?*2 z)FȀQ!ގ?snJ|ugnyo\uHUOH<-baq \I\gfK,Qd!f7V\JZ:#"ъL2`b'=Hdf=d*jmIZ@Sd9ѮGFh>TI&IaWX9(вy%SKriIZ0RLo^3;Q(bzu+b(SU_F/`+kj5 $eF^ bP7jMlD7f@Zo (@09MGM-bO b3$_,ء1 `H2&#ӊ>?z{i}إmm FxdpAa`СZJGG?ؖ $|P 屯BPSn9PvuS;Z嶖ʍ[S`ZMs0ƿl\1Cݱ[w6\ט`o=4F-Qi2+AXK-YVƕZAR@ͿzJBj L/`yu%FۡەaHDp>?&5#u)0$AF.Ϥ\پ )QO"w`MC-Wb1wz,|[1{{p-řj3)1 ?gHʿ\k"ʻgZ1US;w'ՙlsJ FR~ׇN|xm5vҌ2r֣v"P WVωBW_='H_'Yسߛ{s}l-k4Mu 16s~CLPY`5>̱dFIPk}(v#ϙ:w+ztf!%\nB*n8 CYئyxܧ1=9M-? _CXjϖWx!;eB s +F&8z7pɖ˗*܍nϡX' Ankd6XEkhZoT$S]<ػw/}lX9u~rQݼəEǽ|6 9Bf`kH8}|?^8{{0:CV7Nέ\ܚթi.9Zsm,/l4>%D@C 2$9&Iihwء*blH0MGf솕JeܙEl^a3`f&I젱Vr6b gIa_gܘ\Ȃ} m""Ij"?OOz,Pwo]ďFc$2<Ѓ#FK~Sϑ H4@^.ҿKͣa@ "6jxLhO"d,mZŏ:Զ6M:xSM#ݱp,fSXn6<(@ohؑa vÊŲ|v~F"Z62v7}>~vUXlLϯQ:F:1< |(pЊ=JiQsG{{/j&AMfY( LLcfiUNo1*N32Y߿w/?Fݴ{GXuܜŧ~OW~7ܟ\kT%xx\Z;XG*7D>T-ACJ=FI'DM$$~mt'֙$h8E6 XɖRء&豽-Vj!r4#כ.E޵juG>5޻NZ,8{jn;3+w7Q>wٗ#A60aVA="f=rcԹ׮ 3E {5痪UܘJuяP&&&s3#æD&6oߺ+3706CP%nxS<.>meUeV!i g1~ab MN-0,SRm q' #`єnX(f̅rr/ 0v 8|gVrQtQJ(3Gm)ice}H8'>8\("/wîbefSce F$<<}r;hlz|cw 355: mW)s _+; 7 Vsj$ήIPٶJY-Gy޶6lc}[|RvYK!.64.n}rt՞4^ "!{[5X\ZLuGREz^bF| YRG5sBFuLnE"QNh:j)wgwQ,mZ]N^8wH D%zh驱P*㏾UaMֽ,jl<EL\Ċd-EC qж6l䂮,yeId_50p$%O%xTG%35xGgf9qNEdgGwg},; l6 #FEW&++IDR47Km`& Zk\}Ytw'a~i;h|/G$zt +,ORE#a#sFV,Y+KA(Na@ ffmFkŮLe`Zbf( H$ңMYc%3}1UNDykF";~to|eT%2l =19PF? /M`t~ʍ'\'kuŷ}ߑP-S3ցI8|H!ݧSY%1N* lĩ$ʍkM"a"n[֤& #h6YHRWmq^sЙpK (T 6-B! >yyy e1dudlF m\wحWz4@Vr*@ـ%,СxL%"}hE!\fb"R~mK=&砱bȹ5z=!.=/>ٿ<{)i@[rfͧX5gVh){6 $Y9. oضGl`Jl<&wѲ"s#|# i"҈#DsR^$`qq 2{N6Q _u`TKkzRkMh;XA|1<ЁOHw>"Tr\`n~6kkB5,T;(Jii;SEUV% O?ڍWh*+x|?]gsH:>q/FCsT=FtI^ -9ZVN;]etlL4wfZSjbr*7o϶ւF urܜ\ु`$! vhbdmiw޿7޾L6+wDk.ɹ.&9ϿaW9(ˁ@7F@[xf/b]:`\a(О>?N3c&\lj KeDYó&)"#P cjWnL{I"Wwf3yZpA_OݵR'SdOIJ}!@C" iMj>uh+4M$bvYHFӱ:{2ny!9,.x^9Gk;مܑN@::RNn,h^@A)g(@UX,ayi W[0aynlF[f#:SVϡPvԖxd2Ͼv麎m3+0?`T&cBOS,j5Bng 2 b=hio -T 2_ΛjGבE<G\ Rj:#V K@ AdqJ*(l)Ψ"IJ4„ϝ$IkUME^}e :Rq(vfPQOY(Re%Banqhmcnf)to8A_MF$]4@B1G,5>4OObݝ!`iigKYp=[7%WO e lz(S]]iW4nfz~I>'}{04Xʤ4ﯖbr1ɍ 2HὕUe䐀,N7,Ĩ˷bv/n+hapߞ>pY[jJ現^? f4 "3 Fr704:Pͱ Z{⛧b6# Q(ȡ=FZ^R^PFS*ܯqVF'Wƥ_|Q$b!m{JtbAZ u tVah`Z׬q<NujfyR*i]kE"j[H3x5Y[F?ac-_C2a,Lo0Nޫ!t5K^^Ls綴L}> ;ܶ-1%ZfA-WxA/$ɽTg݋ӷ/BܽvgS5 D%+\h5X#(6"h1bfIu2r}d|XWfΤu1f13Rdb?'d;5` 6_U@R|m-$\ō;mTHw^Y0-oH(@(\{HkFǓ/m܀g.k"t>GPbA]JՌeT]f 3}T"?}CfSialO&?FٖmXk6^UD] o^U5.FVC%=Y zS(%ۑ±#Z}"v'pZ,5:F0)|,luST/,,zM8:*.+nd#DJL|dt#ۉdCև^9\2|k^X SiǶr} qmۢ$*^8:>hzdq `ێF(-ߤF,<8:ZLܙLH7..zKc4kG^9t@ de0tх%K%)8O S"gpD ̦: YD{۲=Tּf׬7C=#]G hqx;>'F&d'/3/_78%DWW,%ap`?CdspkE-呫Z VNu- &ڶEk"U_5 xLݭIemo!F3:AW^: $)#1ΓdRl%(fB!?³ *eH,e#(kѵS?} `].>~Yܒlrw<|p (kX21HI;;pPҜ Nr6˵[G*]4ڕ(.n_s-`E djBcF=߅=c0}#g3 ?fڼ~TLϓ7X2cGNcnlhl<^;.LzŸ=2>.}fi-ZmbpqyrOk̂{\'9<]5$ u̵?ێ'0!υfA8N9NRfHK,b`_s결qw]`"DP5Ln{rkuI)__*φ~Өɤvd|e OqQN%$ם!{K7Veyvм.£G#5~93"ďuP5 $S-s4 ?ӆ}YY Iޮ/eҖ6]ZCyw/VgPGkvdPX,no9FNmmu.K[siUL E~q\_.d F.p 59.l) ^aX 5)(?t\ lt͎g86Z ,NX=/:P?~:O=^Ҽ|c>sl -r4[i 2ë&ő 7LvV8$:vg4Qq4ӝڲ>G7tRM,6,+kط p iRqWsy*ґh4fI)Vb7݀K Eж,(6L# f߮l k&&8}$6:fؠo|)#wCT%ls&@4`SQTH1v8d=I`$l=MY?َQ T3y}2g4"i+Nݔ*y:g0.@5 ,l|Gx2/ khhW' eg bo ̋_lDҶvdHB;t 򘌽sswftj+/g}$+7!{VxxL)E3`O׾3BTFP>~,;V,%fgY$F`/;ϕR>e3S$&d2}ZeG;FΝ܏BVHJ8w>2}P2SrmZ)Fɟ5=)-56?sOWI555EHa֧ h3}Mݞ';ᣘ]F⒦ m[XVX-Ma-,JqJ8/"A%$88ڋM45z)Çq=63/޽jcaa]&*,&r 級 泷&X++,/b7/~ m{,To҅e.51Dgpdʽ ?ɛ/?awhCZ;2:%*RNIX<ϣ\{YԞf3T%]~KgT%έxB)Ss6-r{ӳq?1ni9n?TaOE_&$!FW_^Od3,{ZkGߞ3.KwPE_ X{ QBE@5]wėѶMZH\mY=>ǧ`FBxRL0DP nIW)eguK^|#&e 4ϝ>,F#lb1i"۶lCj !q>NjB ʏ%|rԷ~xwOڶV~!}Hs!_ǹǑJ%;Ig-o mkH6JˈmKEȦbȲWIfrD0D֚2¡p`c(pۮ/EλYlF<`vK d& W\;i՘dޟew~Yl 9O q\uJmGx`6_fjTy|{oP*X/?w湗qqFB-rs|nlw#~cрҤ>ze"oܚCs"iU*WqUV-ɈzԻ TNzj8u v "~ }Mwڱz^gLΒFB]쎗4Ax;H^|~Y;KS(DÏ[:kXHufg 1Ok|ƻOWMmysiiEnƍWE&'@o&tt[?6b,w{bZkHP="̖.[cCRIfW8<֍' <dž+byKO/d7~mPE \RX(l4PZ PLam~ZJDX!Vsw,e 3 9ORĉ[e,h]wxmkG$[/ 8΁t">n`K+x6gzK"KnjAobVH:an֏\;L\\1M0,d)SGY Tq y#Ws=wsv7iTa_kP4ӽ>J!P4ua؎*QmXGML_g~qM`DW>H5fĝ۸szCjF姛ek$A7V3=Fk$o cjv~w"LTfӸ1?{zMD%&Q &xN^Ynz?6\h (:3I}ʮl=gh*(oy,MbefIR)狌jvNP(7w1W sfKiJ%L߹t̕ZжMXH1Ti| xKÝ9AkwI#EvnT5gme Na%g'(qWE:3'"<@0Պ2m K i.&]k6-k+_2>5rة 8) _?oTv[vƓL$AM $~ڵv8*e(mVYǶz;"aGgwϤPTHc ):KUx-B꿗U)#)q=Z9Iʁ2FAsU989-2-ǙP5LޙFܯOE7!smREpm B>73a^u[o^gpq7`QD"jՊw'fm(4#ƅ?lH5F7-i-WcldsMMϓ=%IuqPU"ǤF;Yę(bKB*cW;4c3ZQ;5+P^P>i1N򓑰 &<.u^|kS vؗO0XR Pw8l&(C:l$L,J#[<  1O3Y/MJU*9gWVW2~m,c.m&CF׵-+/ns˨(v-k +PjVJbwKL c.F2B'NU@I{֓Ν:ё!Xd{FIg|x0ӂ?|Cuzͻ;xsb]X-i,˦_c lyDKR~{ n͠,Fq9|/Kc+"䶀SYf3zjVڭ٦=&жMZHB`DBsZGk3_.\7|'#Y?gL 'FZv @*hȳ?M FXg gTPS9_}cL=wp~S=G{Ϯ5C+7P,WїM p,{Ϥ)lzd} 7%53Ύ4G#ZX'Wg> #}42ꕼWѨk)oG6iV\SfV󴣔2Q Os9|?@4pffOyQ,V8d4DR I? +Ek4;aL$伏N hV悲?:+ K2=-Rq~N:z`/V%o:jtk'Wk+jءhW"ҌZ9)6xn}5/יktB)sW"Ձ!)?wWOȎLz"c<eT ~}DZuá|37Ѷ-X;"6_˱T9Sʜݟ^}j=UG \/]ѧtayӷa^5'E0KY#ql e@(^5 эIW^puq9DA@JDxS{T#p<шD8>|Z?/rGT ~V\wM,K^h/Y z,tkkGB~U6*544fjE죬x*kP+VZc:KH4rI^Vmk^D@IV@O3cSvܟ,X^Z i]Aeƕ *Z j7$Ip=3ONXpjfKCGɔFa@+yh5X,EvkwOX嫷fdFAwݽ9ӺfzBr]fߌV@'~@T8:H(F%~0tCr`Q @o1"aoCWf'djQ7ڰ+|Lѽ Y\exrC{'͋x*d!5_) Jă/.ñk?ƛ_$^tʏ1Xt%#kw?u6 CH4\7Lۓ D$mkH =[Ce8%[U/- B.d/E&C(ql Qob2"2)Z;`F ]w,bM7SBvdtY6H$^|?P/Ha{ه9 F5ɤJ6@h G[ىTUqA 5Dۦ{m(Z}TB,ͤpe A׶1pYds-cT]>~]F "M!@'c:`ke҅j7MN+J+&7s8XKɡa\,nz/򥋈$nCE2G(Ț>'e}wT3Лf.Xptuh { #KzLz5ړm)ЈՅ]fж6'lD硜_ߓP7pG&fB`툤.!HU!A-Ti[\/stRX0P&S Lq?7|r ۶Yo@anz~aaPMjRiA~z:>*y'Zk "͓8kA9#Mz϶m'1fJ JѨzGdHgʍiWF3G痋avz $gd$@s)4mr!ΈM!Sjrjëziͨ`&ge5O#YpgGD,{xo}^E{Ԡ ~ˆԼ p97p?RMKT VѬWXdHgWGW۩6<ݖHdZd0I,ԶmcVRNkO+(l:ٹEN\mdmvONϣ7GY&c#ev ՒUf^ی!lCTxЍW7>VFPW|X 6D$fvDҮ啵hHd"r(㻮Cj#˧x.Ѻ5mF((t_UxؘS߸p1Zc@ϑӶ{XH]AfH8=ȴ_. pgqdvaq72A:w)]{b` h'ĥtڡVUfEA@v@2LSfd*TG5Sh_2;/rH6CcQ ζ,c*G% Pѱ,_ ytcuZu<`70,?5f-`OIbZϫ׬3iJf+,VV:N5z8l$-nmYH:kn\#uձm4-OOݾ3˩2 dr*T[\m0ґ"?Xէ1m4Vs 9s,Nò{]Ԓ(DY !̱1Gg҉_/R6 x_۲2 -fh[\ZH@ VVbu; Z"J<>zX)vD¡Sjq\K`y}#ri-M gS~wg^:[nWѶ $VR}sX?dl:˜(RkkE: (Eݨt}~jMT6-gky^Jsum߇Ba6\7;'|+K,ؖ"p!K:\jLrkyK,2%l@YoXcAY춎FÐ.dx6@ ƳjR<߲)A0SG8*p-lmk7$FB{f!+W&'PۚaLe^t.岖,,+u-39 8gQvT=QQ44e?Ũd FXKJgU mϜ<MvDmYF[7eI [96w=uX,W*RZL.J3Mit e̱gК<>s֬YMM~/=䋕ߌ=mm eH I4@*AF>:X/ǬFRrRqi)U~4\o1.7I ۑdɳF&g)iQ~Zұ-;4̞9ϐ {ixf7 Rn5rlT2+0[Cw{XZ+by(pvmay\N~CH|SZThZo.'t_H$ pux_@+`ԎLeFQ[,68cTØm80>NKX"rtжgH,-*FB:H IN I1~Fk쩧5 J)3[ͰPŪ 񠺚/T{:pfa󐌅H i4Jۏ!MGB:#z-!Q띌@XO$lx-HL~}=)P;*2 .,>!VJoXkp, ZfV!QۓЛb-;* t<9:ϜV7vMZ. 0*ax oz홍H皞=O ,l([ɶ->`7Ȳ{nOpxf?$L%鷺99ɹT5?p;@wG,0]Y4kHHi1MSgZ\ hqږ$G#C~IN9w|?fV|/ R/?m\DcYj/8 1`y;JoTtY굆H/wD680֏Ͻz_Y|񓧅r{/C~/ sUS_Ɂ–Zh}tve02܇H$PFg,mdUT<[yf.=Y 㩣cdV6=o珿j)Y!홊HzSXfHn`kY~]4)E1 :qԗ0ͤG+arf o_.▜^ԡш_'uu1@ʪ$Ev2DIg6P_#^ z wPfs"B;GzY|fM)]7[ 36~Ho+x#=\CïRA D"GR~aI ԓMxe~Yqj^?G2TgF*e!Es)?q2aιwff%SB@4d pӃ1CwA;+RSZLH>,"6ȞU3M!rTelTՠ~H$/ jxMH &}=ۖ5 < Xx۩!\2GNŗ5H$:e) t#AWG OZQSt @/L7^W$D*FAh49<ʞHMupx]؝xyՠS1ݚt]F#:6SMH$B q~ϧGΟz/N4jҘ9ʦH4") Dsg"OUK+Fk3Whgo^Jן#,/1lvI@j%h4psbnEk33qHUϊ'Ň;аxA8HeؔF:жj `=eB|Ʊ~ԓd&d[(?cGqr݀k.6}8~sϑ, ?#'p{z`ZG3%wC)IJړZL%̨F4U@M)-brr}z LN/bb+9<7؁,_zo\ gNU_uObW=zgONNT2'_E`M#⏐Ұ-612=POOp}" 4L`њr!J4j.{ܿr}/P`H<RKL~TR#ՔIײT6YhpWe$P(ߒyp4A2JF/85[IV 0;.هgsU޼(!m>?&~P߿D]]١h$+,w3>S/U/|pizaq9y"d wI%Oe--Gkh_?pGȓ8]QkFR '$ Su 6Z|XY+3޾p cq6rJ)R7or72ܻ"WmV.ѬY:f 7 75h+eAC@ETؘxrjw7'/Sٖ^m~]G?6-5ST8,>d$u8>4N'9z%`pӭanS3A*K {4_(to|lzO,8=u۲l YJ1qpыF(wΔV“a[Q8oG)*m0J15(旎AT&MK嚓T@&X*l2gwHSR-ղm 5P 1Ǻn;!=; "j58n2)zQ֚;qqiP3b-Ea)d70 sI;ّ>>4ęǰ<0P0^Xus~ ssҲp b|ƭX#,rdkS "Iǡ3 "Z 6Xl),sg涜LI @.g7S k+k?ul C1M*+&E~mj)5JO|bFS $:ǐދX=G4=@(BNH]qo"s +:(g'@z/=h!LWtω$uz'μpdsk| nC ǎħ?2:a)˖h柒˟s4 qG.5tDfZ6ȶRbJGz,xU'0US;3A``5#@z<S_C}3Ch/8d33Bm{'gFuujMӜ虣~j(br3\WlTUR;a/{L?ica"ϯ0戄I;&r]< lRu/޿k/Ɨq D8tie54=;o/}߻?=I9JN(8v_Jؚw;6W Z,޺Ÿc=PyM4!MSOp ffJ~pC}44ҙ$&E{<dS2o<,(Y=u |}HڃJM ۶V#=Y\I(+qZJm'|~~ݞ9sG^m>~1~#G׾s@kdf-NH#rHUIHlӰ:wd׺m!CE  0]12N6M<-kC/;?>uѱC(G* 1mœ!WBIH$ u⤸̎uNSojjT+hMPy S(ȤSFzoݞ/ mS]$ITU>v/dž{ܩH%lQyx^\ '1 x?~/9OZH @_n7&SIܙVgzj$kǶ\dYݤ1OB.x9W۲v8Z@ah-A:XmRi3_|ϲm;;X\-J[R2ՙp/idSQj "2G\:"4it6rh\J)YᓯO^3/>Ͽ Bnܙ[:>ޏAsu5Ufk0ϿF;pXi/O83kÜ8>/7?=\8܇!tt07u'.~B|(tGCqAۙD&RW{&! 2&bA ӪThXZáZ ]_d<:}l #%Ȥ1|r_{HDr][mEPfTM)seA)-#V.۱H'~!i.ŕMi2齖gRWAGG2(Y|oA)px]]hLܘCNМ0DЪw,*ygZĒ=#C'_?@7g~ 斤@?saf$kL:ݙt0ɘ4~26._4iOd31ǝ1*RytY&zjO󃲻e$֥L)1>:˶}@|JRrMe!*q!J‡spkM|'S!@Jr <*F0.)Ǩ3>AD)I}E9Ӕe".gͨm7ԗiSZ\w. k"w~7pp33E E?gYsgOvC'^:ϼ榒;qf'_}T2`^ $Bؖڊ:-uWIy;K+բJwd]7 ryÔۮ5X[+R\_{ZתN`X?dbbps;7 2"ߴ WVb?WI(*GEdnYS'ξa}%F3KX^ZZO`ffFDȨ>3>wdP)5j֧f!JAŮ@k/_)=4:;695?9$B\-WD{لGh$8pI1؝!j ; F%}YRݫ&B<^_x `jM9atf) \3K0GgWT%%KW'0<ЍdvaF+°M(ӢƶFZx][u>~u@#L֊U\r jNZs!fSKK c 䱶5ӁtMLv[׮ߞ] 5:~R*:440O~#czEVw#VVP(ȶ+ uEL :;:053'?;^3;kϠYH0U]W ݒB`*fR[Q[ BX1yqW s+h׵mʻێi-ikD< wQ-p8Qh90?k!g3)/,U,cHrCt9tLafp$͝ɏeٱ;vA_ۀԲ*drbp]dM]"韲)[o E4{qq2&~>u+Uh6=n~˖̓u6K[WPj `{QI4;n*<Ejz(KnYHܲ25l'_~]ox)Y &3D|{ķ8h#Q"2o{[~/Mypu<׮%MN F]{w#GZ:US['i^Y>=WPMGluRۈnEs*xБtuDz9iT qYI>KHϨ)CZ-qlQl*]z0B"cMYjHT/} Ʒ^)QpZ?8m..?uj8 EKxtD~gmKWd1ѴaW4+hP(WѿCDW3?Z0j4nۊ@7I('zq-O5! sֹF3=Wܼ= *Ij+;E Ћե&f˺+}%Ex %_% pՉأ h6nVK,]Bm{u1A$b`Ͳn#%hbux]=}jUN4Muw?$!FZH"=v_vGeӳSb۳cE Bx"R 5cjȎ jmpR¾&Ф"&bG㫻\,m\/aM$*i(-i6c VZ +E]R${sd+8%='0FHM͗u+y׿;:X m+Q`uD֣|tqٲAWO< Ǽ[[~Ѷ nc,?ǻiF>g~_Usw{)pN;qdRYY6W]RZ& 8\P˗ǯGΰRwضMl%ԥA2@[}M;^sGি/{_zo/Y2P7"BLDlDVL'?Oe3${~R >0BN".)&( Hy]TÝ H}_z[[\iin7=#(Қ h+^J¿Ieq[',b+m0Rt~.)O͒eKC'Tv:M!P2ӓ{.3LP\jVW/ɿ~JӺ~Я;_(nOx?CqcJ*Ħ`ϳ$jz?cvM'Ѣ=<PF`{ԔdIUz&VˊNHAec%+ yB^59I{{'MM d[^!I\j5i g9UEOOLL/:  $Pt6N61Dp@awzruxz~>YϔHmcqyLU#MkKsa&+T'F\g at5HM6j]YxE7BnZJ5TL"W"߬\# 7l\,$frb9_s'E`6v8'vnysހڦ6NĈ_ MT]U +L[^N)s*<گN2 bxh8 :);RJM*ԩńmOk3ixvW59i9вPp*/4\*vyUPw=u$9J`3r|Iqђi-j`N&AJt@~Z[.aՕJm5XM  Nut˕ Pp>5'II<WJcs+1RSXń( Ca;6_ڌ.V*]&Z>?BDac_J^*A _3DUw֣K^ˤU *ljeKiimhHpu;3-:Wp).V$T#An8-} [|ȸ 05޽G5( $Fa&Qپ}᳟лѕ1dIGijOuh? X, ]0Or))iXȅ2AagJm4 2Y*bDۖŬr61YIeNQZV@tⰯp$BCn,Y8W,ۤI>#O[]l?z|ϱ˕'E a4LH4Ti1i/^ 1Q*d4e RѕѫZ[L"1lkT4FAKif(7ňwq\LJN˷^ߝrD8CR"ɕ'%TƢv.,,H^0y{Tx+.|ؼkA 1kO&iɎc0rW˚5yavX)Ƶ:cIK$OHu<3qc2l@WݞǍJXXc׉zyBްr xͮsnwܑK)Db)(Rc.\'̞9]̔,\OK)lǧ>vl1&"?ZFf"f xQ9KPN#=\Ոv}J2aBRUm TZw"˹9P' qdpO2 dÒn:)UG;cABG>ңnYE$dv_ Ȅ $kA$[.vB V&?䶛7ʍʲCl}G#P?]' .ɞ'!ƻ,uL( "G&CZ>+51PYدyW.è訨db>75=oٺ+e\(rH1&O1}H+?DYM D'"}ģ7e:Cta,)57h[Q 90u_uO>rbQe6>e?<.ŠZW&ё0i,_fNફ xcS&+ R[ $5X]MYU=3Q #T2#2F XYp7F>IZZ*C8_\О'XN\[ڐPw4BA_dBJqs s¹Y#̌-ܑvjovV̜1Iq5 *6ϙYQS[baIJ"EFS"Qݦa@h/F]9 ċ #!p.v"kgߝ tY2n<ќ3Aғy"?zPvD)BÂ0!r] >M6O|Z"DI zOs=rt;rNZ:3ژo2 ü"frJ`w- X .BXRV- .+*oa1=]H6_O=wOo·Q6JK'/K0T >z vä\[enɓ'/gC ),豜Lr9.'3IΞ)(nƑs-i!D^F:<bdT=6E)."8_ LR)T(hyKsfгYCۆvQlͷn $OPg+jmTJ+zF+y@PZ:AV~ZFs?cDj(k҄q ;ek_4DdM9[>ٺm*G4:U8$*+a K)e%[܈(CăDBF dc׎7Dzhff+m$ _G+P yT'u|"*GgH]cD'fYk֮Ƥ1D0Ȫ^G(x@"e2^U2wh,N\Cߒ-&F!V >(:NV @{3L| WٔjwZiU%mh(߼DgViIebv=*Uzlʫ_~?71زx+`D+(@ºB%'K%6&yaHq(e`q lW^8)_}hFe+5Wd,,#ǟy]jD.V*ui. 죡fOa u.bh!M]ր߸ .Y4_lb,G vS!Ys-Pe`|,?M.K3F7H& 4 >~VC⽏/*c"eˉ@xfҲ^]?<>2{t ucZ~Ѿ}#OS_݇$L% 1= ш}@٢gEZ/10 zm-+N:qMOւQt dJmEEu_`p ho%|!7m'f|gk3q2gޜ3Adh+yjCH& IQDIϙ,$ܫO#Wacsg7>>>ci nsg]6Y}|_ {JtKأ9EPJJ 0X u-*P,-/H,=PM/6~޴rMRXn8`gcPI6Kie}p{JɹW/%^-OwݚHiFlUI!c!īGϙ+Z==9 b|ge. Chsb5'У!ƨX Y9u GRQUuBy\..1H eu$hqIM{z@q  XfiOV!t@{SlJaZsiwj3' Ʌ0opJu Iw*A߃nrV-Pڊi mT$%8+Ag4#JM+Q&#P 51EBELD'v+|をՋhVK_ƺ}oxkWIPW~;>o"x8Ȅq%vD#8`oX6o[_K{a"^ƆSbB|Z^6>CzH:ck4 ^Z7,YEe}|y#'q"c74(b` ±"b"sNCVhkө^Kz$jd<pp "߹+xR<1+ **-dv Ilt(d0YKkLPmY-v [nzZn(; .۷l՚5̸$7#(ܼaj21DUR&GOm<~JGJŧZ[^[$>ـ&MdY#50z20X3ig3r4)]#E. Gw" p~}gw?1 84/ЭY|+d48 6хy.4]^X6DֶvK$GBԫ11DdۮC-.HDChO$mJ?/31>+Ve_ۅ1-PtWs$ Agm;9F$Nғj]S{%@yr^v<隚"j+ӿϺ7T =53FB0O>{oa]JtE̸wU%।|$";-5$H߄Bb!eh"l8qʜ.t[Y- (64Nn)4M+k]% !eC(2#qپ\-ˆZi`=!:i爫Q Mm>[.Fk{dt<^9ii4T3lOK_7fkyF qnd"Nڕ+W\hБp=AqA0b3{%S^fA錇4-w$'tlͅ%Eص} (Fg0ӮaHK63fg !Hs`(`"<مH=[?Aƃ샱G|xᵃwE"qkHR%q#-k5mY~>%R~ DA_Z} ԭk BnZT㌓OLkޱ~- E(GKKG<EE ~, :{ѯCÿ/9wXId && f@-KMPpZhpM X`-qG^Zt@)Qś}o{}+ni2 >m0%bb2HdyS#\FlmHd&%rjdlOI~n䱼07e&&1:> 0փ@(B2IhB0m,u)~ w/9z#qCb&M$d0E7H^J\)ѱi4q,Xfy̲|ȯ3--eJhDOƟ߻C yhûH:J (891=42'¡B2OXlyʟFD$G:Qq@6H2cyeߛu]I\۪HbL4Rvb,*sEed)kF۝bJ/ pY,X%'OQg]@6$t1q]~K"O TwDsB,' b>yo?`  /_I[/5d /"ݒua(N,'BKZp%[.t=N_)hkZk4/P5%z[@k׮i_BBʢf8wV)4bVfbCچd8^w:|.\vY΅H4++=INࢧ{eEYℑ7xwZ&\ʜtjl43l;RC2";+{vӻ%;qdiCVJ}LSn1YkoSCCӏRHfƌLmM_h\_#-rr2rkq" -KJL(! UέU,)^z0S*>%0ߍɤn=KƖI$< 첩ύ+v.]:n߯<["ѭ;qW&Y`hͥ=-1-wjR{vlBiq]kÆ~nfZ/w Eɇj<{3rs20::DBq0b~NCу[wb݃^5(%Ҏ?_88Gg"Ni+"%@(:# aVLF2_XMsv۾O iFĨӑWTi69nyf9s X646M-mɔp11J)7_ݎ) OE9'?_}|mItN_"h4y3Ԝe%MK 6)D<>#z7nmپ*CMU*ˊ֏M,= +ȦcxluM%KXI'S_ NoEqtڝ՝6sؤ[Gv>6}.Zn ,6d!1F ^4H4>wpE7tTuar( \P {3P^ĬdbjA%.KVBőik7o7  Q]]L((xR=&%Z FXPJ8lBd['ܼrՔrdi}KaT_0?($S3A~^tC! D4z;ER*giKȄ(sXުӫf擪U\XҒbqPC&ںV3^RPqrGwiDso6uuѻ{++3K bi"';m4Wu64q{QLŅfmaLqE.iksXRb-p 9ϝU[&XaZ2 v8ܭoĥw=HY+3%\J`8Q+YX>g|%&)2J^VYk= S&F^ìZ@RSzttlL*M*كB|Hnwr_fvJlx)Ye#4 6u^:|.\xS)u@CH!)f%$"=Yo`jxr @FFrsS=ƃƀd`^֒x& mF ѝlK2U]Yƶox4l %Ņx{ߍ(*up_!.WBh|97q9}o,$ aD$g$XDj`7♓&T%(,Ammg%8kMW1[ΠM(J8Gٻ#U_2;kbj2Tpώ\4ˉ>?m݄o}kػs]=a_$I:nmK0?I$'7;v᐀RdXZ(ޜ /Nc55=Cyq t"ȿ]Vq%K qY c钕梚U\YQl5e(-.@qa*ʊV@1=K(/ѓ_-5<.YE77@Z>مزBBc{mk-=ܥgAv'<*k J)Nޑkp\h ({'Nq07+IdpxT޷~_쓸rp"۰#129+^%YXLB0{'Xyqq|(_Q^<ҕyX+EjZ a-yrQbNŖuP^ԌW# /샽(/)6pԟy?1M; y*K oW(<3O֦PGCYwD|A8HpC}҃'+9lLMлSY1Kw붕xz'm#[B 9,a@6裏o"$n,GI|$YHR!΋ؒA#?bĜ!){w ul4OyYcs‘HeXyx{]e;byǗN?Ѫ zؚ'P!ѱ AZں8ꛝDx C"`3;$KKt !(S:%A2b)$S5|/*`a eE[3@Ն9Ӵ*/S잉{ؼY+وl: 80|ayk A5965 ڀjyHFrBrOo4,]D\ xs DVt (*,x9~&WDĆaae&c}Gxe]pCMb[PzC ܲ<M^H,rdI#4[J"%#3qtgݲĘ/Ҩ !$JKK%9%<嫷5[B142+[ڗF"X~-0nYs/LKj@3y GIK3t~{)OP˳K2F&#$AɦŠ!-3,-爳nsdxڋ1ϠQA|NH;䬐fH7#?Q:\x4}oMhGjWJIā +%1陛\iơ3qfWOKƆ']no(6٘7/zo31!+o'%-Ң.;(>q:@ߒc=ï_O]!:O_?^{09esHM/@~e;yo:[OE)nI_u0T;o7R?0$ +[r!)o͆|9 '{l~ݮZ@OE9 YI̝\fWk6 $vls{~u#x7_d:q,d}ڜUj} Ykܸg̊ &KRum!t{2V= o%2$g^vc; 6loFLF2Cf22cfCa>%jB̲$K43ǯ6I]KG!u{)쒖D))xj{knx"IB~QaatCawxL{G^~mmDV8Phb2`n+K(?JʗD(35"O &%37EmG?{= }+-˰v`b[ԯ:/ʰr򟟣/Zؠ"Q7@:`"+ p~m&@5(.Q`kpjPBAZ7﷌r߫{=@qQQ **sk4#gh5ՕܣB^T."i5r#f"ɆCbX!dH+w.~-B`|d#%{Z::'cY9a@!a`w$߼f=J#DiTC~07ax$6$bN&Jڛ/ӱ#I&֭]dVo|xGq9g88}/yqLOm|ǧIX5t{8E$ы q[6neLA4PT\TX\PZa畦i$ un4ttuF["G1?eH|dQ]U(#V**SgYCṾbo2 Hqq{I8bf; rۿwvE]d2p r-BzpQW?Qh6 wXTKq" 9r@/vl9 !%$>SIpr O|R7,pk6n>ܷ„}RiP\KXAyF$"LYwě4Db;mcf/T !y+PC׹i݊ONC;aWyf$.y x8O1(q۶I]gq !SրCn{pzP$R nJx[C'>'SvIpJB ?,%A~mip A >/PIwRHć`$7Fue?ɉ ;"Kޏ(=s+[-'~/g5*0d/З_k/|~۷pXIy$w^` ;1ۣC,B]Ousx|T#z0_t s֬<0eX\YRV7+AK!^_N65 3cͪZHIw+ 0t`4}wb/usӍ/J8ўK݅qJ%btCi.F<:[myb+f$q9,}nWP}]cGg+$r (%%%呼5A'HRGxl|b҉.x% On{3NJwbOX{ˋr%U$MPV9L^/i-+D8t0M 5ٹu8GJJ*5'"xdj/>?.t$Q6ƣ!!޸d"`,;5<:AKWc|ZHE{ȑFSKOҤg6|>3!M~. {?,%MH&= L9U"! ɜ N4Mw,L]kӿg_׫|2sRk6 #ٴzviaҕ˗S'NR ;@$fe~"i{),ث`# #lLgoJϳhJ^c7 @ :mMkgURdHN | }}Mm=V7ݨƉŒSr¬%_ib|V55`{(i;ݔ#Ȳ0q̤&=\wkn{"s$P.K, 8t=I"⿁禒㔌 C)yn8&DRRRd0 ;iDbNE_\yu?-. %4M,a))dP@hظhZOayGWyZԉa'zXsU. T9ө\&3娯)DOd2EC}TSGWYx; 軼!⼗ +TD8T%%YFueᙌpYƉs#B1{G~"/Dzv 3a ݓH/ %%eHAI.19H:5ƵV uZaA>a(P#^ '2 Nn:Oxdt\P.]Ha(NNu1'ۇSf$ OMcC9U%C?ـq~lr5M|xQy6eѾQ C&A'86>9Ö7T[{yIWGW..IۺxIIٝLoPOsW,}'yЈ-/̖:Ii@0 2j,Ð)3 ?*8V*ʤcv~%g{:|Ns5Z;ãT&9LeteL(R6C.984c'{]^qxNZy)۝DɌ=ܹk-5ĩ44#%RKWˡB'N#n"INWd!yx'0lTHbBs =4pnMC\ ?!Nȯccs_L{0"׸bPI4$ʨ$ Us4OHx+ΨS+Mυ=*qG4L"mmb*c#4{&w k&H"҅ Hk{ػ+` $ɟ$iIց$$o&-I@d?Х] 9Q=D`5OӴաP>RZ.*ВEiŴjR*RzKcތI0t, cXq i5)ٰp~fyMXba$HLI?fl@{]r$;Ѷ]'5DD_q*)C@`eaab=?+n5̏ ʓ?H (/-Nmx͋hpxL6LZPAy5cY!^Dظ죤)dq,e09i~4~~Uz[{m"zt%%RDI8fP#xi˹ a""SXmF#lիR$L^[]q?ϫJ_t2G6?xUd2AFr8Z9h6> m #73$!C3kc!Q{k5M{& 4uETUYUhݚTTX 7+7{ao~+xk7WK$$(,ȣȡTWUPUEգ ?BMF+蟿}._A\Nb'nY&C6(LOdڐ2mh !Rz^;FMQ/c!#O0EJJo"U]P6m$:}(%&ҫ+C=ԉ$rbL{Rbլ)؄F{Li& ]פk4~X0g$ם@q:l2 '!Q J'8'u Σ3iG ]>¨` 5U4PĶ4!ๅTHHcj*#HIWbڠ@am 5w6r踙7?>(&bQRѤ @ .ot5H] X0OJ:-[82:J}V ynUe_Ef2!4]kTJ,.iJ)jJ(437yq(· ])Fb tOi!%%#QzBl@p%WFne2(cM{$ Q_JKv)<=Ҷ׍GxR}:zKHZ{)3'9y^݇/!]GМJ:&̸4d"wzKII\ BPWB?%lX[6͠^y$LfvP0@X'3֞d< ^M ./竇O6@D߽SRGd,ȁAIZ$b7`y+;gCQFe厂)SSs9Ξ(e.e k5a,+*w3m}:Om<=))E)DIL+` 8a1x()x&ށXwOԕk-:ʅwn}&<^4.F#eXDRO9x,龁!x<)}_x0v{' CwrH` (~Pc?4[^_|q6KJO xKwspzt5gz{d#ȟ,W9Қ)k3WOw_fC?2$*^o2,伨(3hHkaC<[C#װr{t)_۹tzt\\Ka 7uݑ. +W,>}UU/ IWFrG߀#{I߼fwLJoKEsqgg))R)C$ Ԁ*:I_G_]d!vk|^K4BicM:u688LN+y`T~k嶳6m%4˥ Š@X .")ⱇmZEj;Я1'O;%'1FP>kW4J_PI71;Cjo?N;t\vې2$J @$mjԌ|J 4HǶ= ZCs!1QggN9-/]a__~9Ɠc؜?ݽgSz6g1YNr` cIDFHII9&1^@|e=hӆHIq>*̩]ȝ.]O2zx2* 4'Fm۶m۶ݳm[m9o}g>5loAnr0¾[Dwĕbqb"L v3CDܹH?yX֫9mX0o+1_DTr9Ac~\?q.W@Cv)*U:dQB41L/hoŴ=# ےqt>bGCbN >ȝdj Jܵ5rq\^h}p#X {%j؏t_"41LP +{: ѥ]Eb:h&iG*'GBV8 *Y-A8Dkikܱݱw0IzrB}nvkSׯH &|78.cAi:.E|7\BݨmjY|8P f#C+ ڶaٽ'7alœb;4N]Z"7Ybh`x(dy2 J"v(ʊgzTr؛qUJhWiyu+_ynoH"Yp,_x7l?iV"VO18l"wc\KۦҔHjlA8ؘ! L7A){D>7;z* r sMbUҷSl&Mh4uDG޻lXU/Mu/Wܯq@GH8sڷiq|=}2{<8N Rʕ3zuj$ۍl(?|"SѴy(<(a,~88xu#-PZcG 'ڑ?1pWozچ>08%`K8ױ] Ÿ|zqvɽфp.$z}O$0*8XvkXGEζlP2Y:*ބb(T5sou +] Ca^pǥ 0*V& {ukO 0|k+*ЉwbXKqɹ^!Yj꭭ 2 8KYB[aXXQ̒%q048KAe]S/Kg*U(ػ{Zr?=!nʍ']߭~:#գk(^xxhr8au`8[]ڵjJ3&"OGczzoN'9U` 4 K#գcG ; ۡKokqeE?୮qd耞qDoNLi%de9q 6 p?}AÇlU.b% f0q\H;lL2+Vs:Ypr>FyׯS]d vGɋ"sGh1lٲ# 6L}P!qzӆbH\9tlBfs-%V.I9g==a=  !q@ >Qvͽs"K~8Kbk-] ! q\*5tɼrq~x lY2TjZq\.#ԅ::ۻvn9gd?WszyO>ezzzf@$L8S06lPcQ̶ҭ~I2!VtlZ@1H88 0P†=w޿nłgg(pJ]9PpۯQ>*1MUȡZcpq@R*sLڶܰz _W˻nvFq[V*6^HgMР~ݩYKC=,!q'r0RAU6ilVTIi|'sF]]$~jYj8W&a,]0fbXa ؂ ,Zq}`.PM"TAI8Sm(u2BOHT2qqY!qgZǿڬIENDB`PrismLauncher-11.0.3/launcher/resources/flat_white/0000755000175100017510000000000015224505336021724 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/flat_white/flat_white.qrc0000644000175100017510000000424315224505336024564 0ustar runnerrunner index.theme scalable/about.svg scalable/accounts.svg scalable/bug.svg scalable/cat.svg scalable/centralmods.svg scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg scalable/jarmods.svg scalable/java.svg scalable/language.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg scalable/new.svg scalable/news.svg scalable/notes.svg scalable/packages.svg scalable/proxy.svg scalable/quickmods.svg scalable/reddit-alien.svg scalable/refresh.svg scalable/resourcepacks.svg scalable/shaderpacks.svg scalable/screenshot-placeholder.svg scalable/screenshots.svg scalable/settings.svg scalable/shortcut.svg scalable/star.svg scalable/status-bad.svg scalable/status-good.svg scalable/status-running.svg scalable/status-yellow.svg scalable/viewfolder.svg scalable/worlds.svg scalable/delete.svg scalable/export.svg scalable/rename.svg scalable/tag.svg scalable/launch.svg scalable/server.svg scalable/appearance.svg PrismLauncher-11.0.3/launcher/resources/flat_white/index.theme0000644000175100017510000000026215224505336024057 0ustar runnerrunner[Icon Theme] Name=Flat (White) Comment=White version of the flat icons (dark mode) Inherits=multimc Directories=scalable [scalable] Size=48 Type=Scalable MinSize=16 MaxSize=256 PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/0000755000175100017510000000000015224505336023472 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/flat_white/scalable/settings.svg0000644000175100017510000000200615224505336026051 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/shortcut.svg0000644000175100017510000000043615224505336026071 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/screenshots.svg0000644000175100017510000000034415224505336026554 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/cat.svg0000644000175100017510000000162315224505336024764 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/notes.svg0000644000175100017510000000037015224505336025343 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/screenshot-placeholder.svg0000644000175100017510000000060515224505336030651 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/datapacks.svg0000644000175100017510000000513415224505336026151 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/status-good.svg0000644000175100017510000000036015224505336026463 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/star.svg0000644000175100017510000000033715224505336025167 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/jarmods.svg0000644000175100017510000000047515224505336025660 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/status-yellow.svg0000644000175100017510000000033315224505336027046 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/java.svg0000644000175100017510000000035515224505336025137 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/reddit-alien.svg0000644000175100017510000000132015224505336026550 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/bug.svg0000644000175100017510000000076115224505336024774 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/custom-commands.svg0000644000175100017510000000060715224505336027327 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/log.svg0000644000175100017510000000037115224505336024775 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/checkupdate.svg0000644000175100017510000000065115224505336026475 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/export.svg0000644000175100017510000000140415224505336025533 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/copy.svg0000644000175100017510000000041215224505336025162 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/minecraft.svg0000644000175100017510000000102215224505336026156 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/externaltools.svg0000644000175100017510000000051215224505336027114 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/instance-settings.svg0000644000175100017510000000200615224505336027653 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/language.svg0000644000175100017510000000755415224505336026011 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/centralmods.svg0000644000175100017510000000046015224505336026526 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/news.svg0000644000175100017510000000051015224505336025163 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/multimc.svg0000644000175100017510000000074215224505336025670 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/refresh.svg0000644000175100017510000000051515224505336025652 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/quickmods.svg0000644000175100017510000000040215224505336026206 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/new.svg0000644000175100017510000000035415224505336025006 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/server.svg0000644000175100017510000000076315224505336025527 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/flat_white/scalable/status-running.svg0000644000175100017510000000032515224505336027214 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/rename.svg0000644000175100017510000000136615224505336025470 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/packages.svg0000644000175100017510000000046515224505336025776 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/discord.svg0000644000175100017510000000163215224505336025644 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/patreon.svg0000644000175100017510000000040515224505336025662 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/delete.svg0000644000175100017510000000166015224505336025460 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/resourcepacks.svg0000644000175100017510000000060015224505336027060 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/about.svg0000644000175100017510000000050215224505336025322 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/proxy.svg0000644000175100017510000000115315224505336025374 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/loadermods.svg0000644000175100017510000000046615224505336026352 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/coremods.svg0000644000175100017510000000045115224505336026026 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/launch.svg0000644000175100017510000000060115224505336025462 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/shaderpacks.svg0000644000175100017510000000506115224505336026505 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/tag.svg0000644000175100017510000000127415224505336024772 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/viewfolder.svg0000644000175100017510000000032615224505336026362 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/worlds.svg0000644000175100017510000000072715224505336025533 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/appearance.svg0000644000175100017510000000054215224505336026313 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/flat_white/scalable/help.svg0000644000175100017510000000146215224505336025146 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/accounts.svg0000644000175100017510000000063515224505336026036 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat_white/scalable/status-bad.svg0000644000175100017510000000043315224505336026262 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/0000755000175100017510000000000015224505336021371 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/pe_light/index.theme0000644000175100017510000000024015224505336023520 0ustar runnerrunner[Icon Theme] Name=Simple (Light) Comment=Icons by pexner (light) Inherits=multimc Directories=scalable [scalable] Size=48 Type=Scalable MinSize=16 MaxSize=256 PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/0000755000175100017510000000000015224505336023137 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/pe_light/scalable/settings.svg0000644000175100017510000000236215224505336025523 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/shortcut.svg0000644000175100017510000000217515224505336025540 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/screenshots.svg0000644000175100017510000000233515224505336026223 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/notes.svg0000644000175100017510000000241315224505336025010 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/datapacks.svg0000644000175100017510000002620415224505336025617 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/status-good.svg0000644000175100017510000000172515224505336026136 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/jarmods.svg0000644000175100017510000000505615224505336025325 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/status-yellow.svg0000644000175100017510000000211215224505336026510 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/java.svg0000644000175100017510000000574315224505336024612 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/bug.svg0000644000175100017510000000271115224505336024436 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/custom-commands.svg0000644000175100017510000002552415224505336027001 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/log.svg0000644000175100017510000000220015224505336024433 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/checkupdate.svg0000644000175100017510000000212315224505336026136 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/export.svg0000644000175100017510000000413315224505336025202 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/copy.svg0000644000175100017510000000171115224505336024632 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/minecraft.svg0000644000175100017510000000175115224505336025634 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/externaltools.svg0000644000175100017510000000217515224505336026570 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/instance-settings.svg0000644000175100017510000000236215224505336027325 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/language.svg0000644000175100017510000000657215224505336025455 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/centralmods.svg0000644000175100017510000000173015224505336026174 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/news.svg0000644000175100017510000000127715224505336024643 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/refresh.svg0000644000175100017510000000156615224505336025326 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/new.svg0000644000175100017510000000176115224505336024456 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/server.svg0000644000175100017510000000135715224505336025174 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/pe_light/scalable/rename.svg0000644000175100017510000000174315224505336025134 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/patreon.svg0000644000175100017510000000170515224505336025333 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/delete.svg0000644000175100017510000000623615224505336025131 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/resourcepacks.svg0000644000175100017510000000213015224505336026525 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/about.svg0000644000175100017510000000150515224505336024773 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/proxy.svg0000644000175100017510000000230615224505336025042 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/loadermods.svg0000644000175100017510000000214315224505336026011 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/coremods.svg0000644000175100017510000000202515224505336025472 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/launch.svg0000644000175100017510000000213715224505336025135 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/shaderpacks.svg0000644000175100017510000000542715224505336026160 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/tag.svg0000644000175100017510000000233515224505336024436 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/viewfolder.svg0000644000175100017510000000154515224505336026033 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/worlds.svg0000644000175100017510000000546715224505336025206 0ustar runnerrunner image/svg+xmlPrismLauncher-11.0.3/launcher/resources/pe_light/scalable/appearance.svg0000644000175100017510000000552015224505336025761 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/help.svg0000644000175100017510000000615015224505336024612 0ustar runnerrunner image/svg+xmlPrismLauncher-11.0.3/launcher/resources/pe_light/scalable/accounts.svg0000644000175100017510000000275515224505336025510 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/scalable/status-bad.svg0000644000175100017510000000150415224505336025727 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/pe_light/pe_light.qrc0000644000175100017510000000347215224505336023701 0ustar runnerrunner index.theme scalable/about.svg scalable/accounts.svg scalable/bug.svg scalable/centralmods.svg scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg scalable/jarmods.svg scalable/java.svg scalable/language.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg scalable/new.svg scalable/news.svg scalable/notes.svg scalable/proxy.svg scalable/refresh.svg scalable/resourcepacks.svg scalable/shaderpacks.svg scalable/screenshots.svg scalable/settings.svg scalable/status-bad.svg scalable/status-good.svg scalable/status-yellow.svg scalable/viewfolder.svg scalable/worlds.svg scalable/delete.svg scalable/tag.svg scalable/export.svg scalable/rename.svg scalable/launch.svg scalable/shortcut.svg scalable/server.svg scalable/appearance.svg PrismLauncher-11.0.3/launcher/resources/shaders/0000755000175100017510000000000015224505336021227 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/shaders/vshader_skin_background.glsl0000644000175100017510000000023515224505336026771 0ustar runnerrunner attribute vec4 a_position; attribute vec2 a_texcoord; varying vec2 v_texcoord; void main() { gl_Position = a_position; v_texcoord = a_texcoord; } PrismLauncher-11.0.3/launcher/resources/shaders/shaders.qrc0000644000175100017510000000030115224505336023361 0ustar runnerrunner vshader_skin_model.glsl vshader_skin_background.glsl fshader.glsl PrismLauncher-11.0.3/launcher/resources/shaders/fshader.glsl0000644000175100017510000000106615224505336023531 0ustar runnerrunner// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause // https://code.qt.io/cgit/qt/qtbase.git/tree/examples/opengl/cube/fshader.glsl #ifdef GL_ES // Set default precision to medium precision mediump int; precision mediump float; #endif uniform sampler2D texture; varying vec2 v_texcoord; void main() { // Set fragment color from texture vec4 texColor = texture2D(texture, v_texcoord); if (texColor.a < 0.1) discard; // Optional: Discard fully transparent pixels gl_FragColor = texColor; } PrismLauncher-11.0.3/launcher/resources/shaders/vshader_skin_model.glsl0000644000175100017510000000215015224505336025750 0ustar runnerrunner// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause // https://code.qt.io/cgit/qt/qtbase.git/tree/examples/opengl/cube/vshader.glsl // Dylan Schooner - 2025 // Modification: Implemented final Z-NDC re-inversion to compensate // for rigid OpenGL 2.0 context forcing glClearDepth(1.0). // This flips the high-precision Reverse Z output to the standard [0, W] range. #ifdef GL_ES // Set default precision to medium precision mediump int; precision mediump float; #endif uniform mat4 mvp_matrix; uniform mat4 model_matrix; attribute vec4 a_position; attribute vec2 a_texcoord; varying vec2 v_texcoord; void main() { // Calculate vertex position in screen space gl_Position = mvp_matrix * model_matrix * a_position; // Invert the z component of our Reverse Z matrix back to standard NDC float near_z = gl_Position.z; float w_c = gl_Position.w; gl_Position.z = w_c - near_z; // Pass texture coordinate to fragment shader // Value will be automatically interpolated to fragments inside polygon faces v_texcoord = a_texcoord; } PrismLauncher-11.0.3/launcher/resources/flat/0000755000175100017510000000000015224505336020524 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/flat/flat.qrc0000644000175100017510000000423515224505336022165 0ustar runnerrunner index.theme scalable/about.svg scalable/accounts.svg scalable/bug.svg scalable/cat.svg scalable/centralmods.svg scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg scalable/jarmods.svg scalable/java.svg scalable/language.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg scalable/new.svg scalable/news.svg scalable/notes.svg scalable/packages.svg scalable/proxy.svg scalable/quickmods.svg scalable/reddit-alien.svg scalable/refresh.svg scalable/resourcepacks.svg scalable/shaderpacks.svg scalable/screenshot-placeholder.svg scalable/screenshots.svg scalable/settings.svg scalable/shortcut.svg scalable/star.svg scalable/status-bad.svg scalable/status-good.svg scalable/status-running.svg scalable/status-yellow.svg scalable/viewfolder.svg scalable/worlds.svg scalable/delete.svg scalable/tag.svg scalable/export.svg scalable/rename.svg scalable/server.svg scalable/launch.svg scalable/appearance.svg PrismLauncher-11.0.3/launcher/resources/flat/index.theme0000644000175100017510000000021115224505336022651 0ustar runnerrunner[Icon Theme] Name=Flat Comment=Flat icons Inherits=multimc Directories=scalable [scalable] Size=48 Type=Scalable MinSize=16 MaxSize=256 PrismLauncher-11.0.3/launcher/resources/flat/scalable/0000755000175100017510000000000015224505336022272 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/flat/scalable/settings.svg0000644000175100017510000000200515224505336024650 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/shortcut.svg0000644000175100017510000000043615224505336024671 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/screenshots.svg0000644000175100017510000000034315224505336025353 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/cat.svg0000644000175100017510000000162215224505336023563 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/notes.svg0000644000175100017510000000036715224505336024151 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/screenshot-placeholder.svg0000644000175100017510000000060415224505336027450 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/datapacks.svg0000644000175100017510000000513415224505336024751 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/flat/scalable/status-good.svg0000644000175100017510000000035715224505336025271 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/star.svg0000644000175100017510000000033615224505336023766 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/jarmods.svg0000644000175100017510000000047415224505336024457 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/status-yellow.svg0000644000175100017510000000033215224505336025645 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/java.svg0000644000175100017510000000035415224505336023736 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/reddit-alien.svg0000644000175100017510000000131715224505336025356 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/bug.svg0000644000175100017510000000076015224505336023573 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/custom-commands.svg0000644000175100017510000000060715224505336026127 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/log.svg0000644000175100017510000000037015224505336023574 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/checkupdate.svg0000644000175100017510000000065015224505336025274 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/export.svg0000644000175100017510000000050315224505336024332 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/copy.svg0000644000175100017510000000041115224505336023761 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/minecraft.svg0000644000175100017510000000102115224505336024755 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/externaltools.svg0000644000175100017510000000051115224505336025713 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/instance-settings.svg0000644000175100017510000000200515224505336026452 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/language.svg0000644000175100017510000000755415224505336024611 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/flat/scalable/centralmods.svg0000644000175100017510000000045715224505336025334 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/news.svg0000644000175100017510000000050715224505336023771 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/multimc.svg0000644000175100017510000000074115224505336024467 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/refresh.svg0000644000175100017510000000051415224505336024451 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/quickmods.svg0000644000175100017510000000040115224505336025005 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/new.svg0000644000175100017510000000035315224505336023605 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/server.svg0000644000175100017510000000302715224505336024323 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/status-running.svg0000644000175100017510000000032415224505336026013 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/rename.svg0000644000175100017510000000034615224505336024265 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/packages.svg0000644000175100017510000000046415224505336024575 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/discord.svg0000644000175100017510000000163115224505336024443 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/patreon.svg0000644000175100017510000000040415224505336024461 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/delete.svg0000644000175100017510000000035315224505336024256 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/resourcepacks.svg0000644000175100017510000000057715224505336025675 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/about.svg0000644000175100017510000000050115224505336024121 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/proxy.svg0000644000175100017510000000115215224505336024173 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/loadermods.svg0000644000175100017510000000046515224505336025151 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/coremods.svg0000644000175100017510000000045015224505336024625 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/launch.svg0000644000175100017510000000060115224505336024262 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/shaderpacks.svg0000644000175100017510000000506115224505336025305 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/tag.svg0000644000175100017510000000065715224505336023576 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/viewfolder.svg0000644000175100017510000000032515224505336025161 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/worlds.svg0000644000175100017510000000072615224505336024332 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/appearance.svg0000644000175100017510000000054215224505336025113 0ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/flat/scalable/help.svg0000644000175100017510000000146215224505336023746 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/accounts.svg0000644000175100017510000000063415224505336024635 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/flat/scalable/status-bad.svg0000644000175100017510000000043215224505336025061 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/multimc/0000755000175100017510000000000015224505336021250 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/multimc/48x48/0000755000175100017510000000000015224505336022047 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/multimc/48x48/news.png0000644000175100017510000000455015224505336023535 0ustar runnerrunnerPNG  IHDR00W /IDATxc'įm۶m۶m۶4mڶ|EOW؈l2z<64ll԰ٟ 8l 'F ;6gO~p U @%xZí;$  np (e LЂ'qU7\v [ڿrgά}hw:FT60Cf.1f&>{f黚WjFk֬W%Uz&tk5H#'v^-x@^/|HOg!3}F6|-ש9uOꉬ3&C-#-VquH;z jxWi $/t}p㻺3JOې= FÚ2zU!tZʾpw{Ԡc\LZ.J-|.:K,lG(6ٝ?nJ|Q]~;RC>^k tJ-]q<"M6EstMx&ȇ5|wn_Zؼbs>b!?쫏˞M8XpPdp=!'Yy_2Cfꫪ)N埀Ɣ? p0yTqu}8B3&ia֤MSI*pF;f G (ci-|v|? lJՀ,WĪ&A㇗iԠ">;W-4BCz`mGV -cUp7tGC/,Xx '*^]ibDYpX|M]홪*P#=vhOYDǚŠR"f02a qBpV5BX60<3!+;E4Ț5+Gx>_i )%kҼPsw_آ 0\٩?07or\VqʔsUY!<D󍂬_R~0\"f8MQN`mB6bI HtrٖXO_+w!$LnmZ[mnΥE 0ph[<]!tR(Hrw0m\ZDЎ0' U A(`mHXB4Zc/B%B&ߡ(nNGk1_`9<`W lE8ٹ>[Ax&xggBo{?Ta1yB1s#rnw80} `QbB2XXDq~g'ChA O̜XOCw D9^ %dXgY@ ;9v%=זV2SN0!x `!IJ,;b8I=zau,A9wh#$fQrw!pQ9ece,B_,DP8A?DΡ,N9C=Yɜ"-r~P gߧkTO%͆2#5[س(U"JVPyO(%"!@g1O~`qKjbެJQS 1B"z ͰZ +t' q֩/~t3@bv#ZB@L%:A`ǰ.`ԠbbT-1d3IGB8 p ņ ڌ\NOʳŽ{avJrCT2YϬ7jJ ta;Fk%Q'7wg`Kee!lТ$R)q߰:ôbfKf|.5mܰ蝴ρGeDw :N>TLUZS5yD4Tq[ v~Nko#u]b ($洦v9^$*[_"-/oKsʍ|O^q򉇾d\5`!N.LHćPI!'zQN0Ҽ#_!.iiz#]y3KF J$ao2UFPZE7't"wg}jR&F[m䋥Y< N) 6J$Hrjo>{EG:I=7dN z/x*X $v~Nܥs:EMaۥ攉1nkiC~]" @Wmbgj 3vy8*K%;WP2? ?J\6@B'|?oV ֍BOE79Xԅ }!gIz i䛳4h[ƟM ׅ<ҖOIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/48x48/status-yellow.png0000644000175100017510000000272315224505336025415 0ustar runnerrunnerPNG  IHDR00WIDATx՚5#W*fƜ72cҍiD7Ǹ0KݒtWh~=\F,"4j!}8{B砅P=w#ԕ˲{/"0]T& cdX2/z!=y x Y * a ZsqҜHCMZ9~_)ÎȢjD+![ DΦ}Z/2Q D9;Yj9RW +R}<#׉j! -P5f#Lm_@~_uSyFirT!m)ҐMx\t^ӥsrXEcsH{BtFs>s9vr\G^-ȍk$N+A*WӤLbRD!K!2z*"}LcIf$ZL@Zi]秋õ/93HĎ't7pWVm<]HTN&"gS}Ck~4 NNt_3g'4n8BxUMynR 0Ax؜  &a`ѯ5tAڊ)ڠ-^ 'niTz`ᵄ?+/ _&!شA[Iۯ`ѝe!=v#d6 ?6h6<6^ĭD6:mmVI^7!?|H?ڠ-D@'Yv2d`XW$@jvb fG[/FoA~ $mmj ]%X}⽌Nڐ~|w!qlڠ-<2ڒ[9JEcV<^{4fa@/yO`s3aV 0cǝ±֜;"Uw؉iʇL݅Ԝ`ƶyK+pIȍ@DNT Ӭ݂,>?'WgzM밫U6StqDž?ss8f2 u:y1coU!E U#]m.N,k/ IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/48x48/status-good.png0000644000175100017510000000335115224505336025030 0ustar runnerrunnerPNG  IHDR00WIDATxřzH+$${f_ O W 333y2fff^sX£U?fuUCQ5)jb z.˺KSt5E7]^ꦫBAAfQj-Ϥ`,y zTswf;_W!29'tqؖE%P\Y1I+/A իڒu% O4^ Ky8N8Oa{.sE5:֮TRP- P0q x\(c0qrL*JIU>j^+uѓb"d T+H+q? )_WFM W[IX@10"'x`b35tz BQv4(q/ w: 7BKMHsa ~؃6JQ<}rnCĈa&F< =* OT.Ábx {0u(;ws=ߜ!_Q0HS]0gϝY6IN|'x j0h0iIjv~>G  ]1nPV+b &A` 8 N#A99$va2դ&ڒc.>//Ox<M&Jӧi[Lz‹EG¢BdkDX1chHY7Q l\2H=4ܳ0_$,xs[@W< tRn?Go<^:G88 h[ ?Z@-fa7< YAO-_m~րZ!81Il@B!FWے_"y R Z-O nBoK0p&/k)^ `*LgJyz &LI" g `*"4S0shc,on+고'ӫ?HUSō7̌A,)5)tM'y9107Lֈ7/⣎Btyj h7F9o]c yY(h7oe\HئÄb#?MPQJb df† #0Qd1/ Y_,K&9^$@>{+93Y8vf?w9p18VzN;fg:ڡβs2o2[CkQJ!F /n+YIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/48x48/centralmods.png0000644000175100017510000000470115224505336025072 0ustar runnerrunnerPNG  IHDR00W IDATxKekfum۶mێk;YLLEkUO}~kQ?gޚGLobYLŚ}ӑ;զ=q;E沗Xo=\/ziWɏ0gGz>iܧT2rG3cV/doLY{sS|C,ʤR^+M{M+uHjQ_#ZI>fXF֚PR$QP7Px("RN!9/AFUVaoKizMt";nЕ%KKo6mFU'/ t6M2tSoG лp;eXooﰇxm>GbAwmZ(m jb]i5lR4pKQv"CtMq,:jn(QP]NzWFb㿢;o<2BnWRx2erƖQiD+#!oJv($+9%u_R˹[0wM:*(_1 ̶3@YNG\}#Ut,ϲ)ldLh=Vϕ~FX%Ez` bg@ѾI3SOy=)>t2tt>"6ʎbDDA+vbP,`ؽ>?av(e d4BBۙlQXŐ}olSs=R%Z-H׌Jgy&n'Z)Y¦wѻUJ")3;v 1b|"H "#W3L'-K? Yā~%}A T"\qlzG#cZ{ 2} jJ@Z>yx#lE 7SIӂq}7 gQ@89ohBzdFP߉mNŔl{R9_D OW!a?d")9Nao;KcFpW ` B09 km>sYѓz=iGKE,w"%T+@kYU1mu$M,;p 8A B[LSmj|,~dzr忐|2~ 2YS7aRK- ao[K|=ڽm-d=k#b"ھؚߞaO3g`E;5$-bC%b{ 5q4t*NiPDOh-.ⶰ>=kbgibOP-Lp6ҋ Ǒoj|Xz h aM~Mo!!Y}j~^,ڕI!r T܏|q=AOl<9 &A)k#Oa >Xn= `l:UBfGQRߔ ~k$HACGc$FzRb @9PYWD 5 "1lr(i L#uROfuC*C{hrq rs,TOtb}H '5hZπԏl:.q!E@ҵM\hλ7v_tHᖊ2;8)Hhy1R"}; , #N žYB1QwGtMorRJNÿN.9Π(.޻Rs~%e+2ޑ( H8PR&Q\įWVajPA믘- |^8:~I' VzcI`vԢx޻.@xV|@1T 2j+lsvTz(KGOꈹm;rH1Yw@+*kզB[:5{h-`Dck-|"o5uMG9{I(0Zg9S_\6P;xD~ 6k+,tj|8mezٖ3 K9_Qj)W6PEZ$E@+y',HZN.)MZ /hY-~ :h"G8i0zwkԫ>"P|}{޽.= NIV~jTgA ٜjIlX{Ee4VF,9z. +Qrz˦ƚ_,(XSo`JhLqIc\hU(uq}3Q]hγ.‹ *0Y\ JdN{]Jε#7w)yaiZ&u՝Pv?cfUr zU.<^ !,POq<^h jv^hsCX 7m|[$fW"H4V 1@=&B!@N 7J!-6@2SVRlk"'DKH'RhI< xZtz l+Dc_KYm;nta-L 336̜ö3OUb*=ƃ  M{S9c2$J.  e ނ=Q`bdD~!ڵ !D^ $K9ONG7 !Ă ]&6/ L"g[ 9\ߠ7r^XS0P[Nyx!T (1Cت-%1^U9qO&B"Tf x!ވx:< M)麖͏+zU0J;^5'ZSeRӚ\R|rp?/iMwՔH bbEbduğW/,4j燗Epe3R1ܹ5O"Al{BL9<0 !W5fqo%JGZRMv$tR~~~U 1GQ3O-`stEUON,vV.h8fN:=t吹q?QWcƉ'AS1B}`T(D+&P$PghSa'Ng/xm[\' !5\ѡD #8ṠV%Rh [r۲nOY5dy!KseD>' !T*K ]&gp?(ũIQIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/48x48/screenshots.png0000644000175100017510000000464515224505336025126 0ustar runnerrunnerPNG  IHDR00W lIDATx;5b'bm<۶m۶mVsmc{/UlVWu9m1sZ$s T@<^OzPZ *`S^gg#~Pɫty}j"kV?GS]ݨ.vZU%b f(#3 pCka1WqOagn/ӃM:( i!ș|wp+nX:Y-S_.`씤5 Q\.H3Cr(BNu5} [h r\bZ:۩r)' ,1>>OtLlS Cbњ49'O;KRWhZ-A q\!' p ,M5-z$#N/qRlbO\pȡCadGTSH rݫr]k61~̫hhIl#fSJ/"AL$tkqA}Aj' 1lvr1Dv>C9F[8qcHAy׿z;E ve.`!,`Z::z8},}Uײ=̡c?`]ρo>l["!ATX@L#hʄE 9P0-˂cQDp4_CCOkKXB_XE:7mC0Jޅ []Jڶ$d]b" 8BE\5@IZ"djR!Q)ⶰ@.j: _~Nfz[i^//oNP;'~E& Ep퍋)NLD_$F0H m D%P$WJE, 8AP6%ɤG80̡C_{tܺ;kzU >ء[LA$BcKbEF_x+QYu45V51P,,[ٶ9]̏wnbECM :- 1h+Q9ҍ(kogJaY XRx~ )w'>E cE(YJ>^GU&9` H)ƏF1:ʉb (.a00e<-8B <1#|>Naf~]A@FpHh`ls.N,LJLa R-e4ǎwaVhae,]HU6/(B{+}C'%*HCM2=-µ n#p|i`"Jb%@,Q`[/m3suHk[?p ),?7f"i-_v=jH,ҼRÃԟ%.ďZ0E: ql4L~J,>zZRhO9HsSE7 ʧe[h8͏vH[HDÅxZk1|mںH$Zp}T_6 Ǎؼy%V4S3/O*`bbM>6²O2$[).)[Ҕ, E-KHO}10ؖB"gr&fH_MVQWW C"={oě7B l nSIex$lTM|}6l.R0d8RƩƖ e5ɤ=ZZV#A)C*]Mur/aǖR06h#۵}4v,h6ss(r ig!QVq-yaP!rQ,,P_͗Qvܡ<4 ܲnh0Z.BiEaFiu%ZU1򸸿їNgA]3GGUDWxtW-c0`38g\S`vc`A RF;|ֽIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/48x48/patreon.png0000644000175100017510000000115615224505336024230 0ustar runnerrunnerPNG  IHDR00W5IDATxQEk[jƵv[Qm[FjaնP۷j&'\QxNpշۧI6lb@t`]koB跿W@Ad7X_'y0D-Az$߆Mf(` g`/> @B`Y֏ /@2=h]7X>A2`g? ^tX r  {&JiqlXfBClyʮI|\i4սi*XCX{!0!r/`Ą&`d{<ȂY"9[8`o[I@zؽ\t jѾrj,u3EzĹBҍN~p{j2&?., mI n ̈́Ii% k D@$ D$zȡQy X:YdIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/48x48/settings.png0000644000175100017510000001006215224505336024414 0ustar runnerrunnerPNG  IHDR00WIDATxO@``48kd<@D.~h830kT51 ; w`AFFLfl]s#ۻжt^&#l۶m۶mf&"Jj}'Z{ }u1ӓ> 8pa /+RںW\3HBp8D"%F1B8 49'y>AaX|;?:dY!`nNvv{?2*nӹ.1!!v΋e!6*D]ssS@ %Tl6=!jf n3BDm<0 h "Bp"}*>$ZB8br5{ UTT/U80::5 KE ^a(JT|%!*s2!Zp9 < f,Gسjjjy @duBbk幻#  <Ui)Ã+p\"L`Y%k뫁2%+*Qį=B@@KKqW!D j F B+CŢ S(K&qE ߝN %$SʒԴ`%aG$FY@#-uf"q=B>@t)@!{_c86m%b.!.ڀE ȧ \,DC}]tLt .X( l2 syM0OGs-dloo$0 X!.r PiQ@rݓT'"I ϫ`= %(yxƵas1"b}w݄8}q`%0i㨤RɐTWel.AɆH!`C|>(%OW]?{z@D[f ֮;thoH&={ܲeא.+;k!>X& b `>k5۹ƛ=]Fm\φ [2o,J~_*W'\5ueQмb*32 iPUU-_TIs&͸V@&ylN mj@H(m>3=˧~=HфY;i1x"hhhij"4ނi 9QQ.\HT;콼r[n`xk!|pyP *3ǰlD"Nyye%PD݉~r͚lZPZ$!ĽP1%-׿}3^VE8D[ee+*iQPKvP]ǣMzr]'v\UftӤ+0<=N]lΝh􏤸+6On+ IZjT$z(jp\UWeqFc @E x[pu"=oяՕJ##uubKS>>+5|0vQ e^[y˧y*vcO=uűQ9V%-f5DIdE`pr^ZYym޽_ok!JO%}3Bq0IXANc3>mGQԥdKUiOzznэ7b{JD%|«t,!V8s]Ç鯪#%2Wř3gtYY!,&5L|JgR7>s:_'v-woy+^I֝{1Fǎ1^(qm%%Q/}i,ټd"XJ$F GQCQ-Ib;.z>ϑӧOw7} BY/D36Ӧx72/Ϝfdc=D}Mpdy%z*r]wHO .7`9r+>l `]o '?SnA~!?!Rt)N"ܧVay`' +8X YbH*V׿s`UoG*]uu_^%5|kd{bΐ 3)vxY{*TT$YN"i(๠).ѰcY_F_u׽iӧkX1E RLyrp2B$]1R33/m{*.$l4$hr!X/:=󴶗LFEXb'Ngh.h7۽ۿM?#ƅ@j]Z/f؏p=IB82~+֮^v-[˖.]Bm|)x DD>{>r_엠8`n3!8q4b.B,;G]KjBH \USҞ\Ć_;ncdcFWlV')z/xn/(G7n/q]K-,i,8јƼeT꽸ZT)N] >VK{1(ك 7 U@_:Lk d}KOw j[Zs14,,,LYU% W01B7=dd\m/rcW9R|WovYST@1VK1ʚ6`P&TL-{?Wfs^׿?JJtUSv4:-ul(6 mr%'/ k>-쿎KM8r4hkښ6V2ye?)т 95{~jգ`e`499LSك`Q4oŅ@uWz4rےE(”I@Jbz|6o+q'/DJp C{F68%8A2HҔJ>:s?胛w8$'y $\TnonmR~(;E(]/1ds虡K~/.d^(/gTvj8椛@]SjN_Jrηj,劋ǥ?yIR"No"iw>6HG}C)tRG(i ˥`u+^>W{  /ZfCzYPѯݷ[`BWRi T.wd_`PGE3-|'22ZXV ys?:YG*i&N\23<\NڿFq5&%|9`!3aV'łolsoLn͗QݪE1`K]v(0Rn~'Ln5P&7`}dץcONjpP@ PdD@fG Cݐ @Īq98ۢ?/ ʂW14wTEU ܐG,e T"(!*pdJP46h\xm1H7,e(C TG%XNƇ e8%dVӂ,"P. 62 y ( PGA' o־.EN.P$[,V-Lc(/ RPִ!t `H yE7BXQJ0'R$th9:^J__28"rɪ],I h*)HC nL8TVmaԵd)pc"H`]('yMńcUy*Rȥ;~ c{#5`pVpA39`aM)&r܎N|K0qO\ #0N7j6}u,<@b+6+/dX~4sTW^. #^te |M1'!#$C.7K*}GlXEk%NcQ 6tb! 1FC08|+Zݷ >x*M_{\㎤7ބ7ޔoy9 Ln7݄oo)Ls-ּ^3NկO>.7]h3333&fԑَΔCbf_y` ZS=1^xl韔4)^% ,`f=XygK7OXmu cId&ҟ e4ldh q xճ:dnf+Z\r0}&lnS@@4TvN{ -A@0dzWq0]pqGf0-͘"l_@LشcZ m[HJz a#bWfT"!-tC«o=/NfHQB& CS)OEN@ZG_C^ǵzϖ4y#i5bôJɳ_Z.']\}/IUv)$#cZsCc?zozU!b< -f\5d{?/, ۝!)AjY\}mV}9s|V@qkkCRW;J;HPJf?,pSj62@8^k=*0 C " Bz@wx);T fD M BLz}Q2"Z:"DraҦV7 -CjcE!XܼD2#!M_e'k_ɖ,6Ha҉يIWNRzi)[L{PK<~MK#i8h(%b9uZϞbh44ZĴzX7tCm Nevۧq4ls yf~rC$16F/'iO \z,֛1f|l5ُOTg<&oOL2'ϲ5~b1%{{׾g%z[X/IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/48x48/copy.png0000644000175100017510000000235615224505336023535 0ustar runnerrunnerPNG  IHDR00WIDATx̓ajPy-grG -Z) a"e̮P>30& 8NOs;kI7n#,ɲL89@Bqa>r/g=Ua'>_A"b_LCu:Ѷ7k׳FQyvLR >- q]QUռfHz6L;U=u q[mw8xh1a/-踚i]V³( |q][^\gOkXa#.pr!P@ !^ ňyL@? d@t!" ς/"d]x^Bfwwh;̑qpsPć;p+x޺ #'1tŸbtFU/NUQ: z4$h؉(V"28F"|^āAy"ޗYB3 {! O>Bq'|Ofyi+9B..y~עC9uxktxphn+58dzhmx}mF&/69J˲"m63%?#`o`a09btrzk :o ZQT U`' ?=M}.!xO5$ k%q 9{.^B8BS{GOFJJ|RB8({{7(b~~33x7_ŋ޴ԴՕ0=DF_#KK˸{>|7 ssJL&V.wL(iϟ;~osd>q0~sA&tfp6~󺣣IIblK~p˦`Z=p͍}ÛFA%ifR%!u|K&)H,ƎWUUdeT;2ۻhITn 1QI$ YEs]"qNF1=xR)/YeύqU{ +((|Pf8By{H:V 9b Vs/6>Qt@ZtC$] Ba[ Sܬ]b>"GBA徿;<VMjҌ,9~4hfc5NiXD^B^yüsU2$zOq~Ĕm9YL4D Ԇ\" B.3,"E^ď`j]Ԣ44PubX| .w}r ݶhčkEƻytCTԙXĦAo[Fõ;yDHhC4I˓l'V *8;səu؆u$Bs;`7Q5_H*mIthޟ$u"<\/߂7rIZ*_w oؑ/5J0>+ 3!B{y?ld/6-h4fŹ B7H.EŽSO-.Ӆ摟lg8B <.h*wU!9ʕIC y)ivfqPЙ/g6 \7e~>:Ѹw@l_Z^>qv~=1WoAO0!K ☔CPxŅ77 +k!u-k$%DT#,vIpcl;.Frp4 [o3XeIg*:Ֆ3W.<}qJ9[ Ha8 .^‰?-<!_MIVmmrrw_(3p4YX G~-ghz!xw0l>,ȶol٘+[|\nk k'@P 0Y,u p@P Rd;Z?p7spNVlGo5R\б_hRdr.IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/48x48/instance-settings.png0000644000175100017510000001006215224505336026216 0ustar runnerrunnerPNG  IHDR00WIDATxO@``48kd<@D.~h830kT51 ; w`AFFLfl]s#ۻжt^&#l۶m۶mf&"Jj}'Z{ }u1ӓ> 8pa /+RںW\3HBp8D"%F1B8 49'y>AaX|;?:dY!`nNvv{?2*nӹ.1!!v΋e!6*D]ssS@ %Tl6=!jf n3BDm<0 h "Bp"}*>$ZB8br5{ UTT/U80::5 KE ^a(JT|%!*s2!Zp9 < f,Gسjjjy @duBbk幻#  <Ui)Ã+p\"L`Y%k뫁2%+*Qį=B@@KKqW!D j F B+CŢ S(K&qE ߝN %$SʒԴ`%aG$FY@#-uf"q=B>@t)@!{_c86m%b.!.ڀE ȧ \,DC}]tLt .X( l2 syM0OGs-dloo$0 X!.r PiQ@rݓT'"I ϫ`= %(yxƵas1"b}w݄8}q`%0i㨤RɐTWel.AɆH!`C|>(%OW]?{z@D[f ֮;thoH&={ܲeא.+;k!>X& b `>k5۹ƛ=]Fm\φ [2o,J~_*W'\5ueQмb*32 iPUU-_TIs&͸V@&ylN mj@H(m>3=˧~=HфY;i1x"hhhij"4ނi 9QQ.\HT;콼r[n`xk!|pyP *3ǰlD"Nyye%PD݉~r͚lZPZ$!ĽP1%-׿}3^VE8D[ee+*iQPKvP]ǣMzr]'v\UftӤ+0<=N]lΝh􏤸+6On+ IZjT$z(jp\UWeqFc @E x[pu"=oяՕJ##uubKS>>+5|0vQ e^[y˧y*vcO=uűQ9V%-f5DIdE`pr^ZYym޽_ok!JO%}3Bq0IXANc3>mGQԥdKUiOzznэ7b{JD%|«t,!V8s]Ç鯪#%2Wř3gtYY!,&5L|JgR7>s:_'v-woy+^I֝{1Fǎ1^(qm%%Q/}i,ټd"XJ$F GQCQ-Ib;.z>ϑӧOw7} BY/D36Ӧx72/Ϝfdc=D}Mpdy%z*r]wHO .7`9r+>l `]o '?SnA~!?!Rt)N"ܧVay`' +8X YbH*V׿s`UoG*]uu_^%5|kd{bΐ 3)vxY{*TT$YN"i(๠).ѰcY_F_u׽iӧkX1E RLyrp2B$]1R33/m{*.$l4$hr!X/:=󴶗LFEXb'Ngh.h7۽ۿM?#ƅ@j]Z/f؏p=IB82~+֮^v-[˖.]Bm|)x DD>{>r_엠9^ (ʣN # >bp^+8Q P EԙO?A?` vPx CzJP ԝMFW>a-e@ A>$kb;]*GYbczВp7nԽ[Zr AFۿTn`p6O}P *+O{P^ȅlyX6uڜ6p6DzJin*93qIC)r +HVkg`~4B.]~M^Oi"3Pe<`(kAG6cs,'J!CĪbw{LHKҺ<.+oBykLjb#-  Ǐ}1z"Һ<.+c@&A duTcB6+Ϙoy'w|/Q*dB==75Sp;_Ӟn<ߜK W9 GÛg8BSe2Þv.d{e쏗;_5@P‹y !/q <ටޖ!"||GDˆ0 츀_cvݽa> t˛?{j,";x~=8MWT a8l,__kcxJn)/K>yӍ7 `ˑ<^~U"䱷z<250&|o_;8<AxS?w~/0Qbȵdq9wrY7 9yzGHny }aByǽtR|L-ZM[C}nލ [ˏ6lŘNJ0r)SXf(?U`MR$-xQg{>̷V[q[nt̍ZXi@7b@HяJ;,4X/&1".vX,rR&&^]\8<(СEP1k & p8frlB'yҥB=O8ɉRhЄxbQq(Oѭt)kQ3O~.Bfs 8z06"[@f" ;~ȝv1#ΞmnKWfbŀK- H,O P9z9c)I@n:1-X.N ^IS0rܹ&$^Hgztv >aBr(vu!S/[G: J׻ G0@&2 yڴbAp @xbIN9`9飓eaCap+$(}$]%fg>~H&!;ŐD„7t~ˑ IO*:gH0vBm8(PԪ \F[?.aϷf⇴ѽ8ǎ1e%9BFVEuJR縩aFǻXQ+k ,,k <drx3}\.!FH.A;-.``کR]LԌ@ Ih"2M8!I@)h7 `#ur{ś umv;0bP} zK|5ġ\]p9s_ZykbIPSXZ1aK&<0Vߜ* Qj=1Xc+(.pJ 1HsNUB;O Eg,"!.x#zDβUE0ĸʧq[% 9E(q #88uΩ/~/I^xf-lB 1d\%@3AIەrIRՄ8iHSBlU$a2^ԥ-yj:FO$FjwLtO*nd !"9 ]b?I'#wu"IxD2(s=Hshc>)f`n1G)ɍrdrv\S7Q wwI4j*^"]YiK#ݾRi"2&272g!%͐}OXS745)&mIWAL* PdʩRCh1Mn}4uLZI*Rh@*BY LP6d{ ~Vp.O62ճ@d2ma|)-Ĕ)כۙfZ`:? d1bI5Nsib@pdZya28^  ejrKf&i3ޯk#1CL yὒ6_{ZފXU 0!v=fk`\-.-oD~o쐓U[N=v)^q@`>3X0u{iO>CS $@XA:JPIniQw@O.8Δ=wQ(=J\YY toie:|̈́)L.eJi-Ćj1wR${ba_Gbz<_8@:4(58y…Y3-+ Ězexw2/$Ya2 -4TNgc&LfΔ6iպ3 dv- P1@8eáʒV>猝7CH.d *Cjiy0jG7poLK `e2*$ X Pp@ IĤ$2m&zM?h]:]/ xHqff&? |ᘩL|4e "#jA/%?z~ d`?n DzQXXȋ,fV {dQT@JN!K,[F =d@?8Ө!v\*Bb \2  e)aݱP g@ d ŜmGbKRkG5#{9mZNKXvlGpAK 0](mjG(P K9bz$/Rz%,7KV pWw*jWnER6uɾS5Í-Vl 2u[ǘ|tv'؈# ɹ0+׆a3)W5?>m)dszZcK'E"Q G@}ꘈ ŸQ{!q2e\]#KhӠ˜y.*HriC46WX9VRV3zMK3'=s ̇Y['9;<_}p.3|8" ULL[J'+ PܘR%cM΁sV0{Tk @}SX',W3z/L^sxuZ>w }btF)JIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/48x48/help.png0000644000175100017510000000670715224505336023517 0ustar runnerrunnerPNG  IHDR00W IDATxZ9Rl7Cg2̌_,Xffp23d:hBIۑˉ8ߗ2>лOnAhU?_*4h Y`=]ieӍ BB |nt %X, YZ5gX38`XvՈ%TLA( 8c; a3tWpp7;s;&X(مLsˋKr5gJp9A"8X tp{=1hՃV; j/>y @G ?B̬>P^^4Ri~('Gex!@F`1>]޸z8<pj@A@*Mcng{J+BGp!B}w"` %N75dL l^v/Yv;>@Ё+Z\x}_{i#LQw2p>aB 8p 8Gi rAIؒ {=&xky*+ɏjiZ`d]1F< ! AcA!"㉛(OT-V:fD P`f^]8qPH-b$ "~ #CQq!09n(Psaߙs$> \)/>O}' pr\}> )TI: :y7v-iٵ{c?q5Lv nA ǢpD`*'Ȑ"J)GA0<7luOq(z虏'j.0C6sJtAahT rL0m L: ˮK%Nʁ'_H 8LUD $bz%g2x~5gX*Hx @@dr) nsrQ;I/S*)~ÏTn.nJ{)>LT IA>KxrK' G-%5i:O۸ <4k?7?EL8Pc]Ɂዟ?iO-fPLr>DmCY$|JZZgwY_IH0Rqh,4msn@8XO.fD q#;}`{\V墍g3xd6b:!0 q2\ fi*7̰ ؽh(L\E1zuFeE}bKպK{C>()!k>aHt wW9}0!W(p$b:Bwyoo%`cF6#L"Ǟ Ra'= dws"JHZ֪ ,X.ű?/ #PVF 21 u`)aZR9ӄ0@&JK x^0.|`,"c` 5|nAP ˒T:jky5wU;mT.G啄VcbSm' @(`#C|&`6l!ϩ6so,b''O-dV!=DLcIl@FeOhJ ;i9 6NxڈYB-OPj"*! gQJp|77eH X`bF"mk Lbb371-II U>O,BZVW'wWw;$㔌4àzу LPB-kj_دI/޸ClDL>zBN h p:}jD4mB?<_S;,֏q}TLeNz.g Z +ɪ B2GIsru|XR\:% BAƧ%1 \:%JبR&Hb,T2)%8~?<{Cx&OL>;CC^6iL{7J!"$?F XO!<ߔ1\i'؄62ua oWi1'4|^M"S ^(\BwRLmt r)d`n5C]N"˻iydW !qW4p`3IRP;C<Amө_u:~ZKgv;{]靀qB7$%1}?~ @34XVhv?zp57wWJjfB^w?b"e'Rٱy)f}5Ĥ{m467^pj:3)8 !B8Ԋٜ/~#y}n{`WH[6E~ߟ7{ Ak7Db:w^>}nԠ}@ nNў%J)'u~h}Xuh`|1_Ԋ\yUGTrAg\*ͣ~ӛ}z`T$dT3.=ܾv- 8;+L$RH "?$d5>0֧N`9n]k f.  t&M9I(bu%>*5TpDW;~sN׽Zd3Vq QkHӬ%ST1'N'kl4:GC@֮xܾN'R) .MN'c?15`C?vᣍ@ݗt%F;ˁ[34#Y.I2a2ƹS/ANX6pd$5=9rï7 ۇ^Q?D+:1305:&E}ȷt(bV{ρh/N_VńA*r\MD2MHAa`°|_5 JR-@pjW@/uew*!E*V'W %ECkSaR\UTU> K#p0T(@!ρᾏާaP,B GT">f2 VX 1PhBB"1aFU9v3?*&ܟggbF6GzBBfveo|g7 "Me2J$l# ;rɫ~a`Ofsh˱{v/w/7l(! B!( QH4Us1WNA\2kN7*>w/p*u.?H)@~+Fb=`~.^u>rUC 4̡A-BcYX!49{E@CaY>%+@!c\S߾=|ッߥº:uz-؞ؒO;V;|#z?V`BvlaUJаbR[vic>w-%gjD` LR^:Z6 QHP<@( ݶAAl;:*4ȃcFĒm"OpKZ =40Ma 84/08CSGm܇It tRLH1U?쨈H⿇rmv8Z۶m}ƞi|ݝ:&q*u᜛P٬u{ XNDk'VzA?!HjBS۞2; O_FpjrjCc>Zåyafut D&ea?0Ku4A}5lܢ{ta|1VntXJ,xgyć`6g$Rƃp< x@{yq b ᙘɷw:2KC/p:`iyC{}Ih 3EB:͗ny{9N:<@pd٫!(mxN`b6}lj3LOP:2JFLse-=*ۛMV,G'y2&8%c\=L+pI>cxO_irGo[ X1M1(ŊɥMyE8Ǚ$sxKDZ#xL+KiRaY ) *7Y/(Rzg}yd!.w9sr(cs\lrM6C܋EGx.3c\%ĕM*ceƳW/&4NXfvs6;ʉ;@6kX23_dva{#و< bS0{]&CNEh/?qgh8YZ> wwt\.f$v=?[ϓ%zg5Bk [5!xPr<;w cR ~L `qICS!2Tonqnj3\8Ny:ρiV+,ЬwtQQ!֚W5;Dq$CuG[Ȑ(Yw28/%gRInwX+:#/䲖\L5՛5r,wRHd0 =rNs(Sىܮp >iR7rYߩS%,Lqé㋈D!5>0HCgu>J"Q7 Q&,ly_x]‚B<* ;8>ߺ]:ǝ+U29hv/*dh@d(Ɩxq-8W\B~",vqP>$ÎR13zK%^3\ۢ!~bs[l"[Ƞ#B4(RQ X6Zn}$ ˍ<.ɘZSET 1Cn8rbSk(vg,^t1"k-M\H>:2t{#qn5>ك;{b2!xw( DZq3?QB糾O{E~;d#K\1Z4X :0J@9AG%r[0YZ2 D)r*^p79"N{$Z߃ pAAxFg_|/-Loqhe!$"St(a QuC1W,J4 +S-2x4(^C04@l5C 򞇹^8N% =$GkkZN*m|Br ƀgЈ(q Ѐ_D{sgpp9g"0JPI+ۄl\ɬ%FĐ](nևaIVQFvk4`_7] Zǽ¨0F@ N `Q j ]:AD["1莸rs+%JV3 P\)6z씛]n3H;WrIʩ?:HĕrJYB9.xJ`;ڳ6Mhud;?y r`8:{#LP4 >@3N )ra ~Vsx}aU휙qη*}4rrt4)i(ExRYFն~Y27 4gBES_bLn9%RV4U:qlgv_f7i{]v_y#*_ZO-}Wo+-}gGc SxIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/48x48/log.png0000644000175100017510000000324015224505336023335 0ustar runnerrunnerPNG  IHDR00 1 gIDATxn,Յ8=03ˆ+offAa/y|xjԒ,˞>[jlUacl?|MHF3:GplsDӯRr8N}O=@Lfz өAV|K> 4g2N :Kk#ֈسNJ0fZT9́2%c Qn]QpUs I|Lmg245f%P#o0OEz4!$$Y|9cPyBR US&@Aqt{="a7L1JV Є)rf2333,,33333ɋch9z?FUf$]W烿{wO zw-;y/跃 b8dg?C {e'N7/j4sVy+|bϖg轢/O9YA,l988@frQ>aEo>S^.R G* l^- E݌8_M?LLL8b` 1jb^{NVp8">ʕ+v8K Wɧ4fm._>lطs{{{Y`:<Hg}ɷB8mط5 Z-hy;V|iiɾ_x3~==ˮI vNjq?_KBb4g+Q`PVzcee;ťY=s)Ϣ^58W$ " ՌHpm1[]\.)k|Ɛ&G54KhkVAD k`6ڭ5ޔ&mP{1e5qDyK~/sE$69VM]7ŸA_62pgUT=S Zhcmvtܚ)J3wIA!u4fB,0?܂v=55z=y_YJ%&6([Qf sVGk׳$A쑭&<כ3G/~0-rԧvA-I/"RLx }ƶ&0T f5kNelej)ExS5+W"Oݚwh˽b6͖SQ P-IUuLrbŤl`/\`R%1*(6 PQv)ܳﱾt'zar18^qV7>d"<g(B8IӅnGM# Il: vIZAvk/C9VbQq2r<iKm. gn[Թn#%Z،j83U-5v.qҔg"MVfaB E=ap2Dy(N/`="C+bCҞCl^&[ƤWC24+. kl#RɮsEgiLMN3 $v"=p/hd~)JQHFifzM`LL7*d̓jei]7-lu Uj@V{*NrI-4=^xU^#Ψ$3:bd#4f /)LYǗR=$='֨)pEF+tÀ֘d[Xܦs0"wƱtzh1p.T@Yz wT.:4J9&\rNIU8C/%$.e~U6E>Te;BnAQ-yУw13)X\s P:`=Sc]dy";1CY ?ܧuyDu@) ,ⱀ$8y1G U8%2v ҘFYs>#rXsTJ5Yȧ(*hVS0d=Tfʸl?6¿mdHg38O%V=Ռ uqC Y"7kDѾ[vIZp8dH*ԁyk޼,jppa\n\a, 3l~ri-sԄwOYJzĨ=/ _ ďeIJd\PxLD"ggnV @bSxz4@ki>(c* c~9Tĕ-]oǽ=i-${*xĄ@..d7"A^ @p/!RHj)taq+9p6nɥtb{.,eq,luLQ-%d91=vyUUZ;wO> S5.Nb?Go"˔(ղu[^mA=bN7gy^g@I5]alX9$*ܬQ )K*3DI2(0Y3R{#% IR?,BҘiCAU ܲ:jiIAk0t t z`cdϨOB2CUzdsx_}ʔzu^ꚍTX,zv)_V seQx̩ӗg̨qyhUv:ܝUk<1rja|BUW')rb4{5 HM>O]ڶkJ 84I vD`Rk]NM.ϳs7ȅv5G/mR17w!gҗ≓%d±(O "Qm:d;RRYS5MGIjj^j5`X0 l.o@,+Pdme!E`WY8F,z=0{3 sqeѸ2MO,q)\UD>͚j| tfgq$ۄLl$(e .KGf0Y+[5rɻEq秦}HET`k\1A_ u.(2/XkFz&DFeGD+нf$c mN5Wg}vlT^d) œ1X,jbguN\˻73^XW,"\"|/G6sd^G1K&Sb)&5X9Sˬvl>tʗka: |d֐ |F6:wiN%>O!3YrV=5?Ȋޕ#khU5duA46p1IRhٱƜ1CbTr%[Z=N `}6c3vr( 7Áa}c\W"D n`@ 1>(;Qs_A>~$+뙯];Cqw]HCZP;iTLUT̛&KdԱI0PjS's7S {}ZlD)jѴTX*xQl0p(bR)P/wdL,ܹՐ֎lsi1C/ԧقlv-~gì>c;'SR[OhўA#X캚 ڊ[h~*)Z'i&>%J^/n/f"Q5)휫1!WHu8Wq*˪~EGX}{ I ٱ]퓏Xڿ?K_L ;+'9ށ&/I"j0RZOߣ-e[jثmlRQklPms?OL߽әT5\˓+*<k;$hTAUOVgTǫ-t޷,ZÈ\^pX񷅑Y>oam=?%U=Mh&5pjJtX_n*sU{mwҘ\;I~V;qߚ(~VR{uޏ,)u۷Fj.*VQurfɻ͸#CYՀ9a3U+[|ȵ |{Ѫj>0KOR?F+X^)ݪ)ucEjTDZn~/g.9?)޽j^Pv?|<6U<0rkpc㝼M1&IIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/0000755000175100017510000000000015224505336022031 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/multimc/32x32/news.png0000644000175100017510000000247615224505336023524 0ustar runnerrunnerPNG  IHDR szzIDATxWmY\[ֶm۶o۶m۶mBmJ.{tw ߹=`su9O$N-- rP.<84S`R>iyٓ<_\?yG|>{Ewj<bFz3[JĖX xK:wUfOwQ+Ȩt^2&Ƌ>?3oY>J+!H* )BĜV֨NY;w ժ2DcK$/ǫO$giqb G3$h­;G.v%cc,]!!|* J8(Gy~U,TlA:?]M(E}y8ZSLh7T0ӇQ$.AL豭 O8sd&".f 6^>D["9e'D)bl C=l:#хiNBIjKT{-5h9 |F  2 .hOc"@I= : IZ٪L"R1zLJE/y ' *)) 4UE$~a@;V - iqN@ܧKC|L$ 9,2Gy"5NlcZi& I!BVVN Jg*V ݴ3J3AfKZ$f* { Xx˼`ҽXfvфe!=PJ?ncc[f~3qga:6yVώdk+#),ӼRI$<f:Qwá$a9R }لp*/yqvG 61a-~ҢlO`6S#$, .\=c;\ mu4gwSf<0D^YqчJ8W]z/ro %d1Vڄٳ}X7 `rYpls$W{]'~7Ą~p y5ђ>E(/DnDiS~W;-N8,y\.8|F 5hl|e[mt$ü.zM^nqg*gZ,`YKбP?slzN0>o0 x_ܤ =O3 ܛ'y^O8}[^U'ܮ$Jf0IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/coremods.png0000644000175100017510000000307315224505336024355 0ustar runnerrunnerPNG  IHDR szzIDATx+j$Q׶m۶l۶m۶w4z3LЬ͚^}]]U=8fRIR}֒̋)׍[>|G7}ouI#lȜ_zg=Sn1G/g+x j{pGufͱe8}͵4"yV"S %v7_\ɒ]n]yͿ3p_wؗ)'W}/[CsG.J :ndl)R.w MH#"[D'>qK_\TJ%A bkm~ E?[;ҫTws-]bV"]xC vˣҎ[I;m4㭍[H=u_vЅ>Mg~{Mԭ4~}4ƽ>ˣts4+ E-*h$ku{{BR3Py47in`Z $cyh@{է,,~E -Cg S"Н^)8H^͠,H!߀rrhqiq@ yLPHi.J3Tשt }}kMd](,-bpm=O$=h~C"xn _ ؠL,5.uӇsի`?s5聄[73&`HC[5'A PJc@@9ꢲ=_}3Ozsq}DkapSJnjb&!4&a028K$8&6N[OtfqZzUXb*\߅&*ښeb͸ %l?~|>Q&4jIZ;br3=P{kHٽWߴ J'ۆoMp@D˳[k2i?zK&4>ivDl6DDI3a#Z,m[cٴr2㩲s-m0($4A Z{~9 "J"OYC(:rהN5ptqm<zj^l+ ʰ+V=^N\tbѿۑDߝc\8Ǜ;ӊOg~zdžqόLS% lU/ Cr!O ѱĊab2n=HJ#Ȁ=`0E:dx밌$&q!De4򯹍XFBd*ΞX E qOgg#eB?8o,?$:"_ X xzi`X@քݢ\STpIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/status-yellow.png0000644000175100017510000000161315224505336025374 0ustar runnerrunnerPNG  IHDR szzRIDATxWvm[?o[W0ض-X:᳥gOt^յյ/‰eB RΗl13E u !e"a}xz9pJ)2qvv(V `.Ù\&UXYAܪy#xΟļ#r2ŸxAUT4^M\!\T.ʼnPF!>`,\" *KAÁ9X]sEnnTb|^E1v|Ψsf9 !.vvds .ӅmQ8~5'BlPS=).? e"0 Y= 㭨J-#"VfM#S.E;E"D.Pv1|Yg4Fc`$q .? x w.6bIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/status-good.png0000644000175100017510000000177715224505336025024 0ustar runnerrunnerPNG  IHDR szzIDATxڭW+i=Tkk֦l۶mۚii{rY̼3m}8.`# %lKU9Yd ϰ"QxSR*39'҂=Δ`+1)XGJ̏g=ۓOq=r5 NgfPWKCx{=3m`!17Vj G?\٭t3q#/B܊ ^v ~3}<˫#>䯉KϺc=[H cVbo>?sfǖac"v*؛؉coǖTss*8?n["nKB]{3\ I&5 Im3?z0R x"*`lN*V&tH&WMU`qn 0m4w3:,m.sc0/Ƒ&5EGdwre89RQ:<+ Hh(a?uhF+SjSiDXh,?9[ȓ57Dqlj˜~<វ>79ߦI&F<`;|1Z$SFT(X٬py"°pBBkr5J>ҷS&!AtL Ĩ>ݘiex2N^:}:hE՗gr1(B)hR4+g|2ZC#gDRUpq{\=  IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/centralmods.png0000644000175100017510000000270515224505336025056 0ustar runnerrunnerPNG  IHDR szzIDATxNEw%UI۶m۶m۶m۶m[nG&/;wٿH۶ݶwe'-nj/-zB\jK{2셖3zYO_k7L;ϴ͸3n~Q qsu뙁uG.j3u߿8?E_03.wR%Oy;H'kS+U¢&I/?a>ϙ?Cu~nShȊ'f~]TXzǢNf:X&d_9 a{ 9`CYINIvd>vLaѓH9q>^yz}\>K]Ȱ u$.n,<L_)!ޚnV=z%+Dv. Yv^e}wJN떾  ',}2{%=.HnGV0h3r2迏Y%*MWQߩK)b:DyFO؋XXz+ɯ{9ܟܰA2Z!r_n"2C.C ,Rl]M~m߁Rh7?%*DD\KR1T 5Dazb}|X!᧟QAoJ.Dw멦ĥ`Ukd#`T;.nK:$/x*o\}2I N,"ꂸ XD˗AJ˓ugwKNNai{ o̿sםK4Smpf]b^N.DluZewV!j0!R? fýct.{j.̥.D ;"(נ<1l!4spZAeݺ.n{ժ[莰ZB85Au4U 2HNp!tug]}7t9WuD 5om|~{<ڐ" \$x/4 YyK d9D?OJpT)Bh'/0~4)7!^O?꾫գ}Ԓ_*tvz@&d$BZ> nЗxd׎-<犵= 50p-yyazꬥ7Զ'(hC(i \j%팤|are<}1Cgv?FꭜK@}g3J)Y|eY0~HZ72;P79=&S( j^X,b!$' <\,L,-oY,鹛`I:&ՎE}_ZVÅa}U\I#a,p̗4=3-1o|ʹ`k.측krAe#k#𱦕=ɶL8)=_(8c$/Cq@詞hO$2^(0C(@пh@oF9{9 fQ>k@c~QK3>\4 ɀěS.N;Ј4|8c/ '\jcuJ\Qƿ6FGרn4]ŵl^sfzsmo﹌^Ͽ}";-6ӛ#gQ.?/N`ImN`29GACgҒ-T/sIBy}zI15I D*^lRuZC)g#w&$^0(3r\P沼}W e'`\VG"+io5Ng$KQ;ǁa ^>m5BVb473k1~5Aw(bVRdVyE}:@DOٲ10^cLѩ~ް ^WzAsHmb6"RHz$"]*a7|pe7I+LM2!H?a i.yAR[&CAyCÞmF1k@>n!Em`Hȣuð~0m=hxKIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/resourcepacks.png0000644000175100017510000000447315224505336025420 0ustar runnerrunnerPNG  IHDR szz IDATx#YJJb<ڶm۶mclX zک{Lws}] \şa z[שɸu,p0Pz:><]ƥϿ|c@48c`3b?|XȾ5 -!ƽ MP`9|h )l{ |x5#ǜ_Wߞ]2yK4Mػw/ Jv4Sl)ܬ*`iA p!pkJ^㿯T0БvAUTEFV1&^< GZpĶ9lyC[hnd2m#IFqq^z"mLj~mC5pK3Rdt:-HaY.K>"1bO<_܃.}?$HŤ2 #Me$vŔȐZ8>(]g<Mi"{, #չݛcJ`߆gJۢʀWdǒl}*zUc Qd0H4Ŕ99(zq1R{ ~%"]}ۆ'm bm$yca&f;ŀpO0o`pk]KvUEQĉgõ@0VvSW);q=7i,-8Vf\>x~s( 0TFs<txKvx{u.9!NVVtr""[5g(z8œN?b )@5VMj5;hB ww2'88:G5Wb=$Ճ;#ݧ(5{+ inX0֍gk!v3B9ɼ^/$Y&0Eq腸mB^3GǁtL78< (wB oSzj &p܁hY5Ll\_9 V4`n@buK_s @!Za]A#JR*@((DQrMĤV<\r˞U 0QP98Jv>' kd~۲(iٸ\.EFɤH'MrFrOE;P?+)5d]|7566"7#967#2 WCV!e#& I6c!BT"z| Od, 5ǚ!; E\RHGe/I `[)P(Ɩgth{=Jj#{Ri 4h[!c?BLMB(.lG ?K%cԦ m yІg}Iy'vUVx+++ y<4$UG*V1l!ӗ _߷HӁ%lȂl@R_WW !!F:Q QM<"רNHssS;^[iN9F3ټy7nDhhh@mEGMu!: k00.!gx!JleOO>9 َhj ؾ#P1l N#˰hѢ9;v 4hJSڱgϞ,Y2o„ GmJI)_EW@h1BMV)r!\EϛrZGO2/^\TT9:dY 52vk4ӏx"yeO~l߾΃_q%l;4MF[ñJ|믕Q^8y\Jm&vt32@5kl:mu--3N{T'M:|ʔ -.J9AKKPzg4݄F@t2iSvVeT`3"@8+;IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/jarmods.png0000644000175100017510000000257615224505336024210 0ustar runnerrunnerPNG  IHDR szzEIDATxڵV+[gf+4^>۶[ѷmѷm۶m^Ŵse\dgͩF?fώ9g5?,xlg1sf!C):uImx͚KO?}y%")9%/O4B oBZ~>d")d &qo(qI$(vz{HI$HRò"CNѶ鵶'$$I$c*E"]C]] %Op* OmcG O/մ ^P@P#zI َ[Yy{0?UU]Ufby쬊gҴ"#de2$qq5FrA)bm?SI`//.dGy+{KƨpN I:il xͶ O+.咡w"/DC&#< X!&`-`wY ZP)q_A$#8RPB*nU!犠""%t{ x !"^u+c5#a?ʕa]^˱a ciUix{Y("}BH>%, "v`_?ZY]ԥKo4MUUhP՘t_ʴ@*v]%'@WWGDFݖvR AsU=v{;X|aaB8H v1?/lA9\nb` qlEBJD*AFok'VB&3^Idr!>jTNKU2T^zn8jj7Dd0|H2,` Q I&BݍEYq&Vu(,bmCF(*>6а0Akm5aěO1 ]d'`M NGY!ڞH.ůFL֨GsZ*s_g[#ʘX1 Xʘ$ X& -faIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/screenshots.png0000644000175100017510000000255315224505336025104 0ustar runnerrunnerPNG  IHDR szz2IDATxp]IV 9y̌en4Pffb8/fj;Ge%-c1R߀x2+(}ju8pVU$KGFvyg|* Ha@L* Mݮ=$j⥴9pB=\R[9|ioMarc oJ=^.%%9]}Aܿ[{ 0=o "hHLsBQff~&a'g΅ {~qշ#NAAC Q[Z`\.O(9Zv"K76-x18N >׮p~W~ HUF6K ) bRhuIôic} gOTFebINSYYN3{}!Ā: n,)Z2(i=w~)^oa b /4AsyAS.)[uY Rw,n|OTf}t~mB`$!:]'0b19gI9Pc9giwȄ`ۧFlA 6^#7iMX,wVWqc[ծp`(. 3(K\7SJJ$I?l.@ȑy6mcbDrro! +ٰ.o[l=zbao|.dP{QwC;#cO"%Z@(\||+?p{ޜ!B>/\4N^C&'(֞& ™ex nQB\(w19>mKvB1PGlIt@?I w[%Vz(V C֯ax8Z |G?)5Gyщ4Yc8,h4%1j~Ŧݩr[_I?#SxMsi.1G]`з+oOHhBZ)Vo:sy コ }S g+|* mz0/rԱM5IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/patreon.png0000644000175100017510000000067115224505336024213 0ustar runnerrunnerPNG  IHDR szzIDATx̜Akƍj3l.w7SDSiKmxQFƬڶx_w'yճV-Xb (V ;@z,g {A,hժ͏;!V@m&ǁuHހF$|6 #iXB}Xz6%`V! @]]7"^ a,ly4C.$x,,1^Dԕǹ dz[Lg LI-`#И^@f ԕw Hhl'ZI&@xő"YX1CNeVK(S/d<#uJfWҥ`=`c`]LbPcdN@@4^>-s-(/ZIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/settings.png0000644000175100017510000000426615224505336024407 0ustar runnerrunnerPNG  IHDR szz}IDATxx#yߙL&H󭭳g۶}vo۵]m&33|{fYxG :vhEal#IlVV,V(fbܳG8}Y\'Hb\@/6={ \pUw1d0&D:h~BUՌkE q{[?_#G˖-UYxC˕u4 ǏhvH 6vPd,( U>lk8L^^A;iKjj܁P(Jp5\hYm v4k ;ˆ*ں8ur2NGw HpeeyiGedPVwd!I6`b n׮<@I(ֺVWՒF..TPdҦ v;8ǯrzt֝n`Q2  @,4 J gB4f  9U;L}2Yyyk޵ H$@0P__@ι NU$M@UJ?9᝜=Ŗq:p@O[!j%]M T7@Jp{隲f7Cb\,O.t[܀pVAZf NLBJ2Xt&;㗶fރ~¤cоS& RGzf:@*]F[ m8477t;Cxܼ*LyDpPi EhѢEݞD=!L@$ 12]6 _Z΃s I![e4x̶)5$صds4GZf !n2SýgBbW"\CJ^()<=wQxٮl1%%-P$d}F^fb+?a?X)d~.ڏ}pFNV&)N;9y _cZV,_ohD)گsªF͆;gO=/e"cL& Jk"[G*+?>rWv=GeUeeh$Q%^}8En+֏.Y׮>g\}ƜK/ر犬-9p` h @- 6VZfQi崉-Ne#]\=wyT%\ Vn߬5V[ʍD@DELp.w{!kl=6qфhox_Yu 4u穲j AN{#rOʼnn[CX$ ¹͵hYkPP?NiJqMit3p|CgQz)Ѯ:O4sN{T{>N $ܢFw۔"/wtk1Ӌ۵ @q|@4p0u+i,-^n?'W^sVjp@ȑpWFa.qZ+u,?jzf2^t!96)q:Ј!%E.p^$ H ԓp@CslhpGrBHȏŁJEPRҁt )0L(DMpu^5.Νǜ^{|\=]KHl<,X[hƠM5 MIn-uޘw nք0Vsu G<ńh7AhU`AP ̣rGOYЫ>j/0l!xX9Lsɩ)_`@9zW5zqCO @ڀmC$Ak{` v=߀uH``6iui.- 46%% ɝ yߓ}vv/|?|UykNR3u(P0QYHS =}ľIV7e@'Pg? S qqXnh+UŔRua3F/*;]7u)4Ij#ڦʀf;IqDZz90-w' A@mb^GZwnpgATmR,]ǝc+R7BXw c+ur `] MA&Rvh `BsOYJQ$Zl5__=S)H%t_~4'sG_RA~B^5owh0L}Ck ijh2WzIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/loadermods.png0000644000175100017510000000300315224505336024664 0ustar runnerrunnerPNG  IHDR szzIDATxVdIhOwtm۶mvl۶m4^UU?c*n2#2/%~3X@9%5tWq! ޗRU#ccnyN2kRMrpԑ,#Q5vli`rkE%N04hlv)͕K 2y݃O$@|_%خyϐ=YE΀__p\# @ɉ/~]c~G.0]kiNB#>n%}QI]ds evPw,rtq^"`zN$k (aWsIwÀ.¯G1Sp&X$M C#p4LРrC{ @>bE;·8Y %.7eL9rzrJ9H̠4S*Ni_Jr@ӣL`+c9%'wh.%S:\ZCg;5GZJ?hj(PrE-jhT̕*PH108 uK w]0ӌeFBo$ 8:rTi2஄YY@fi!/HQҾMz|m<02Ą10\5C3P]qd܃qYyi۶5*QƇoN)5de t~`ԣW+0V ûr3t*Z^/[v]SýgЮ4#؏;.9jʚl8=JH9|]7 ?Zne"8Fu )<#3 JKQ@¥<$V؎h7y9Z`nԻ0"Es2`ꑤ!N; dc>lg*UF/p]Կ}֞X.tuĨ=u M3w\Oz'R II =qGT2[?x;Nt۲[uʂKٻrETUD`ͷ^믶l3>|Yw+\o Cyj^ƧcJZ#;n~u*a(C %bu ,m-W u|q SZehQg Umc-y1s3zs/շ pCRP /*ꖑD2Rr? U!3Fr4W+A țO% 2"F>ܷ8Ā |yr43SŸlM %^ap)ˇɵ|keLdLyάz8P+PEâ::S9 #>*2nn~ωIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/copy.png0000644000175100017510000000141115224505336023506 0ustar runnerrunnerPNG  IHDR szzIDATxĖMJAɄDo2@WZ Je6l;?S` +3qM?#9wK]d~n}QJӌ@kdY^w ?BaxHCs bU 9 /npܤ ڂS."h4 *x/#ALPe?N"tuxL-(DW8HTL ?@x*z'$ 87AL( c C\a[[Hj_)z88,K=- Hp2`|k|o¤ICv9 {gYN0}56X8o^P@qFl o?~bx9CJb25CsGf,Gv-`;G20]~}P㄄"ÉJ]GgN0!rW' P">/[$rH6(M KK&㲜`.$}~GwgG]:`r:>H>JHрw{~BIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/bug.png0000644000175100017510000000304215224505336023313 0ustar runnerrunnerPNG  IHDR szzIDATxŗ4Fשm۶g۶m۶mwmx յUt&=˝> DITDxx%!(=I="_#2 %qh( {ǻ|wp'g|Gc6>.X,Zݶ-r-/`dbgr.KXlBNȪ;찃m馶&<og) 2gJc#\FEAxb,?U~N;,]Mkp\xpd9 ♄E%f~ 7~2Cx,b& m'\*q?܄CK鹸P>1 , b:gxmip/(/wt$$1ӟ$zvdc +w\tKR.AQI!=Hõ2 $^,czLG/} _D&EvHbVsC˕u4 ǏhvH 6vPd,( U>lk8L^^A;iKjj܁P(Jp5\hYm v4k ;ˆ*ں8ur2NGw HpeeyiGedPVwd!I6`b n׮<@I(ֺVWՒF..TPdҦ v;8ǯrzt֝n`Q2  @,4 J gB4f  9U;L}2Yyyk޵ H$@0P__@ι NU$M@UJ?9᝜=Ŗq:p@O[!j%]M T7@Jp{隲f7Cb\,O.t[܀pVAZf NLBJ2Xt&;㗶fރ~¤cоS& RGzf:@*]F[ m8477t;Cxܼ*LyDpPi EhѢEݞD=!L@$ 12]6 _Z΃s I![e4x̶)5$صds4GZf !n2SýgBbW"\CJ^()<=wQxٮl1%%-P$d}F^fb+?a?X)d~.ڏ}pFNV&)N;9y _cZV,_ohD)گsªF͆;gO=/e"cL& Jk"[G*+?>rWv=GeUeeh$Q%^}8En+֏.Y׮>g\}ƜK/ر犬-9p` h @- 6VZfQi崉-Ne#]\/^e a̠b "R5<w; `L"b* u(5_u@qV&kh뚷HF㈝%OǤ']<;|Ү|#l10M+6egnjj |q}0)1߹Fi+I=lkRUh$H<FpFDpsh,ZKϷ-`K&rj)x@_] cxaZ*"D;Pj]eի/0%;|>:q/ j? ,\[3VYpNg4' `U02\l9oʽ ?\xތ6&?lke6e܎Oݎ͆IF t #jvL*r[#J'>"r$%Q3Dj&`YEfP&á@)0#9G 5&ᷰFpVs@|D\8O?0߉H jZĠ0$P{nj1[H=CXi5޽?dVa:<IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/refresh.png0000644000175100017510000000357715224505336024211 0ustar runnerrunnerPNG  IHDR szzFIDATx%ItRX{w,o>OmpKND:ʳ9JC86"~6ުէw qy3ZehbJ3I8_~"g[2OΧdEx5gs^4 LSW{*Tƀ,F k?C ,A5OS 6wb'? @B?Ϻ/!u) O<~߸qcw8.x?^@x's/iNCt[zŒ$ g!?~ BDZC?QqZj;?*wٶ_lxȆᘯ1:IBT*笋đs؁UKW1p[Sgwi PW f^[~ "4h6?, ~ 1n5E;+Yg֝~iģ=6]`;Q(ulmkc;lWK;Bd@Zj[1@ʨz|k, O9;>3kv}@BKuϝ d? h6oą/:[*:z&QT-H]h4K *iK9z"Tm h+|C˛F_}BrrVFde NUdbTAjBM-@WZMˌ<˱E|:?q]ʍʃa CDhш^?A(0unph1 4b.bph_"a6YMrSbZ KN~Q6DS@YZ(EO'iEz%)t_v[[vVW4jq5̗:FYaP9^Clp zP8Z۞!gJMIlomɩFKbY1[@K=xJq+V8]cr]rD5ʕە]yJ>4&1r;Q@}@3w~/ JO瞂Qj5p*xVzI$n4aN%M DTMu*k,VL|TOFS{cfk(- Uִw3&ON<;K:6VJ"%Oo)mZ7$ 7/oS]gۛh!(yЭ(d+r (Dn_9hwnڲ34sdG:*bEӭ1_U?ddAAETEK/tz¦$/(0?]\ude*/3%&,il& -yP`}GaN9*4Nኲݎ_L0FGsύ3Wu@*0=F4iH#KMN;mbo&ꇁ?V.:kѢ6i*g4Sx.Y`)kMvB W)vV02W=}%CJa _zS*YX6L? l9$.ތ߱|E.lߍQ6ñEٮQGw܉sg~v7F3ߛjd% IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/status-running.png0000644000175100017510000000202515224505336025537 0ustar runnerrunnerPNG  IHDR szzIDATxV$ib{m۶mFοm۶{gU^[cJjzM\ [غFՌ"hDBY&w!#ݎ'j;AL ՒՂJ\PJ  $後Go*g' w,^:$X7!4%v6O ֏ /" Idn*sg/>9 G))paW@L>=G ;zfCFFev EK4zm뭞nAro1r?\MBE6Sy#׳KY. q39Xl+ 0UnykNl7i fSɑvi |m[_y䦏ֲNR?8ɲ^ CGg^cM@gx1E֏$Q\ t6 j0EE|Gd0G2շޕߘv\=P<[l'HeρCrS\\b02&9"m (`IARn.t0* QjC+ +áxy|B%@"2E,o}]9B+̀#qcqMi3Ϩwew3]Ò,Рșַmf|߫#,31Nq洘W[w[VtIqvݚP:|>b!``mB߁3/@WKB1WMǝ~nܮ?i@/)es"7v]@0wot]'-?C.m)׿kpl4%ĒLl/"3ʄH.g˜W _C嬩Eha}sKs-/ݺ;|@pl̈"B,("1.V=tdo|ӡ#,^D#b;`GSO=P2Χ>&,Y 'FdLG:JHD*\:#)N5h{w=74&"a8'ӻz'Ö1`$ob{ 9SpfgRӃrzvK5* 4Q=!q3NЩC}C^A Fz.μu\S@9NiDOm/5rؘI /۱m46S:O`lJ˃q`]ZÉ7,%D mJޒ}Ūv |݃FSFw70f }>ˆfb])Cx<.嚫 \n?d,py4{3gw GDAEV"jZ>+ }!DPR1q~bs٤_4c<0 MveZ*\2c\ez j/'5yK+R5f* b]P; AR e)tE'^xz[>.{M:E4:^ڒ$1PEAKh!W$,rLdë؉_UVϫqٽTJfMLٳ Fyh U^r#*DbnsgMڕsQRVyRjx8U9vLeH# 1eѨyPJHRP_]a#GVdB(" CCwP[]v7%qBө{j& E,{Av$ 4 Oڍ/brk 6:[,bjXx޼<;w-)Rqr1]0:$@'E[(ML.UJETG4m;\#4h} ѿءJ0YqXwN՚ Lgo^L6_@{H҉' I"T=5eGKbJ!a l,O~+@Ť$g ޒI'vZ}zRE(C;lY@g\[յӯ :/jtڦLBۑ1f P!rV,7zS3ߩsT<Ý,;hڭJ + P7t:r5ќәK'S__m~!t/72dkx4YYœ Dz N+KfDTΚ g PG '.q.kH&֮̎֨t֛eQ@D[p=qMsL TPu8K׿LIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/worlds.png0000644000175100017510000000414015224505336024050 0ustar runnerrunnerPNG  IHDR szz'IDATxlkٳǎ..(*3WP+VQXfffA'q4ԲQ؟iw0^|D-IA$7/])-|C"AĂ6|{gHQ I@(*|֫kVbaJ$(0>=(V@ 6 FT@D0&h$S" 'u+i659WyOWիx @z *Fed[ w2@$ZB?WVIL8E0G ((`6 w@-5lR`8M!u,?S.0w}K,/RR*~Nݾ @qbU6s5FYBZRP~[ 1ڧ?7i_CeĀaө HPوLJi{ss5B~fj C@ 2}u!RVs5Y4ed[?=$>lVV^-cz55ivf 5@5N ӗzxU *8V@hU_-+4`\T+`T!RpmuvD?cjV<+F5FzWR.@ b mLIZ(ڜb#jGS1Ccwo`~fKL.1}+?L 8@M,sQ"QęoOgN$2dbGٳwW6pxʭ%LLh7?Y|*JhJ&^,.0+ vdql(WfVYxQ#η1i렦͙5Nfm̹]&" F>~W=@W\.f,`R$ߠRK(anjaSŦ兠:pQ$}+X ,D\0ZamH7G/ryf3&"mQ#,-c4Fbi|:F3oYnf^ţJelو= Q6FT5:z{ [X`lB=8TA xP@nNM\Z (P8T=|g푰TflKoc 4k hD& TUK6Zwj).w6BGRmEqi1Bd&}c<f6PCmA}f"3Z\ PD@p6ociK}9e-UZOQ( H`c-]?+tx2ƴ " f/4О%? ~;7Aj +cw[Ʊ+x0͍vh"Sk l6F7MMمv~|eux6mϻ?䧅 x o3q1 (ř"6]w_5 IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/log.png0000644000175100017510000000141715224505336023323 0ustar runnerrunnerPNG  IHDR sIDATxڍӵ,Eᷪg+ %""ïwww>3-UǾ]Uѿ<ڝ|0~z;|`k4 7o!KaX{:=5*Y!˂lj 5FDŽtGh|*jL6 5ys+~hFmUk"8ʒZ9Hã=Eg#JZC A$ 9'k< ֻgIf f dѾWվc@,SKQ,JV0BGZ54bpOB4-LDs֮Ba۪gQDCɮh(6cЬsuRheh%m_QSXU JSmS,KjCɎY~R5vYp,8}ulH8fn 5Jm#v#Fs_jKfd[`ަYSZОL eK9~F m+%˾w_ђPkT-,@p2,ˀ5ȶ|5̨d;JZYVꦰ<$Qo'#x_00k~RZ`A0Q!:AgÊmcÎP Eɮ`Įc4dVZat#d5~u$QV7K4 o]-dr:[Yp$@!2sXxs>/Kβ'9: XDdS^Db3WIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/minecraft.png0000644000175100017510000000440415224505336024511 0ustar runnerrunnerPNG  IHDR szzIDATxڵeH_eK oO/*Jj8FRM|6һD7SI$IHӔ~꧸geejJDE+N8 `t<#htڱ-E~8VE sssDE>%2Hܳ!6%,zw)yrxg)t:ՎR>5" /;_DZO,t >55J!*>99QlF)ޞr֎9 $9ggi$s2[$p͵e‰)%{vUQqvzrjFZtMj /TԆy/ \9ܾ ˁ򤋑 %F+uEhpW=/B':uQSpbc-ωö=р yNEO\.#UǏW"hY\}8f䥸51kh3}n8R"T^)MCJ;88P4|fh ֿGs+r̼C~(aL@p5]ҕs?'98Ho DA6^k(p8 $%n; 6Y^KijspU4)̵*-mVbM ޮ*f~r4?gU쒍 .Nooo_}[i`ϖh^B5)uA3_~)mOݟ/%"g0tF_C,=K t[/&jr:Y Ac&I =ԩUֹ ^L eɍoY,p9?!u7(I˘֠o$,KMHN0 4W' ]'Nq3UMuP$ĄiBX`zeF 3'gDS1&4tMc)b:[$ٓaH\6TSM!0SͶ8 GRbNrDʔ]M7. j3f6B"4>!}V)ZV!Q" ]H11rFxr~g$DqMAũ*1- [-O]HQDlfGueյhF1Kg֪%+QmK.P +gl<eab ˻@M3~"~3`($aZF C !A.ƘAy^wT$&vg3=.\VvmQt\9W*LQP,h$%N{.Fr'!1r/c/Mٶخ]UwB&L9=d1ݖaKT(RWTfBW-08^:qc!6;Tz9fF ^@qrP&*xT3!4~1.Rqii_0QI _4=Yzd@bӑ6(z) ́8[3:ndQ<((ws yQ)=CP .R0נQ6^|\mP4iyQ4 Ks0 &ni+l3EoǠ|y z"ȹޝ :C9ϯj P"#`2prGif`+,_{@3ʆͮRU:PƢ%-&[ H|Yqy]O v'uf w5Ы_`Vr#:8#XЭ?XжBRO. 薨^Ms+{ŝ@̎)';Mٿ5;~x|Ooj4m*'M J劇~``D"ov 8Cc:eB LiTV1q)Pڏ*OJ1C_slre-b|<5~ 0Ray>gU % b)+TȑX<,eĜHR`5 5{zr{)E&pHK~L'(1}I H @܌MEz4F DI$x>F&f$䠗!(@/FKTÇ/ QQ!Ț+Xifq)z"FzDE7qDi AIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/gear_legacy.png0000644000175100017510000000433615224505336026776 0ustar runnerrunnerPNG  IHDR szzIDATxڝtXӆd\n33333S133/333P!I1mI=Ys^Ռ^뢪oKͯ/y x! uWœ5unW]yb]}|mDf@y׻E񮀁ͭI{: ?Bџ@T]P?Gޕ|.CUQkhHg# K.&9wsL1SalD7d5敨T.zKSS$DA4Yi[k˜ND|՛9C)X܇5C:lI҈Lt7z"Z3jMpI(jŹj}M5jLcz2b]^o·?lyn ./~9ZO/^W} Ag욬+vc-߷}?] m]_Z9;)x/\u_s43?|Pi}[nzvY٦ȯ!oD>SNnfx*OH9_Cղc}T>̽!uO;䩙'OTJQ?ܐ.ϝ7|-P rJas1EB\!wuS>:x:ynR!<]N( DཌYoDfG.Y#40~-;ᙜŘkdyxЩ%5_r/)3xN~< jrUmpz^+ 7uʾ Giqhڜ\ӘlF;OY tC D2J : XѦ&$TNB#GDVKI<ף _saO9N阇Vn\B& Q%T%C eW=4{Ś|ɧႮMkS *~` έ ؟2[ζz]r~\M&sWW@Fѯl>{ 4.9/] ū*ڰxQ?>/+I:%8W37vDigm/o?]+NƂCU 'zc|.fc 41uw.8 X !p.RgC icᱪ^N4,6+CULl,/˙Ph-]qQ7џu\4, aE3[DA5&vKfˈz?O4̗ڋS;a>Oy ߟy_lر&*Ύ`)i 7 7x:7޿mm>M:06#nû$fSbaMwTc:*3P,4=BcS.~ Aј!y*=Laۮq gŃcIX_ +,KInE幗V%KΦ fԚuzO0/Sd7۷u>"G%b>RBo>i*֯֯g镋\.8`qYW5> d@i#QXWwk-ǚ(X~4h6u5Ax/"O( WRN تnr*PPF=L18B+Ix1 lgb}cKu '{'C'x~_[gPP |r77^}$:n!`9Kz{/Ukbm[â(b``VU=*7fL;i4-u+k@ݏ:pX?(NR7|u}HF 5&Kbj5!&1GWrk^0(/$6moE6M.eX{Lbf[qfY,,ЅxٛхuWS R^1Qz5&m6%Qܑ=c$crEuCj|LL^] l^-n>ix#{1#$؈c.?mO_[%an -ҧ"8To9 QzIͰhT,]D&?OSꨲ _7$ԻTtƦ`⃹N)v/bw"ouϢ[)Rge?}_HMC̕q_{(ߏ uԕެ$qLBp 8[ M gBMuԕVg>10 #E:ʎK_*T?H=+ΜK ?;ɩx%/Yf vZv$hfUtlϐId`"Oσ;_No 0z˰G96Ù YӳDߍ.~ b SufLM6W \k&+xu QhSER0ߏ)<+XKD$K(ot~7cZoNAk1VƔ*m=]J"jiɺǸзp ]"t@"8C`SP| \~8&rLqy<= ԏtxe3r|sꋷ%D̈ث1i%NG3]|Eyl~ApD{q윉9*Rm6t9 y+JI ֪(N.wb|y"Ps_B_={'ZҼIvţ9| Z1`Ǽ +A~-V6aœ}%;az>s[d'.l{WiOؘoV@j>;ms /c"Va'M+Nzmza1 \Ex ( @q{XZN:)Pk=M>WM0[osrqfšg~YnPH/d@JpR^ETk̶:k!-Nw@xhm c1X=Ͱau6owI,K$̮1ZBźJO=.??H㓇$n }!m000hI8).5&e=&b\?-5З?>v9(u5 B.^1;=/Is3+*RAM0615/ |xijEC.853I5|5_;B]IZ[ -R -P,:GeH4 vf ,GrC8$cX$k6X ّl|??ԒB{3bs3gP9ы IEA,bF<Sc}H^gNP*P%{̙͚wy ŬZph?2Vޔ"&="ZUNz6ӎFC̀ok9|~(Y@0 )E5IbN>I?ʖڙkMBx%m8;uww5txU^Mi7/Ʌ2TJżHYehA,bqw ,~rF{z`- s SA- >tP-T$\X]bUsX$hnb Z%E7Fp0#nLZ[ j~Ж0[aqKrI Ee,HvKB e)`{ %0\|54%XL C^9,f#\֣(@sdgd Q҂+F wU`֤bmVaˤ $: "zqkآ$1[] (H̎'ŲIDATxm0 `  ` ݽ"M4e3+$Eak H޲UۃKڻmAP@\-@%\WSQ[Xw5gV@[KW@79Xn3R?IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/skeleton_legacy.png0000644000175100017510000000067215224505336027703 0ustar runnerrunnerPNG  IHDR DPLTEUUUNNNOOOLLLKKKJJJMMMMMMNNNNNN@@@333Ϻį÷ɧ||~~EEEIIIAAAu&tRNS -9:-IDATxӳ\a?S["ڿ198:i$9]WN.i4eKF8mU4\iW ZPQQW@Ƭ)^bLcKqt t:a ZV"YU=M>T/E X1Cps%P\ ʋI*;4IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/steve_legacy.png0000644000175100017510000000136515224505336027205 0ustar runnerrunnerPNG  IHDR szzIDATx$]Fضmmf-X!fљ;QW~.0`(b?QAT<"B 58 =l8cڤ7h؀sv Vx^6!M,-S{ spRiD sZCk#p-ٙ׮k]KĚL 5H6^x!#P2 Tj ɶ775Oߨ^n,t68YE6A uYWg,-';mO H1quH6!@{ϱx*pS;jxQS1\[7<<ؽ0]f34XSr19t98^7ҦNhZ\]iZHӥiN8W\Ud|GH'K}Hl-]A"-6RFeNR'7)ډZ(qViޤNI ï5 fKF E";\'$Dھu~P z,TB ,\܏ ϱO~7`2kP%EYW䃋NlOEGay;[)ז;@x#]T3 DB*FPtA oR!+Vw>U7<:>@b.wEPN.Z^Xy?j:;)*fai^( nLho8ptJMV#[&2YJEN?u'< ^LBCy<5Jbz^ e?Gy @PTB9pY׻aA̚J"W4CMgVG2=j5)<D\gѿ/t"2@!|ȸ~6S4` 9c/w½H(ƫ?爽R6{0 .[l7nk5|FNQL|r|S㽭Ąv(#f"B%]d`IioW})`" hwj%a=c/e"'=nd?[2S1PHmZ* @;SQdU`/4>HDۃhxnaaRh/^4\͡{oQ3PA M$C7~ l2~TԱrMaDA>t MMQ-tޱٜ]G|:pK߻L+kjѸbڦNd@4I8:$`]+8f]aнckГ``C¬U_.eo+V}o_L~aQ3(zqGcXs gȥ( ]gц0i7npm")G#f(2d}j6!2Qc ^VUwuM;dpPaZ*aR3+Ce u@? RTBYzs,*@WNMMpscJ(Lfh& Dԗpձ-c6 DU @Bdu"D? 4gPɸlަi,r35h pM4]%+)g!>ZIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/netherstar_legacy.png0000644000175100017510000000226115224505336030232 0ustar runnerrunnerPNG  IHDR szzxIDATxڵW,I<۶m/ ۶׶m۶m۶=X],z013oUOY#0;\iiiO^SSu3`jll֪e ? J#/b&DJ=EP86㕕uo0ͭ-\".3< "(6陙յ5jD R½ "ttww?{EE l?gF5'ǐ{)DylN%RlU1pO0tBBxZc?عG\]Q31o:G/GcBv]x2Lv[XH,xc0sNGPӳ\]]%WR)1ޑZl`8[\'d& Ș 8XV[^F+,#L11úCˋ:9=;; t#ER,,VDžx8@7#X^u5%QeK .,T`8I:66uoQ$$ɪs*+1g{a[ =tpc@ Xy55ҶY٭ HII(:#Lȵ*ņ|#N0}EK#kQkiIܡKZZ,`[j$Nܽ#Ku A]@Ull(2#fH/]PW"Dە55pv A+="t s,"jqQ3mOpn~sT9Bqm<(.ݸAo$tuwX6FCb1s@A!{ϩE Zf^aaKrB.r,l R?0:: nHNݿ ېDpj][[>oKfŇS䖌~11"J4|cPgjޔT;zpS⶜Tqa)LX'q|߈9^IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/planks_legacy.png0000644000175100017510000000034215224505336027341 0ustar runnerrunnerPNG  IHDR TgPLTE_UbtAM~b7gP,l IDATxe1 @Qk,/璿-Jc= ${bRr@^dG ,9Ėx $A!Z349iT >DĢ~gZdZ6paXWIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/iron_legacy.png0000644000175100017510000000030015224505336027012 0ustar runnerrunnerPNG  IHDR IDATxՁ 4@{ C L`O ΥXp.c@iD\(V@`|1yff-֑?ZkQlQFҾq-1.4rLIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/stone_legacy.png0000644000175100017510000000027115224505336027202 0ustar runnerrunnerPNG  IHDR g PLTEttthhhDfhIDATxE!0 CQ^D$)Ɇ&J 3p t=PM!#"h@{?t=!CPku >3# 5h_wfߊIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/diamond_legacy.png0000644000175100017510000000034415224505336027466 0ustar runnerrunnerPNG  IHDR TgPLTEK=epIDATxm0 `  ` ݽ"M4e3+$Eak H޲UۃKڻmAP@\-@%\WSQ[Xw5gV@[KW@79Xn3R?IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/magitech_legacy.png0000644000175100017510000000470715224505336027643 0ustar runnerrunnerPNG  IHDR szz IDATx|\ݺw=d66Vm۶}m۶mS5{9Js|jITD"!G0}ʳ*ߑK^Rŝf)4Rjd9>Msn<qu4Qq%|9.i} EFF[F ?rԫvIV3OuvV*/rֲ#N5}t;5f&9w.YozУ6\㽿^&oɲkiZ[^ ǁY%]C7Pih ُ{ؽ;/+0ps1n7g#}'W<:Kra}^@yyyJsR+26[!xFZ|-Ȼ=o~e~ssnTpX:D-,`'&8FbLp~A`BN54˒G^/4g>ٿEw:tuؽ-ϹޟI|O 6-ᦧ;Gp4c?#ئ|Es $HPjc-@S1yDB$M@ w2bKPjrׯ] ryǙhE AVKL΁P`Ri&NmHnAp2sSN_~g?" |; ً$Q xo)1ui6uU#_m8fc0M/dQy?1򉏱0ObL{C 7P߻7Xpa^bQ 0h ?|+?~ˍd ,2n nEBkу>&}sS˥ ] O7\vuVmӻ [8P*㏞/*S cIR\#x||jշh%HUH ﹲm ʸԹ=^4Hc,z, Œq*X 8]ą9Llf5w+/~/WG& Қգ᱃L-]׏=P o{,˥6\E^?ȡYg7Kȫ,9UiVXoFyp"]=!Y7Κlu6Cn==f}ёMYW]Ə7~{/tZ).r DZg.=WcQŘ7;UzHug¿nGdvǷe];%I=;+j8,fKS)U|L\|1TϯHA슚A|.wj޾SI%j骒 Y1 3Q}w9.T_>-WFAҚ:Ed&uU$1{mX[/L/BYP3P^>:J79 F̜cbejhWyq13S&Q58DDqؤH.NqT0fc!IBA)$p_t HA5s/W^?̛UYQKٲ  P&cйWNMxVC߅_M[oc &O\H>;T͖D2@(K{{dtT*ܱS~E՚"]Q]*Q˅>׍=Uw <X>< !FY]nwMMA6|ujE 5Du*BPFm ) !chZjM΀AęO#Ws{KФ *VA깿}`  7*js>3Q& 6E?\ZC;8-,| (;2 XkB}XU. +EMM."W!2rQ4ҫ Q8@1&N2ښFդkxǨwLZgs:7,9$X`|wICIN)<>fbLfCI) ~}.?6 g<kJI6FXAsn1t)Pr`aX`FjkGn!{ZiVx" h/EgNjuZ38P=ְ^3sf{1瑙EGt?Lߍ;FvIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/creeper_legacy.png0000644000175100017510000000252115224505336027477 0ustar runnerrunnerPNG  IHDR szzIDATxڽV,9Y<۶mpYһٶ g۶{ﷰLrIOWڝ7=NJ)͚h>Ocsտ9`B Ss-g{ iʴu^pviՎU_ԸCjl{!g͝edʱl 6X//] 6sَ^_bv6v5o  33 YԘ`Խ;=05ט۳uhIu|䴆]b\S#F pL ]m'cXx!R(FyG&DQ3̒$iGˌ}Y9.IEy(QLkjq6#NYfP# ipAgaa^c{3ٚ'9MAڇ#gCGLJ]ӵB.Ys5J?gz!Yەm !DܲWgKpi20lbc7"7(S k cl ZD[g;2_/j`9lI?)iM"73YrJ#F ;N nͳt $QcGkj=o8B)wӯMD\jmw,'2a|p̔8lvIMNK&tFOn: z @JRk!#?!шvqVU2vtu>ڱ)UE0Ȏ`7@l*Kl_ml͆ ϩ= Զv{(Z0o _jNx_q+w$R.0^(@.w5xdQQG +_!Y&u.V)8r14+GީzX&ӕg6XBkJLWdt*b(#l?g#dpf0٭,BA): (t@Ͻ|T{L,)qieFTB kT֠% ٿ'҉Q{ʱb3&M? qSQ2IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/ftb_glow.png0000644000175100017510000000275315224505336026340 0ustar runnerrunnerPNG  IHDR szzIDATxstjwmSFyǵ62m۶m۶fݮqɗ "@FdVtIUPPL kZGgȳ>W̋!є?6"بZL뚼V鵨\#=eޱg| ,!,Oy`VF[w/5hp5FM<ɠt D׼U$q(> Bln%C*oչQ@NNG&yyQ| "ZpW LAj ɚUv=\ oY Z4)fp^;B@jt~Ko^:QffR:Pg d={> V eCޝ=H9:ѓHܛ鿝et X {BwC,dOHLBXFXNI8E8MFn= m3`JUټt"Le̷A6?7| K(oĕBd nL ﮅqք4B[*2"aoea΄ 5leh0(0 د\+gz~/$V>4)'4 ~vE槼 3]iqj*uD45usQ\ t8k 'a4Z#ǀd( s 8 g|,!~cc&`I1WMƝc]{Ty֗OhdfŰA%^&>OHJ"הs~O1_b8;{2HdeBr\e `{X:J}psAg)Z2h_px5lox1Ou3HSJQ?NL NNB} z@z%b _xg I` s(YKeʬIp/j@ԹLII).n++"b,8жo&#M8\u6ɾf ԡG!ˋd;0Q&5O\)g<ժdC`w[`glÓQ!#!࿞HLmP!$?;jA@X`<1 4'dlT_0hF!`*iWd>]|$|P.]$bK ?s%gxuGwЀ+S2.0dV;!Q[ҮN|kܙՂUJ#}6O&"f5D-f(%`$uz4!"ٖcrN+1wVAIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/brick_legacy.png0000644000175100017510000000034715224505336027150 0ustar runnerrunnerPNG  IHDR TgPLTE|E6}hQbMngVCP?s?1tIDATxeA 0 MBJL!{2MG8w}$EtUٌJŽЅ" @i_r>˅fk>n"]`کt#]l{.4vQK\<*gNIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/tnt_legacy.png0000644000175100017510000000034415224505336026660 0ustar runnerrunnerPNG  IHDR D3PLTED>84<4-81+x%<7lIDATxk0 EObZ[3=Mrn"^h B'zd ۄb*hg\ܲ LxՅv?zXIϬ ;} mIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/32x32/instances/infinity_legacy.png0000644000175100017510000000244715224505336027712 0ustar runnerrunnerPNG  IHDR IDATxŕwkiOj:IOvjۈY۶}Uڶm3wjνŰ3YqUt8Qr4 QxH VS"2w02D8䛖%a\#wQ}$uGBPq"Q?x0,$# "D4bs}0,h }@Ҽ"El@t4eA0e;`Ò֞G \A^# 8Bgly@{X"a.!{^`O4! ]?8|& " |7H-%>u@E#k^ۯWWO&,,,))IMܳY}^0]P /> wRϏdӨ0Wd~"0u 7Us+KiH0bȬM7, NEI^L٤, 8"YeCnPd.gG1:-<PI;䖝z.1 Ҹ78,ᝏ4pb/p 9yqJZ͉bxh'Hɵ0uaf-l}Fdƥjڱc+XACXTg*li^xCՊ$7IiqT +Ó޺OqBbZk;XEic_gX%%3n*wubMustx@Bkҫ3"~1~d?%?h6*[yeђx6uyf@iVղ'UjO4*&j1mL^=<Ĝ@M6Kk 4::,Z" OEQhHVeBUiS&^z#FLa2h&mf*]_+4H{E^WTcBXV[WWc8#YYJ4SV}y֩P&b8c`kA]ld:pFUbgW-`6lllqGyU:lJ\FIDATxn0 C=f&1ocE7._qX` %*&Z9'vO8n-v5 X5~uWyNH66DDRsJ9"kujR^r&'JJZd9w2e/77I|qN3oεz_ L CnV } zȰd'o5v>9IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/multimc.qrc0000644000175100017510000003420515224505336023435 0ustar runnerrunner index.theme scalable/reddit-alien.svg scalable/launcher.svg scalable/technic.svg scalable/atlauncher.svg scalable/atlauncher-placeholder.png scalable/instances/modrinth.svg scalable/proxy.svg scalable/language.svg scalable/java.svg 16x16/star.png 24x24/star.png 32x32/star.png 48x48/star.png 64x64/star.png 16x16/worlds.png 22x22/worlds.png 32x32/worlds.png 48x48/worlds.png 64x64/worlds.png 16x16/minecraft.png 24x24/minecraft.png 32x32/minecraft.png 48x48/minecraft.png 256x256/minecraft.png scalable/bug.svg 16x16/bug.png 22x22/bug.png 32x32/bug.png 48x48/bug.png 64x64/bug.png 16x16/screenshots.png 22x22/screenshots.png 32x32/screenshots.png 48x48/screenshots.png 64x64/screenshots.png scalable/screenshots.svg scalable/custom-commands.svg scalable/datapacks.svg 16x16/cat.png 22x22/cat.png 24x24/cat.png 32x32/cat.png 48x48/cat.png 64x64/cat.png scalable/centralmods.svg 16x16/centralmods.png 22x22/centralmods.png 32x32/centralmods.png 48x48/centralmods.png 64x64/centralmods.png 16x16/copy.png 22x22/copy.png 32x32/copy.png 48x48/copy.png 64x64/copy.png 16x16/help.png 22x22/help.png 32x32/help.png 48x48/help.png 64x64/help.png scalable/news.svg 16x16/news.png 22x22/news.png 32x32/news.png 48x48/news.png 64x64/news.png 16x16/status-bad.png 24x24/status-bad.png 22x22/status-bad.png 32x32/status-bad.png 48x48/status-bad.png 64x64/status-bad.png 16x16/status-good.png 24x24/status-good.png 22x22/status-good.png 32x32/status-good.png 48x48/status-good.png 64x64/status-good.png 16x16/status-yellow.png 24x24/status-yellow.png 22x22/status-yellow.png 32x32/status-yellow.png 48x48/status-yellow.png 64x64/status-yellow.png 16x16/status-running.png 24x24/status-running.png 22x22/status-running.png 32x32/status-running.png 48x48/status-running.png 64x64/status-running.png scalable/status-running.svg 16x16/loadermods.png 24x24/loadermods.png 32x32/loadermods.png 64x64/loadermods.png 16x16/jarmods.png 24x24/jarmods.png 32x32/jarmods.png 64x64/jarmods.png 16x16/coremods.png 24x24/coremods.png 32x32/coremods.png 64x64/coremods.png 16x16/resourcepacks.png 24x24/resourcepacks.png 32x32/resourcepacks.png 64x64/resourcepacks.png 128x128/shaderpacks.png 16x16/refresh.png 22x22/refresh.png 32x32/refresh.png 48x48/refresh.png 64x64/refresh.png 16x16/settings.png 22x22/settings.png 32x32/settings.png 48x48/settings.png 64x64/settings.png 16x16/instance-settings.png 22x22/instance-settings.png 32x32/instance-settings.png 48x48/instance-settings.png 64x64/instance-settings.png scalable/viewfolder.svg 16x16/viewfolder.png 22x22/viewfolder.png 32x32/viewfolder.png 48x48/viewfolder.png 64x64/viewfolder.png 8x8/noaccount.png 16x16/noaccount.png 24x24/noaccount.png 32x32/noaccount.png 48x48/noaccount.png 8x8/noaccount.png 16x16/noaccount.png 24x24/noaccount.png 32x32/noaccount.png 48x48/noaccount.png 16x16/log.png 24x24/log.png 32x32/log.png 48x48/log.png 64x64/log.png 128x128/unknown_server.png scalable/screenshot-placeholder.svg scalable/matrix.svg scalable/discord.svg scalable/instances/chicken.svg scalable/instances/creeper.svg scalable/instances/enderpearl.svg scalable/instances/ftb_logo.svg scalable/instances/flame.svg scalable/instances/gear.svg scalable/instances/herobrine.svg scalable/instances/magitech.svg scalable/instances/meat.svg scalable/instances/netherstar.svg scalable/instances/skeleton.svg scalable/instances/squarecreeper.svg scalable/instances/steve.svg scalable/instances/diamond.svg scalable/instances/dirt.svg scalable/instances/grass.svg scalable/instances/brick.svg scalable/instances/gold.svg scalable/instances/iron.svg scalable/instances/planks.svg scalable/instances/stone.svg scalable/instances/tnt.svg scalable/instances/enderman.svg scalable/instances/fox.svg scalable/instances/bee.svg 32x32/instances/chicken_legacy.png 128x128/instances/chicken_legacy.png 32x32/instances/creeper_legacy.png 128x128/instances/creeper_legacy.png 32x32/instances/enderpearl_legacy.png 128x128/instances/enderpearl_legacy.png 32x32/instances/ftb_glow.png 128x128/instances/ftb_glow.png 32x32/instances/ftb_logo_legacy.png 128x128/instances/ftb_logo_legacy.png 128x128/instances/flame_legacy.png 32x32/instances/gear_legacy.png 128x128/instances/gear_legacy.png 32x32/instances/herobrine_legacy.png 128x128/instances/herobrine_legacy.png 32x32/instances/magitech_legacy.png 128x128/instances/magitech_legacy.png 32x32/instances/meat_legacy.png 128x128/instances/meat_legacy.png 32x32/instances/netherstar_legacy.png 128x128/instances/netherstar_legacy.png 32x32/instances/skeleton_legacy.png 128x128/instances/skeleton_legacy.png 32x32/instances/squarecreeper_legacy.png 128x128/instances/squarecreeper_legacy.png 32x32/instances/steve_legacy.png 128x128/instances/steve_legacy.png 32x32/instances/brick_legacy.png 32x32/instances/diamond_legacy.png 32x32/instances/dirt_legacy.png 32x32/instances/gold_legacy.png 32x32/instances/grass_legacy.png 32x32/instances/iron_legacy.png 32x32/instances/planks_legacy.png 32x32/instances/stone_legacy.png 32x32/instances/tnt_legacy.png 50x50/instances/enderman_legacy.png scalable/instances/prismlauncher.svg scalable/instances/fox_legacy.svg scalable/instances/bee_legacy.svg scalable/delete.svg scalable/tag.svg scalable/rename.svg scalable/shortcut.svg scalable/export.svg scalable/launch.svg scalable/server.svg scalable/appearance.svg scalable/about.svg scalable/new.svg scalable/checkupdate.svg scalable/instances/quiltmc.svg scalable/instances/fabricmc.svg scalable/instances/neoforged.svg 128x128/instances/forge.png 128x128/instances/liteloader.png scalable/adoptium.svg scalable/azul.svg scalable/mojang.svg scalable/openj9_hex_custom.svg PrismLauncher-11.0.3/launcher/resources/multimc/50x50/0000755000175100017510000000000015224505336022031 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/multimc/50x50/instances/0000755000175100017510000000000015224505336024020 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/multimc/50x50/instances/enderman_legacy.png0000644000175100017510000000233615224505336027647 0ustar runnerrunnerPNG  IHDR22)xkPLTEywvuusrqonnJJJIIIHHHGGGFFFEEEDDDGAHCCCF@GBBBAAAD>E@@@???A>>===<<<;;;:::<7>999888;5=77766693;555444718333222111000///...---,,,+++***)))((('''&&&%%%$$$###"""!!!  ktRNS  '(*+-CQUWWLIDATxݖUERDAih)hTm *Q^=WI`ۖFZwk,k4E**TGBVc@Zo7"ʮ6wWAݴ d `NYPkLiFfZD@\ ()cDǦlP"كi+-5ֱYA3W"-bƠ+3jD\>`fy]SU=ך] yh}w!aT _h>\j JLUOP#ˢxdKNJ7a,/oo#8O5 ?+T?|"tZDBqXϗ@)^, |B-5_fjmG=4 /$0CRr$WE⸄Ok]7]l,,13Kb|pdINkϾ -]>3I,p]pHb"03LJl_nF0wbg*Bgiy0ipwkT !Y PKw@u u|Rm{NAG⥑+SOB{`BHj[m1#Uea~-/ilu"3ֲ.[f_} $z9H9յH}yT"28@o rKZL)=%]|#?OA&Ûi;6?1r;?@"O矍FЕ3&'xʉϞ:v=wIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/8x8/0000755000175100017510000000000015224505336021677 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/multimc/8x8/noaccount.png0000644000175100017510000000021215224505336024371 0ustar runnerrunnerPNG  IHDRdWQIDATxcfQҒd8} :zk0L7-Ca_ߩ%Q jɚi jjaީZIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/0000755000175100017510000000000015224505336022035 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/multimc/16x16/news.png0000644000175100017510000000112615224505336023517 0ustar runnerrunnerPNG  IHDRaIDATxڍ]Qk?Ԩa-Eٶm۶m۶z3IpKz{+\z b0,dy`d*MCq[kBp vPL"MlWQD[q5zfB1E9A](@^}>}*_0py-܉M{s/pPQUGIȋ cX%S(N*Y ԑau<IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/coremods.png0000644000175100017510000000121615224505336024356 0ustar runnerrunnerPNG  IHDRaUIDATxby {.]r?Nf``@,X]7G3oڟY۶mwSw]juwm{Sq$N !ho'KbwQ J]נ>Q?B' ҧg5\)<ɒ|sǹ ΞTbDaˇg3 $VSa2pX̀8 R(VJYB(->02W h̬rӢA6 3 v BꦓKZ%Oݺ몬( 䊂"QUWhyf?&a- KȮ'wwwf6Ӟ[\ߺ5"`2Uf/*W]8r"A ph]_|] p)UtI"DT"$]LkAOͰQyRIsfr IL'IN} 5{ܔS5^gBӦ"6B(-: "LnSۥw";縼BZ/.TZ5 <=+E%>|+Ή5匠*?&*¼WlhR0IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/status-yellow.png0000644000175100017510000000061615224505336025402 0ustar runnerrunnerPNG  IHDRaUIDATxR3bQ=˘d iĶ&m6o6k"Wu0N+Ѱ[2ajp_7=Aτ^ rL S;wB3sU.]fQ ȷw}=q n~V cbp')!:8CaCoKrC>xZoGI rttC߯q 'X,3\1a#%;S$cѝqyF>è~՚zIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/status-good.png0000644000175100017510000000074315224505336025020 0ustar runnerrunnerPNG  IHDRaIDATxڅC+AFOߞ,Nmm۶m{z|T|iT"M\ƹqp(Ǯlzkx5'co§/Լ 7~k_O'^nH>_U_+^Z 6O;!,sZS.WF<~Ԍƅ~aXd\8:ljpzfZ2r$9;[aZZl>+b&x8/a`~-co<~ [7 J-]h0k&'"ƒWC=>OܺlhsK暜<=^s銁IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/centralmods.png0000644000175100017510000000105515224505336025057 0ustar runnerrunnerPNG  IHDRaIDATxb8J;( W۶ֶm[kf j^۶}~orq({&o#f CF8nx 0D}JU?cS ~W5 MߐM,詐dVq .s.u{7eyVEٓ6X9j.x0W7(WFG^IxG^XStAj5K0 oh*:yeG?Z7֔5f 鼱 /= dSO~3OOy^[Qf|vv7Yq1dԺE4>-E Z"A[>[ %ղ+Ҟh4.^ʉхAQWB{u?Lj*~Rlq_g}nF%IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/cat.png0000644000175100017510000000077515224505336023323 0ustar runnerrunnerPNG  IHDRaIDATxڕA m۶m;˘U,5R۰ږN[<ɻHq|n٨ u @{ygh:h1GBN&oFn4nEeݎ/V&&_!XhBR o [ݪG `dv A\gN;gtIUAXv9r@mPi2iA?&2<ҵEeeגdc%#FP>mMWQ Q|fuZMؐ{\}D $ }ރ"%S W@eY9D7G|&*"4lz  YDx eb5 "'$w\S0g9D8jG_G=n]$l@vYN%MC.N@.S-V U?H#k*TϊZlYhIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/resourcepacks.png0000644000175100017510000000161215224505336025414 0ustar runnerrunnerPNG  IHDRaQIDATxup$Mߙ5ff ζmٶm۶mۈQ"^o?UO3LuxsźC~R9ĨQ} =5S>/^̢zH+*Ê4 -ņ ~$%Z9:{,4o<6Ѳp;Dra=L$vpvP(,4Pc*D&PrCjtzy\Ov.Ih/x0{R UC\!Z1N * -ܱF#[, fNF rQ#!B ΗBfZDE@42^FHyz|Oഽrq&Ë!S~!: @usFJ99<*|pe"+3 JyrbYኙH+AwZmرuXoIJcKyH}p:r%xZЙ3eʔ5osmgJP^ѻÝ9m8 `Pl0Yɓd>P~L<#˵JPzt:N>Ѹq*+m٩SvJv\Y߭؋/җ/_(..b̧ 6ܩ]~g0F:""؇#FQj۱cGށ,SLYr偲,AOoh.}s j8QfE1 @(7\ONN"'IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/jarmods.png0000644000175100017510000000111115224505336024174 0ustar runnerrunnerPNG  IHDRaIDATxbIJ>trZ˲OQUGWDŽmvd.'(Nkk^v]]6 mh` 0|ym۶;aֶͰmꮽ;3'?#Bk?""PddRjGR)X_FQQ#"jwGDԭ $0Hw⮤Ν[όFi jL bb&~ٻwI9ut +EJ ?7zb[YJ |% %ǡшCL)Ņ\Y!,l[HRRhDMJPCL-h@:vLrFFXy4ш W e@6xzΜ\t|0_G y[ nžjh|‡D9 I~E|#op1A 0pna F$܋_ IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/screenshots.png0000644000175100017510000000102515224505336025101 0ustar runnerrunnerPNG  IHDRaIDATx3]`mjb'uܧW.m۶m}olEJP7gLj|UUHX\]6}rK( MufRR2l:ddŸBS4r~5+X\y$^ q[R?'H1tU%x৤ +OY^mƢ=<eO//G_DsDvOwYAY,˹ xr\ s0IVV4P@t5S8v £2rUuDdO?UPՃ{3oJ{r# /W6t-Bݱ%X3n읉'k >> $`N'|40 Q c{_S,.pz;,= }6g+@fGw M9ŀ%$P.袼N^vc$ jm  d&ze naMIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/settings.png0000644000175100017510000000147515224505336024412 0ustar runnerrunnerPNG  IHDRaIDATx\pck@s]ݾnmٶmkg۶mڈsiɿ7sG2e&{zz.e)bȊnIW$EU,{]$U z5ܺ^,=ԟ`bK)ɂ)ٴ4MJ^߆BOW!dA $c_9gUmV[kۥ]A_OOVZ zhR˙;gvT:[fSea,8S0ce&?d_fH_CP[Ma]T q}tT27cW y 1#x]g'Zsh*h_{.LQJ+j|m OgB?P$IcO4W`=5Jm7X0xK_%n*p߿;ݽ_ A#\/ʪFLvŀA:]W5uE-t⊌*,#?{?x G 2^x9332tExO89$R2xT24O_x*Wۯ>MﻍO'8>]k(JL"N`˒hiꫯաkLb9"M[αL}3/ {X_7wqϦ;)\[$I˹zs~@T򏀛ݚkkXx?N+~N"'b@i/z |SIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/noaccount.png0000644000175100017510000000025615224505336024537 0ustar runnerrunnerPNG  IHDR:uIDATxm! ww􍙾uE"W>AtM6"l8t?t2OohW0jng frU-X,̓h[HjV@i (-4=Ҋ IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/star.png0000644000175100017510000000116415224505336023516 0ustar runnerrunnerPNG  IHDRa;IDATxCK1m7v:l&m۶m۶m Lwv+"څa͛X3@-+A^׉)ᆩ MbsrJ ϡi[uˊt46*3ynϊĴ~V'HQ}YT4ȇh 2)r9}FƹM.;p[-;[}FK[<@Lpp/oH];MS:k;%x*4ܒ]3WW$!08ƶc8OE|Q >HTnX ~>XXOM_q[YnP>5Aa8Uޘk׺>]ϗk`nKRc`UĐd ._c K]|r<"NIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/viewfolder.png0000644000175100017510000000047715224505336024721 0ustar runnerrunnerPNG  IHDRaIDATxڥ%PEQ@ww%^Ɲ [G,wqy~3n$XUDuYh~A+Q9'=J\0s>π sI 6ɭZ~K a5#%!+ v9n[@xv؅u MîYNIV+V1utEviA;j> ݆P'+΄ kN\IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/loadermods.png0000644000175100017510000000114115224505336024671 0ustar runnerrunnerPNG  IHDRa(IDATxR]1>뻶m6ֶ5m۶m۶|&{L:{H e:ntxӦcݛy[u T4wuK~vwF<(iS>"h2P}r3Z ހ^{`GT7[Ue"mY(Lhc}o: fwunW=~_d ]MﻍO'8>]k(JL"N`˒hiꫯաkLb9"M[αL}3/ {X_7wqϦ;)\[$I˹zs~@T򏀛ݚkkXx?N+~N"'b@i/z |SIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/status-bad.png0000644000175100017510000000062015224505336024610 0ustar runnerrunnerPNG  IHDRaWIDATx`ϼ&Z ׶m۶maq&vsuNua pi@%iXb0."dh*%p|=~ ) !U&S]_ ު%ԈeK#_Kn7-R[2#A/(&ULҒ&=NZl`t, J 2?H )1QBeĊSB < ]#̪GDI~CgeSD,)D6O`<|D҈UV&t:ɩ i$A|gzB$w`؋5齝 MjIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/refresh.png0000644000175100017510000000143715224505336024206 0ustar runnerrunnerPNG  IHDRaIDATxl\|8n>7 ἱ.:xۃù9fgJ1_,_|0S׆O4#R<;{[UQ.s;߽-gs08 6q q7FBVe? ^X{amnޣ 뮱5k[oW8KTxgccdp,ZʟρR*݂&1_ PV:>1DndmQ :~:Tvl FalhgkFר.3kDB٠@K8 оɚtAV **yyM+ŷI^ͲVMk ,e:d=] ,a02F^GQ(WzGg k;OJThРXF3ҍkougj?v.i*w˼3C)(FJLM_DW[ֽ8pΫg;k%[{޿ @<#+KzD4dIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/status-running.png0000644000175100017510000000073315224505336025547 0ustar runnerrunnerPNG  IHDRaIDATxڝCAkq"6l۶5 G϶mƵOWa"[ۑ |C-D-"z-gmG !CH(Q*5XFȺ$D/?J 7/Ua) 4f$UH^s gR 1=z{_~lRT_d<}$ 8Qfѯ@v2-66vT*"we 9z |}$!Q`v5=J^ 1c@ĊUj4 Џ2B!*MJ|MC/1=V Ď~A,4|}h8EXmBچ?R&c|ءu8TfBȞr.) < CYy)12){ʊ//< d %ynZhFRH(wJnI]C?Tn 3^\sO夑cV۔cJ.pk&>5&\xr;f@҆8 4 )">mF!/ʽ\s?W5mQ՗oWuVPMPV _PCC LJljIkY[oڼ:}fVh7]Tt@E]R/IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/worlds.png0000644000175100017510000000140015224505336024050 0ustar runnerrunnerPNG  IHDRaIDATxClW}.Jm3mۓhYf(4"mj<[ꪾ]4[F/wMczP?^~-yN/J<Ͽ P@5*/=xɱ~T-hԘV 8O N*uXY0>1~Z[S06n_ژRq/D XBeοM9"v-aDh5#d>$=%r+Jlx/s8xgf>{\f1lMq^D{v斘Z$tܮOK9l'@Z''6b!?6kIMdƨ *k |Cv.˥MԲ\rQ$ɷ-0d!Bha*Xu+5}m Tm|ߣ[;ti8)]J\9m\쥮Jaǔ[Ч a F rH$UysVlQ8 'tifv""~T壟Qg0`"d',;*a͞-Yz[U8q6Fu<Sej@wM'O[WvcZmKe.5cZu7x& αqP.h0~]92jU ==]u_[~ a꛿5_L7IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/log.png0000644000175100017510000000047015224505336023325 0ustar runnerrunnerPNG  IHDR7IDATxba`8P\# Q}3WN'L!0U vB0C 8[MըhZY{K 4̢k,i]R `O9Uqh,߽Aѥ)rUdPmH:⋊!K4Ņ҅ kՍ\̻¨!T+z$:4׊K'BޕAo+z3zwa! V͡8^ŬD'ΦHM!xVJ֧pG7yt/4޾IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/16x16/minecraft.png0000644000175100017510000000134515224505336024516 0ustar runnerrunnerPNG  IHDRaIDATxt-GAضrlNVPضm۶޹tL<=m,ob -=v`,c .I )%F!bJrՇJ θ׍d,/ȎMoyq:77G޸k|+.zJDQi !Vkض>=}_Lgj '(u}\.R_b_zAeS~ăWqbt|̯|mVF&13$włoWD̙UX2e$jY4^N+~W&uz{xFX~VlC=U _ѱJD#%@$'\=q+PBZhw Qϱ-k=ጃmxН.l5G,\[ўXh~XE򴅺 .u0.\ IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/24x24/0000755000175100017510000000000015224505336022033 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/multimc/24x24/coremods.png0000644000175100017510000000222515224505336024355 0ustar runnerrunnerPNG  IHDRw=\IDATxڥU+K=w2Fo7߶m۶m^ۥo۶m2 2龿֦ԝs[FUOq#_5~>c^;9:OΕ \\GW/ޯM[i3>iLǟ?@YRl5ъZhi } 4;ܤ͎hc$w_oߺΌ#׹E',^߳WUD%I*}4ZdJvPjwC G;RI`Gs0Rkmvi*6Bp%t!E9Lz.tO W{{+FŒG f*DdrÜrThDZ8T| `HP@IB /]3Y/Y̌LM D`D Q/K3UgHMx@1 cRyP6Ȫ e#03ƣ ȑ#; 7g F&aȁ2C(WY̚P(V7}y/:wKWre[# E(S]0,s>5ߪo2gm׿wrzٶ~Rsf0 68&aŠ5/*|OO<×!C_b 7s)B`gYWyO"M7p' [#>wS>^r 4`W":g퀻{Wf[)m窉$.TZ6 {f /V#o 9+*= ?h,ڜ#6HpXZ!kͳOF+lB~%4@x'IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/24x24/status-yellow.png0000644000175100017510000000121015224505336025367 0ustar runnerrunnerPNG  IHDRw=OIDATxڭUa=psߣ6ETE] kmkNgo@DyqFݩOm[cefڪE#6G.]xr@:ayAQ% Z7V^d)qH>OhF)aQ qm \aHq1lt`zTmv}Ia,3|䥋,A]0*v<9> CMt*e6!pi(0 9/&(L "EN~Wy@D[khmxK,).on[,@.GSmzmJ*elt׵y<%e^zzÏ7c_ũC J-kg7f#k˞}xxdIjghԳYRyl8{$&-Z-mJ2Ŷ|ޚ'̖+^ ]8eU0NUxƛ'/[ !`-m#D&EiGcazl@|&ȩtu'LA%wyk5UlQT<wSuSB#HHEXriQd*B p)vE']fP @$MAv &jZGD1<  QhE%S߆Tj#jU KZi7X@+Ĵ_O)}E Â1vmRg@jX$bAJoh%Ǡ-@IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/24x24/resourcepacks.png0000644000175100017510000000314315224505336025413 0ustar runnerrunnerPNG  IHDRw=*IDATx}p+koƱۼm۶m۶m4i33? gs0# 6kVau܎ eQͳԪKj5sw0p QXy_E5ӆSd&U|mm*''C9 &p@'St{cjOtvvܬL(۶[lGDE@3.HB`?#C^+ 2y3F>M$ˉ8z`;EJ %p?LԪ*QGR[=t˲fII4ipX1rH尛4t1VuYɏ47 mITmm-cƌQn^O>BG#Li49q?HG,&q /B!=թO9EF.J)]հ|J=*\s;ŏYz}1d`oغq$pqS"d9Q٤4ez Qb46'osf?|5FmpiKgL0.\ɗ_~{Q78 .BSt#G5ӴhG"=b_~ ptFɫ2 ѱMMFyaZ٢@L`9ǢӇRZ_v?O~-bF+۫zkn۶2j3OSsli3/呂ʒڊDϔϯMnօ}wݠAʻhuP( ʼnZ~Y|9 u+-|I'UUr9Tm?|ݬ_}ѣqeN(++sGpvΜ>hkkK?g|_SfάzCޚ?;aywvӟڥ~Ǝ{{49 jj hrTu#tϿ.nv5VPpvA*(},IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/24x24/jarmods.png0000644000175100017510000000200015224505336024170 0ustar runnerrunnerPNG  IHDRw=IDATxT,Isum۶m+g.ٶm6۳ͼ̊U~eD뽗 klz?s!}pBk=~+__"/kod@IeXy2J5FZ< XAղY@[oտGJ{o ~~*gYuX|xURӟ i<\]τ_k"BJ w! Ve 8+knzA"AX,$&"cP R--@kA {A*/B\!s/E0 ބDUE@ijP(ښ\ $ ՒI"2++"V =^8Cpe.gL^QOL TZUT\e rH=2$X0zqfGu$zlLF"N@N?R)i"::5#{ ^>K ~ 0_19$Vv/zpx{\/Il;9<$PVj9hjΠɹ>ܝf ڪ^J3!^B6jO7}⍎oVX:^ZӌbƵ c?-OMZkI~b2]! 5?;ߧ1 _?6vK>&Ɔ듚u;A*Rx*׳h(C"vv~q\J-/b^aXse 71m\le!r}]kT*"֜=vιWB期5_ huSx̺"(}fw.Dk1r]2G^Y(UmIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/24x24/patreon.png0000644000175100017510000000057115224505336024214 0ustar runnerrunnerPNG  IHDRw=@IDATxDQ%H( B"jLk I!@!H@Ny;}o훉~ȵz{kAz -nce܁EH?(XAV|L>ސhXn*zc.ʲ1^X&S]Y,`~9 (oSO!G@:9:Okpp maًR}oIIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/24x24/noaccount.png0000644000175100017510000000027215224505336024533 0ustar runnerrunnerPNG  IHDRb$IDATx! aw?7f7E2A4}ދh 1W 4xɏ@*Ƃ lC$ō'b͌ CG6B1Kq)%{jp1nEO9$8.IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/24x24/star.png0000644000175100017510000000204615224505336023514 0ustar runnerrunnerPNG  IHDRw=IDATxb4V6586vb1?Tf!YaHeUzS@.@1UU}ad[͗oKY g!ъUH/OH9@KrEQtjcl۶۶m۶Nƶm}&}vwU;ի=qnqG(dmµzQ^0g0C bd0hMB8Iɛ"'S9˵n=cI rm֤whi:)Zdu]#T"!`ˁƖ%Ȱ@4#^EXFI$QFQPd*ӔʭbxҫXqNW J#?0IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/24x24/loadermods.png0000644000175100017510000000210415224505336024667 0ustar runnerrunnerPNG  IHDRw= IDATxb 7_Jj8b'iYpSvϕ@l31cUQ u ;X>4ZY*]9U'7DDOo ;S%EJ` <ۿY۶m۶1Q5Qm۶$˫wf'{%߷N U. 06adB8Q/_ 0!MVj앻iTeWXƊ4ƅ3k歳B㻊A7Q=,6 R$޾$mKV>pw;kO\V @L%s`rY[ d4,BAlW?Mܶ7U =ב!1j{I59&$dxWU1W K&BdTL>"YK(Uj<YX*ZgicvGDH`&0oew=Ncm?v4W2rKHe)#g܊(򱀉yvCGʫiYjZ^y؎ NlT=;mBم(ﵒK@DAcO5mfeYvxY T!YR]8<渕8|drb`$B6=q$y yw|wϑbծ5y Vn`=%+O>2Íڛ++~쪣?a\f;}-UPu*hǞPc/}/4ִ.eemlixٿ @1O྽g?toq}O6y!}{/ #3>mƔ[ǒH2/23b)sKXAx Olk%2{k!5e1c:U)([Kg㲕r'8/N@jv/oƍヌ3@8IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/24x24/status-bad.png0000644000175100017510000000117015224505336024607 0ustar runnerrunnerPNG  IHDRw=?IDATxAOX/8yk۶m۶mvn1˿)of̻8gReUmI+y04b ,kɻ/d`'fdGHzS!R1鱅.s>a=-/ ?PSa(`(ݹ0ag}hIJ1t̹胖I4D`n<Ѵiv̹B`Jʭdعm۬(5mZ61\#d=FrZ$x & da'ލG:HNR3p ׎9皲4~gNoYIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/24x24/status-running.png0000644000175100017510000000135515224505336025546 0ustar runnerrunnerPNG  IHDRw=IDATxUClg83:+ͶXelm۶U?]uED(glO ihmV'B[a~čF| >`w =L𻳗`n&h+ r8Ƃq dPk? Ϳ 6&+l3,"$^1F8x# "fXR﾿9-+:]&8٠`۠z>.H&3װIVAW@QuWP~I_)4t?4Ĝ.c3LhC =|N z3]';C2 fԜ?HXXXS7&xZŜTf al 1<.333N̝ک9;ND9Gpm{OUՏ,..*[[[~m/{C$Iz(NxGp8eYo~rS*t]aq8,<&Pp=fJD!1H`ii <)ab$I~ikG~~۶"vi; vY^^f."2a~muOk7,%EzW%Ez]u avBvwwǴS^%榛nF%zٷ;?~xtNc.Km *ufR* Ϊ9zITL0t\#Q{1ƲA DAEP8=fp;$-[Rx,`7< @>B?Vp^|(r;^P@?YMc` iɒ otjc[=S> n!<qw'k5@s.Xg-9يkNMP`-R[#\u2n]5\VW)qT}ca7H*_gg0YP]a\3|8bm?/|y+5#ULjm_(yY%4@{sLя0l|=+Vy"28T'>RwF_J^կ+CO@[ (@ MҼaLֺD5V*/}\=Ds*9'eդL')?c,Uj|2dQ_+55].F'zW(;8|Yp5ć)c51EW$L+Ϝڲ(oIӸf33l:T b<25Z<\cRR eq>Q¹m'2&6{r [0T-mT;nSMiь y3ί_mQ9GG3Q3kR*SSU@m,ALM6kT-WɼeBO@e(pyUvV(9q̝0?cxy l# bn" #d^y&&?qCcHn py)KO썑*J*-0&㇞"xl9}%mn5u\WqH1M+^$G~!8Vu,Ls/+ :alP  %wcҗ<1P-DD{A֠ J 9v bGY*xe!`QB. 'ڵ cvH@qh>L/LNsDOK¤ bHXihX} ҁ48(N,ycFF]z%NG2H1da\˪.G͝ZD_7nd80r9A bcn!R8b XsܤH!(C YKIs^/kDCNsWzl\k{U cp O^/ !'1'rivǛ-[ 3L! \Ep{:ZZĔj=;˛ rYq q!H K%WEB0&]XyĒs^V +ơ?d.Q!"H1sE3rQ &d;@@nDA/cxE01&E]9c@! =0q$lΑw䰂:"&X\ bL1!1!{8D%( A4! 8&ι19~Zڀƚ粋Tہ86yM_!?"H@ͅ;aNA1j Ȣ G8]VM4@Ou7B;mP'6C8gA4B}&ӾdN7_Z:HGM4 ~%2s 5EH8E@PMUZ85A+gqA'$b鱗 w#Wp>`jrap:})1}cse2_ -2)Z0yo[nٓfmG |aM@m-Q?~ښ |/7>[Z_}'|n>_~&oJZ|* F \C"3ȗikQ>}Dߞ'Ý[bW R}~zWكF@^ngmow=@9qmm%}SQ RQ7՟ܞhKy~ћYZ,7{'zDo3TrY O-.UƟ}%-M֡BāE$e?!s}S\'  q_P~kMk|M7u2;#z]<DD? ]1x"Ol(-3gMj?su酉U ;kFCqxDOc.0wᕙ5`lK_K2Q=p. ]/MA7F ff,VwVw M"tP'{7ߍfq|8:<ǙW*<ҳ;p{}~`;  Ig:~IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/coremods.png0000644000175100017510000001017215224505336024365 0ustar runnerrunnerPNG  IHDR@@iqAIDATx՚pH[8,2|Lcffffffff~33.3 Lc[RwߗRThIjW׿v>B~2)"ܪWsC`p5+*>Ȑ}m|ao *^ȴ֢' CcۅGwA|+nÈKTi=yOig'yx.susXg@7؜VިK|OgN'>H P!P^SMݠ>/M"W}pÏq?~JczL .kzܸϫ}υEYxDR`&`0 T.UH,3ChNU^7 4;.AG9$'8d~N|s| ;K1z{yo])жƒJ.B$Yp4EnA4 "dP(CkkC06wĎ#ny򊦀F .KB5+REQq'^ى0UVlͦJ UQT5haH>_nL`Z.ˉ8>]čc @@ @P0QS׻v#jd԰2V1֙om窙a38<{s \/I "v0XNsw 3z i?IAD d Jc0l6i|Z{߳-_6s`LDӶP8$ *i0&Z΂ V,Si&lVʎ-j{Rpy;rN.=x_6ָrgmh1x9`!~yM$B I\i61u\#!0xg>]M4GbFAAQ#e| ߘ<8\o,ҘT؀|( 0a AQUoւ(T/U `K[pjL|ܷS ' *YQ'*TXAee9֟[vQ2r ߥX q[MB|{ ,P"H/tڤ 08xϊ~݂)`a[s'ð״ZPc䢕Xy/b,GtwZ[?|N2iV jM[96XrK`s[vV%}ȡy @1=vޞєZv:uՀ4-zɼId%wKs M1a;,ywGF_Q%os~6skqgt~+y%C@V3`'DL$PRF#",g95|J+QjK }L_7)'G-SטlF3sb(o'0?1?WIseb\vD>Ě3~ c$nE8(Q>}. ^k5kf,A۲4 |_q0l54nىi( 8͢{u\%QHC]NϳsV>p8w_ C2ᩍ/(CF!4r^G cĕ\-:!HuHWY+ CVUB N2B C?WHɜЬ5dӲw?zHtՏ8 Zq!B5p^ !iX>(]Z)q`"þ[$߮!ȥa-46#;g-Y'^'+L.(E Aa9#e OUyz rC #F*R"(&S\Mn,@ &!u,A1 uSMUU@]]NI/lZ$%R,yCmiZ4yzON5Ѿ8xj&O% c&5w/$pnzXl*|-#@Pk_~^8}k-Zrh [lyArlpgW#b|PHͼu\L}_f'j\Z+B+("T,*w*#±˽nC։vݓ3ӿ0=O@ 0_9ʉCf[iҬT{ k-&,K6Џ=ϟd)2ӊ807;ob1cGx#vYOPC3ŹȪ–༲2r_]7?1 e(VԪvVZ-4YX|+%k'?9~Z h({o_~}wɥY\MYexbN*Q,{ yAjL}vhDK?]#nXj״_DO޽Q7O}hWU#]yåc$m_yνtt.W^EVS8JoF ?U]|)ܳ}+kh}791|\!}z+n09sTiw6 %MD ɱ<U>(R' I5[߰Bdqȟ"e<0rr饠@$mH)2UAJdU~`e:2p/v0@NBu%7A+~& 0WXu*eF IB72L SR7*"LEDTA5 qPp:>J%hR ZdoeJDEWJMPӪ+ɺTM3@a>G#PD9q*~EpPC؅2O2/UPIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/status-yellow.png0000644000175100017510000000354315224505336025412 0ustar runnerrunnerPNG  IHDR@@iq*IDATxr[ ^2 &0Sa0JfDfIfQ+2Z>U߃ᜯOýWB@~_2"x)CG_zKSR<.F;KOȂc׆+?T`/(#%MJ`:I(Fc15!ƫC7Vh%&`` (s pqOcEk+n1SoM o`hKx%.E0J<'.; |m9$$$UMG0 a_ì٬I4&^<)ʖGi+D97.#o {50S$N,`!|2փX ]DoNL+ҫ-.X,A'LOa>}pB`$Aʧg'J8+_oG;̽  `!ȟȄG9kBcO< #}6T!gɍ:?S?C=  ?p_< [#zdF5^l_MzݿT!ODf}}G5MHخu۠BpSSSQ" cf]Po&Z{ˑyܳ0X- ?ÉR43Heՠ9 Ew)!dF;d&&X,hF ?M='y'bS9(ܔM`Rl 3>yB PN`cOuE!%BgdoYCC{FR@v$] ꧆hK9_{86sÿoxjaTxg[cO ፐl 1?+ה lj`!lrl*I"hrK`b>&^-[8iG$8"ZKe9=xqJ*SBG^oُ?`Kp~O{pOgJxC[Dgڗ]7R gsy3% "黭NiwO9K b|݅'E; ,-r=jVNnPs+LdQNuӱt=uEн)}@Kf%R 'p)t BAy:X":JS6Ce^&`;.BygNcE]8C t * @{@~҄+P,f4]`oj /nOcl3pAz:d@X@п=,KЋ AoHO]s d v ڌYj m5_@_%h+#>@4a$>Pj$a, ,DAVZxu |Q,Pj: piȪ< c) ؀6 $56 Ίcn w0)@F+Xb#L[yu>` (41&H )xJ!\Y@> e1Hs`x, AîzPLXU U6 F(O)/f`&C9 @RzK6`I[4aD;ɫ4`Bj& W]eI^>~˄6]P $~>,;n&i Ϗe-ς &nf޾v|7xT,K^Ǭc`fL6zɻ]:ӄ_` a h>FqiL!mLx@J ۈ:gd¨?;Nbƍ 2 SJK(f\6x/;]fɄ6&$50?j (JiDgp3W9 fhYPoPȵ}?y&DԫHo#t^*/c]5a}‘i9 2E\I0Nۘ`%rZɂ@y LPsfg xL< TV.Ggué7Ll%:. Y? ܙy f%`..7 7Jϧo0K| xus4a2ݝG,|`,wwfg&3-K'4̼/GȂ9Bg&MswgxM% &c`W ;3B37`wh&6a?g|y`33'N]8L# s5_Fp\y vitsq4a4{CV@>ě`uNski' ԛxs#TmE&j]vī[mVʍ(h4׶xfO@HlSbB<5<̺0GcagCW/[ǿZhF`z"1C̡u"(a`#Z4MH<EkE9j3\95M|A̍沸%j(6R=hѹ g :4F.‹͕SiJ|z[ch k@̄'tsc_6:rD/X~SϷ}=n٩m4vvtl/XgE8囶I3;ň鳣KEr .p<+A5}QThR[bɈh7 )4`%V#Uew*SVbOQAOִWSY4"ƁPl[<[:;cid`5P~ C¡M|UĜo 3\U] Tkt5:@| Qt)"͝>VULmMk*'T=H\ZU>c2&Zib7C,fT(1=Y3x 4o~7M H f< L60D{3$ ++hV':ظ(V^4ۛbc x"Q._PoG }@<>?s]jȊ NCL7D9d?ǯk~'m#ܸTIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/centralmods.png0000644000175100017510000000702715224505336025072 0ustar runnerrunnerPNG  IHDR@@iq IDATxp;׆Ik;vI.333333333333333c6T {kͧqd;v{='g?5E1 =oa۫q׭^7qY+T-JJfFIOlJʔ3Zٟ)?hWJI(% ȃE`ϙot.]bI(M",D ,NE$ETxTD{ҞŘrBYc;*P]J) )3#TI%sIT̏HԱHŏl/`EjXwad?2OjbIRsлtϱex=TfTZTk0<7Y7FO*J}zJe^]SĦ32>rQJ%Dݗ2R6H4}V>mRIuˬCTTD75,:ͯ{WsoVBƆRa|-S5Su)7teFRJdSq/Mu:F&@+^8 $2 Me`Hx)tRY 󢐀MdR9OCGPؼ>STE*I|O)ER*)%*@ޣ :61 i/#sX(fUDltg*z9ҽ'; W|bÖ&6dYdl ‚;6Bsak10, SOvd S )R7 ;-+yӄ@hm  ׿SJQn:sPJ(Fs7^l*ؔؠe0 H cgb0h <$.P8UL}1_0d>>bE#^6c׏-$=de|OdRPyu3N =xv% !k:SW]L,H@Vr"K@(/;hL.gϿ$Bj)x6i Pt2Q~`$GӃѝ"`PoZB0 $H({a+v08ۛrvWBesBhcD*+ Q$:NɌ*g2Ac*F>d53'R+#B/:cRٜmKec鬗d ~GHO4ytaKrD6I t48[3v#r趫֦}L1IoHId m25WK<P8[cY~n]ح< X a.Z[mrmwD (g% g#?=Kh~6@;*$#m}=j f>͒'+xZ_n263hc&k<4&Ș[z?&o0lspw􆸟j^z'7-WG/e΃W?,zgmGU-Y97{-36=5QG}zIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/cat.png0000644000175100017510000000562315224505336023326 0ustar runnerrunnerPNG  IHDR@@iq ZIDATxԘ;Wn6l۶mնm:jj۶g{:Ԋ/ַwfvHc!*ZI7փHQމ3/O_k7FăaE͂Ikyb'kw"d悈I&͓0%2NדZ2Roڗ@$ړ^_:N\Og4dcBxƇ,De&Wc;F\K,Čʹ/)2L"IMMA0Ƭt3DBOQbPt7HKvа:ճ;JEhAJ7RXj]j5"$~5ˣ[&qlc,_'n 1.y(ЗwK k?^_~ruΫ`.wzL!')vƋB{]]~r>^R21(rL؅gC$Q:zsץқ4UC7uۼ JH<c9(<>JJ^~69w6QR}޻~f%f(`)5 ;#b ] J&F$&~s3eH:RSbDoaqI[/V])wA)ePlB~{fCnLu zB 񌶚hƌ'Rӵywy6.C($ei&sFTtBlT`QrOQ0c FG,:0j9-M옎2IycOns>I`C?/8h!dE3Z0{JL Դu03, g7z3M?@a(ӑiXy5Kb|Z)sQODfjGH6Q/(M FH/U!耢B8]N>٧Y٧{!VT} w]O}p 2u!^E&1Xv5@gBppt4I/3`b!"e SZ,dFP n@)猛 !AM$ 63n_7no|oT5!zG-fqp'7ރ/F/PW#?! "hsv71]N>]n^mGQr"9'_d(Qwʦ$ >Rph vq:mI ]Nܽ9\9e6}7rO9ҐbB=\WcUuϪ{< 竻6w܍ě]7:pd'*Nb`63i!t G%N/Xw8{o "*```BӀ]ȥ#uwn]E>Wsw _Zm.\3U̿))%q 8/ns8SleAd쭆RB BG,J)CR< @C%?teX /2|. mR OA<-͘Ũ' p/`jDxC=iG^WHXEuM<Q!ݰ+]jxW^Vs6C+  uK]821̄a*Sp/C "4L9]ʴq嘆W (lx>{ᾠO,`m^^VBW0˶#/tA/:>"Tcf1S5&҂GB}2F@;F֨T@ 0OC ;^}!wͪžP($Ԡj*Q"voT"TUq Tx#d"ȹ78 EHҁRDtyˮ\( ॿg3*v8tj 5Fb :@y!؁€-s!CTؑ0ǘ%utIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/resourcepacks.png0000644000175100017510000001344615224505336025432 0ustar runnerrunnerPNG  IHDR@@iqIDATx՚p[G qb3:2/3cڟ?)33SRfL_ ɒ.;ܱąGg+9{3̷?w~|8fa?©?#(F xAGΣa]25@/fqaGϿfy%4߽ޮ ~ncuR گ ja-n徲y??sK~t٢pɳ?W,ɽa2 >i hPjX2ὙN_إy?pTHzs̬~bƓ'l3ax<6ٴL#(ؖz461 n=kJ$ >Ao L^|2|1金!ؤՑs 0}8r94mmmP(***Wǿ[|r/ `%R> lG;k+S~DTijx,F,a+vwwA{{;] S6~f 4Ds)@ $ fn0 ,[u].~E{f4 < (q'aU wZ4/qH[$IxU:ڊUNt &RϘ1{Wu.%7T4T:od?j&%?9EN!,KL$Bh4gkw˲P }p 'q ?sH^/Ӏ'=gh(}RrСC;jdJup啨( ]`6 ט8qKRο{{2; |M4/3gNUQ^N2Y(*q:RІ7M5)SQ_?K5FK$@5io>mڄ/3>IT9eNO?k56D7UQJ4leN'c\OIбȑ#3gw$(c4Oy+Bi<$O 6&F"Ϛ5uUBSҢ\ we@MM guwW ί MOl-pO]=n\իL6&.ٜ#:T*grwf9`f_^K@@C_gի0 H :ԄgFne@$h3aVT``P J"U{)aӴJ'T$6|Tn~d•OCBz"M\N3gV'pz4N \6#FP1`X2j8t^F v!wK.~3=2& \A`, ogxzaNڿoM_ ^:O7 K>X[Z|ڙ0o ߶+Xt裏}z޳lb#|@d#aԞo+oChLbsL52=)čH{S}kq21(1 x2h c|(ߐ;--(sfΝ/fgR__Uj֧L+ˢYPp+no~NjWQ1#y 'aifD!4=c] wR}an"!H#t v|Ŷ8f|N4Dq `tGJS0󮽖K{ˈ3JTM@N,k0bXac+]u7["(C6jǾkU;LV~=bL8 NVN^FtNJq5 ~wF L0CC (X^БyѪ XlrrG]ZEaHA$aH0?瞣Wbq1~xTƇs&ʯ&_^ef3 ˆ0Hv-U}%4\gp=. ,9s2)[YAMņ uN z ikWP?2Q}<0%\ V{Sq![E7`a rh+>+<~lQZ?7^V?v Vӷ&+iTVS`S$ﻏa_#F} i=QWWbfy/e_h&8;iL"ª(C;?H}(o p$\ }i04ˣUU5C27@;sվ}lz HUen Tb(‚qL`ؕ 2!LV#OE΍ͪ'a{Fp%ǍC3'7|i?[M\&ǁ׽[%[MADS@ DĈT ~? 7s%5K~wp Kb\yYʼn]7)ɤ^>y] Z 0&H7G~|~.yǢģ) m }R9DvߜM޾nQr?~ѩ["Mނ/O'sY<:&ؤjjB#a)~p1 0-#PȧIgxN| _M;C%X DC)=dگfe`kKO]/}Eי} hع{MLX簮w p :ƂºuϽkOj_xYCb&aj̀q+C[ ,BQ! QgTM$oG/tT,?hNjiW(g_?DvAn.\l6~RP~0# (>[$tuqx:!|7b/b0bI]kTSy3mII@4p&C"C ]@*&2%=mz[,!_A@,W}HEƬq;:3f9iNi@ 4]iJA2U <Tgi1T*\ajJHQmSaQ6/}]!4DHlt:BAS5瀔 F®@1{ŶyƷ^ n(ŋ߰}4s7FL&Cgg\w'n(s{Re6I*px'Wl۵%,c?׬ ,رcw-\[蘒+1t_gXLJ|:0K0dfU }05tEBh2(;AcQzX "( uN!MG'0-FSSzx,'Smk+KF{O. xeˁ2 @~ _|AuۻST|_0gÀS:, iV5"d2J|(fqo82N8Z={vl97+iBB2z! CFsw°gLFޙ ({Lp=RpmxW_}TEtO. D lxW)P&gTP{|*â0zFOlSB&S!~@(Ld$~׮];,y!HMṚ>]mDBݧao̰P$"T)(vN^uE@gSdZ-^Q>Ѓ #Nm@ )l fQ@/#k?BJ,(g2HFSttfPGlr>|} p>N.WF޺}ӦM˔轙> 0: uV@mDMжt +a(˗/[g?m۶L'4 !vSjrtM7բ&wu$nJϒ3a-QyU p-|kjeemێؑL2n0H@4ܑ~\@O۶mk~+W~Xcm**&,T uNT'LaYsjGKc=̯~vO+cTyyk-'}?3B!\v}LwG"zGoqWbmmpu uiv b\|{&PƇ{*E­=bܹ'8఺!C .-E @H)){JH麮ؾm[ggkև5oR_wɼ?R)Ef/_|u6n@MmR WDK++MN}Բ7,PQ {Ŗn5ܚd[O9N sfD@Q=s>(3,W]~Ci->oƃǦQ6y2aCW~VІtnDfg oX,~jٳ3E;lJ}OX+@0پlUo--KB<QK]4pp4ԽRa(Z2 ]]܀ :]j0;*D9bС#*+-"zn-6NfwN)ei!* 82Aa`)Ede9 vBYB70 e2t9 o0z j;BKK鸎^*5^B5 T,D9`$ ĮD]iUTMRc}NvT3MM-w,kn^A:V~XHpٗaHp헨q16iqZ MRQIxsc1ǵm'o6lq΂КN7ޚnor5N@ha%H 7K{mD }a:.DgY^e<,+Aep Rϓy! s9#AE ,]jReTF^4IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/jarmods.png0000644000175100017510000000704215224505336024213 0ustar runnerrunnerPNG  IHDR@@iq IDATxZp98̌.̉c{cZt-]W6.m_Wu v4g"Y{a}8gB-hZ28zɲ{yLXCN?&lր#BՌ1]v} LN΃b<ϸ QG>w\cr /u=)y N,5>ce2W'Mo2q ժ uj<b}3rE|#uC<O\v/T jq}_XN'3$r.6%P {'!Px8.ٱXWP'kIAxz%t1yO\U`yDISTE6i ]-vMkprkrgQ2U*Ivq2$_ysYtJ=+O&#<O&( \3z㐀dtJҀy_ס脧[Ɲ1x6l~Fӄ>,kq;-x@:CM{I G꺆-~5͛getnhrN{gtJ<;4Dt^-3 jkʍu HU{uOHzXf>W1.#=@,{[h7o I|Y91W72L a6ksC=]]pc.[(\_6{8@wfԮ$AYBUrM'4[|ZTD$/mt}ы/6C\bBR1,Ce_'͙mT*\qㄡ@b4X'||ֆg|&lYnc3ޝN3U\cLn-LfzvS]32%(py+B5"7Ƀz ),ظ=9 D,j{pf= :,Ybf+9߾=z^iz>&"kK)(>"H XlޙoV7zV̪ZVۂ?gmGN)_ uRb.We ɬƉFJaX= 6^ j (MEڟ^$_$hz~ICCq.6r~c/MSp<2ੴk½>,( S"Fd~ Jq7SGf3@(Р2d7hUK<,S1+&#-3e.vY p.4cژv HsP f,W/t]b!sNUUbbQThSG>iI%7%%f eN9Pxٵy-sZ|םC-Ao8տK/c:V/WfF򟡡cBSIF7Ex-%|o_XQ1Ƭks:XOro&b6qI[>6c,[>+\}ԫϳК ZY~wu9`CB˗=VG>᷀/ZDQ@. VV .mm3b̓n}Ts !bnyVlrhF6hI|Fc^m`\cV[][ӡCҮkʱ;8>ٱKQ7[RA!c*^?}>XW ~1(l ^@h'D3Q)0:~,D@gjtC)vl?o=:Nzu Duox~z^N^~ेdb}!g_ڵWrwhR(=jya1\nr̊MOR`Rlc$T*h{@ؠlFv5Dܽn?ǘ4Sv|*`a#* 6tk0??usSLmAVX\Gf*c'!rȺ4[Zfa!] ZkUٖ^@uQnCI_@B0&vQ=lfMFFys$@jǔ) },`Wn0^y$?B 0Bgِ*T$ `@RFyػ<‡t_dGo+a|| FAXQ B@ӓvqz_n\#<9TrYrOzgfVl`Ɨ+ve37?Go#ëH>!*Ј&6A)PF9 ]xqqb;&Wmi:Q@;H1&IS"`7}=I"NJg8'O$$PQvZU[[2]JpB'yCWU4PuEl zSLE\vю[vgIL[#\|TS'ײ= ch-6[<:"X4}HXoL %*8e` ݜ?Ko?wg '?7|3|7|gOLs&Ÿ %hBc!"5iGBt hOYy}6G l*F m̬cZC,ȍ $!}ygϜ`?J a>w ];~һlhАI 4 R(9?e.sfqs(0e dzg"_xAY,$2.*Љ* W'Eu"]BJAc)W|zwMoo\6cl:$J%ټ-K6<|'ㇾ CÛ%y{|˼ i]jHsG1.߾M.LJ C0&1ffn!ILu.Z-j&s %:&xnHR'WX(bjz1spV5BJ?V.\=oyfc{xd3A:}.M4Q|XZcܳkGqCٌC\[gIl, ?dp[ٵs|!FIjm(<eLMq$oQ*U__wp6TZ!R,37%= iu\ec&`0ڣhcэ&!Q}Tf?}Bu]."%;/WLLLsUKui>;o\!O:%)v)G|@GEiL- A |Mu 61!tPAw#} m ɺO"ΟA0Per| R;7ofmH>.hm!W +Sl$5z6 cT3BN7iUH`K >*>O$ҲJ%&aeѡIBZh5Ej@!QMe{t?c )ߊ "LB߱ ϝ|_G)8Qleo*rzRKf6\s K-$ze3t^K2ݺxWW%c'X|`E<ד}GQہסm$kǯC{s@uXƉ6#V0IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/patreon.png0000644000175100017510000000142115224505336024217 0ustar runnerrunnerPNG  IHDR@@iqIDATxeKG zmKk3϶?[ֶm۶quþnҕ|E}me"ę02Vlߎ?0$Cad Qu  YcA<%M{YY()I7T~?e9h wpg7qW,Kn QTd02I؂b~^n9xJtqz!9#P̅ p' #U] ABAYYAbS$>o> &(~+<$Pn?s^TN?@2exS3_@$ /L٠V֛PuNW OA՘?_d - Yz ^~K} q>7,PN{쎯 Iğ ]¶|Yu'e 0j`VVHVG\HSX s*J~Npc 9'&s8!9LlβOL xQTӹwOZ;2ȿ~GNx"c? u^:tx/I+,0Hv70H:̼kre';@D.+aW?? `bI!@ Qlun)$e9%%M*KJF(iHd1>{Ay^jCQWɘKqr)L$Jb}$m`Of6OeH0SrLKefffffffffiMچv8&{klgfqs@IwߕlXB>p߾ů}cfq'\ohhmwoSCGG|X_YiEmEhyܛr"[ヒ 9pĤmۀ"7ph `H6wp)~Æ]vݛvښ/Q poNM9,*샂e9K ^_4~ȇ8J%|>&n&si+abFaM˖ /; E J``[EQ0Ĝ*$y E*DmmFI,ҢgcP#GN ŵˀ@qD9$8:rNWm|ؖZ96;޽{&):O? P(&Se˲قQ,"=ӠZ@SOek (/*%ŵ<tID>!@VUZDZ.@<wwKJ"H]]tDʄ>I瑕"HxhD"s3\$rrʰmh P(Hkk{XHMl*(w9+l*5@*j03eBET,0Ht9;C:fff~[ZZ@u X%) PefAQ+%$)$~IX 2B&pZ[}{?ڢ Ʌy&2TWXEYJ "x~ߕ.1| <3/z%eQ( Bi ^G(ӭx#s tX1M]83i|hl\ wm[>]Ջ"@*. s Ţ!1Qj]Ro{Q >$\AeNh8ƛZ}O{ٸqD33sLNOq(sP(3p!lzBU}!|>IVW} I^uܧ}g?tv-% д>tmٳG]-֢॰Cl] VvQWZ^?oS5y+:ЃaI# sA9 E@(<ɤ䖈|@O__Y ,*ˡ,%`B Ry9QZ[V O:S OMMNc@i(J"%Nd JضO/B= S2Y`:xGK[(@X)4 jc{լ𰥜;7́|)*#H 30A (`%&pŢF/$fjPdKP^ l;``e8.(BtGxš(Oz̓P|(9, Rc'WG\<5 ,֢ 044X\j`:ZԈIJ%UA@`y=~4r\u g>*z{gc5^*qVyZT̊%2X_-JE'DL'@eP{ |-gg£TsD*~Fyp-dPWWtv@XTB2n۶LWZCm0`/M5]']bbemr^&stL+~sytn(oeW1:g%4M&L}}K P ]'n͔S✬ X@sS#K;`wɱ'kH~AB 9)vƫG Q 8$<,g6$ lް'qT?6htJ^ΓVxCF&o C ݝl*Z6HKC-4Ԇ4ThL c1mdWё-gI jgD:0%9q_6xk;ǫ1/xk>q?@[]=N>#~yIwgsWȇ<59&ܕW|nN,GFmm+6.tY~ N;DҞ|"O9shxhp֢ 7^7رFc@QBL䧳} |,Ƀ "Un H?QY]Je :'H$v4-d^YfRB2/i,- L@ MBc۶Ga|&Cb4=1uG!S{"(,i2B ZX.:VF @{k}#>Z{ {>ٟJb}m.KTtӕ7_sͯ& MN~{^*+OQ䚟''I8KPFдHXWLxL]m|+Wq{=kʙ7nڴ(7zupuU*A,[C$R3u]~?99} .Wb/R̿gsKm}^"TJEau#Ře˟:9;HVlMfY7f=HCm=;4UqlTtAT6qK9t kz&.n/~_ Ν+"q?h{ķj-̙)xlyJryUr mS_߀9'X|,A|@/H: jrN0]28U|ظ=>>ryxE8r/0>V?HX2-JC) v|ds0~,~ښtQT aQUŔ*`yqpo%$9gF jf9yTvxx?W?%_߻#xcLN`NO`R(Nj0GZzi~ϊ$ >|BV&gIf`@yq.{BH(trt5.To;rW|p(M2zXvqW [vS:? G%x" ?1'6Lq^6=i<Ԩ`eP!7+De[{pq *QjCuuWBF_%+rߝO|s\B[FL8=+,Wtn5{=.JEH%K:hl"Xö*DKm9ioJ(認QQʧ1(KEggLWq`RpP7RpO9KG‘Q[w<}>&6 ,Q F`}ʪ}Z\E 磶N@R!k+amM'eRȢK6G[Zhni@A#I-N<ç> w(+]ǽtؙ EyABJȤ:ƝYz- #!yJ*:iioUAul6g%A44syҩ, >_VJRaukj y}KT#>}/όn7)[q^W>~HŚVb\"4тܤոn SA=HWWBFNuuH*i25>Ij~HM5aaRDޗ2<4Cձm)kiZQU3{ (FQq_y$l,E{[n (UܨS3}Mo^շ{ٲ=V^f$){`)a_x߯{C9I7H K ˯͑#+F??,+}]ɃgGG\-Epۖ!dMXc6N.Q'TYקSOr7S=B===ۻ|5k6g^6R(U!"UdM "\&=z9qxrllhllI(% *W"'piIޖtH'fQE7~_6=xXHQ`lrrN\|sk׷R֯_CWWeYW zbhY*EfcxxAsddx!b#DlJI0 6!)҂5>¾($/!yt`k %yuf~inRJWD'O';0p^MM'l+l޵k+DҒh%QӴob81A:;::n3xraa.HD'47M#[T u˧Ss['=if@i T4-e\@hN0xniy?+zmsE՛zM8IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/star.png0000644000175100017510000000741315224505336023527 0ustar runnerrunnerPNG  IHDR@@iqIDATxt$_UIƶFX۶m۶m۶I]ޭUl({]~ս_6\7Wה&o4y5M-/:ug3޷ܙm\K2 bi c,>d;Ȍ.V]zS |WѨu4 E V)?ƊQ?g U^gۏYw*zSA$-t#5 l.7_I~\TaY3׫Ϟf_8;ssgTX;ֳ;_9t&8@m7kQX&d#d }&0VfNݵꣳpDPTd*ϼIar 4tUG4~$T:|ef!"y+T-C djԏa2ֽkIX~1tly!?S[QvoRQ({ bNDBCr9'd{Kf0SͰ0ܶ ,B+(-76iЊxf})=p~gd1J\ pt LFD\ 4O zۻ|z؟ Ƨ.WyN xICTyMmu(RekΉŤ9gAGh"@&_TO2'%~/3a3mmݣl_}?au93QOxSM8-jtkIv?x‹I@D (Ay<^'7`-X5ӻú63Jl3#:}Jvn| OٶPDm=\aH^ opo]:ǏT 6Q?` v\k#9!a#G@h<;<g WKf?l;B7WIlH4L.ߌAԖV8i: j.m[';6ٹa5uӫ^!ZBE~Я?_Il_!5$$ς{?i:5 g#g bߓ~%Y*[2Gt2 Bmue&_ YG_G?w.1B 1c }̥t?,7,mTtX^V-GzgaW,%wQPvJhh:t3~?xm_95憖]$ "G6^v?fSjM-CjWdMq*2yxc&|a<jDioAPHB'GGqjϙ{`L;PNZOQKA ҽ4f&X:.Cp#Xz9|#mG]W/x,xbHiԬ! 4 N@uw,PO7Mĕk NEЭtWWljqT7.츗oKoҁA!Q?aغLB_vCﴊ~豁q/jޟ]-1Yo|?I0e8z¤YQXPBZG\i;O,p5=0pnMwvo/&PKM 3Bj\&:%Lm|/Ѿ G" 3[YtAйG*.4 _;CD A\z9$K #5á8~UTJh^xaO,}{W3e"B@ qEpyN6b <3% ^hx&;7n|L\SjvwL&8g!L*XteM7(T#崦f/Ua.|Q{L(74-}!BXtKD$9U]WD]J zcsw7Cj U5 w A|鱒ӥ -1Ѷz1p/ _}? X0HFߺ]6r * ^"'4 XN 2ʵg&rʓ۷b/߯\ĖgR_sXT ]hxoCٱpp/琎:Tż'RG{yk0@RűlE5O(+rpxVD0FtZpa_x>h L\3 eE7Zad}+8(,AO:ȕVJvH' ۄ0>Qٺ!:ٗ`_Ux`Ki#be, C[V[{alYȠH{Cd*l?,%|o7ΰK~Z߃{~?/_̦px Xc#ޞOʃcmi7\! }lvA"W7ϣ%^&}B$fbma:.ɰ9_{wl,qYz2~}6Bko^߽~ͣw2cX?̣o_8.ːߘѳQidžǿ(l?,,I؃#cIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/viewfolder.png0000644000175100017510000000262615224505336024725 0ustar runnerrunnerPNG  IHDR@@iq]IDATxmzw3wm16jvP j56fmv/ǞwsOٝ8Չ'N8q℞?F+P xí@--w{7/xb#$T8h}_o3ܦ;XhK1 [ Cq#0G˺l9+qt%`UVm3\٬6| غV `h~^,W)̫_oH|pg~R+3Q_m2]goqz 䫞/FY\+$(pd- + ɀR[=m+,h 4^csIP*`@WXqo'ЉA.@j5e@!@0 H=` &,@Οz+ŷ9wN7\<•]+C %P4[;wF))Xϵ=bvwq rܦ-RK=(ę T6J͑bR^|o|zjcm'FRlvG"+Z\ߣ骅$5_ojZ lfH8;wvZ(6=Ob8$KRW:P&R+BR]=H"B䈍,B&15dWF Efօ"V,@Zc!$GMmRC( J28!"E&ݜB5&\(z3S)44-gI"4,"DWDfh"DHb~ѣ&CJ"$*&rO vIմH9IMH,@I)EgϒRĞЙи*E0 DϞ֧) "Di}<.@hE($b 8X6Gi&BhԈZ2h췄&Rv=ʎdA1C ' I5=J 4J%g?6bl3F>s=Zf]0"2d#Ej=NZRP*aP">3(690 be+5)gI[[+mWeIK#ۜM%%m MiEkX9A1!xH;T{ CbwXXjrImE,f흽zk>o=}pW>UN<~>}avl'1 T"˟^$;wn41(p`OL h @C$f;Zi2L8㯕6G !4IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/loadermods.png0000644000175100017510000000746615224505336024717 0ustar runnerrunnerPNG  IHDR@@iqIDATxܖd1wtm۶m۶m۶mX۶mLߟٞX~ߠfl1FA8LA0~ 3x9_lJ,$|~<5uVqÖ.>%[ $&2:]E0AXR߅Fq(Y;,G>Mn,ט\|lP!s1]/YZò^Qd]aNNdlFFdA s!yE0Fnx),΢u ui!taB'DUh0Q˪ذѮj'!^_3w ,8m8mʖ WR6KڰɳCUMY}@{¤jn]_fTsYQ^U r kK`GXu@3ۊAXl˖jYS(Pk.^x eWSd/թbG:3JT:)Ky*o]cn;EF>7/PcC79I ݑT\`C L D!"AF6X9wHjXY:.< }h{r-Hq ]" bAHnph,DIq /f>_V+Xgq :5 ;Xkm۶m۶m۶msnϿ=t:dU׷g!q< TmG_^zCl#$@O=i;S'!qj_0.>8Dpy.ooM~\s|#yq bqOx<--Q-Q_}vsĻ zYp]iho$8Ə3o(l.|?qWzra#&]FHd,\=Yj237F'[SCn䯺797 qЈ@ ɓ;\Jzuf<XrVGAq9}ou+qE<}K1,']5G] =FҎeLLEN:q.y <]X4X\0`h` D /Հ!"'76>ywFu{<^IϬg 8`t'ӉҺAY%iI,7n;y&ǽ>]u[ !0K @uMy/BMv~Fu}%U!;x_/+{ Pw:;3l㑒0K-u~coKG{2ga} Cbky7ZڄnY ex|$(Ιb(4,e؊ P߉k&<#oeX䱾Z& "Pn?KjD=A٠Id5doʖuMecTHR%Y`.qo/NlZOwk-7<kDC% HTﲜM4ĭUM}pz>He)1^8O6 #ﷷ>yCҀx'䬡⟹oC( LB7yWyIN_H4(6>MDU<ώ#{0YrE >1+䄼xb'%_IKMR|⸴"Kn]y1rPdnJuiN 6ֿ{¥5X9֦܉dV&_Q&op?@_̣/N:@ вvtK> 5q0*I+ւVkuejoZ$h.t@jGqͽsYJ&ǨF"SǹX*G\ت5 Ҷ9Ff|Mx9 {bܾstT1L}^O יSxlO˷A pC/U|JLXߵZL:!5U=&I=dn4\o>Vx-C;5?YO ɏB\dJ F0@_~1Y1a'4 [ 5M OV]4\j״ drs* QHIrt arzn~DgൔO*9x0AqB"4JA]#1!z*QN/ii zzUuWOX`ؒ>.@v1G2W_7@V_Z]lz !ډg $N$\zh A" &SUS7RҲǂz}C;& s `~ Q"L5ݡ_D"ZHD r2S1h:  7Q!th]bn6u@i8կtPU@h gֆe)eI)E \&K"L P5BP_q-~h1lqVq,21HTh⚄Iȴm .M@Z?F#P16U^ i3!E{{AJXP1jWi։ O@ Foo'.|lpT`"X5`02F,wO@*568plPAZ z8@_mܕHCF), eq(7h-Q=Ŷ\88> Pr$!ϸ U_`QÖ: xRl(!{귂k|wcA5Ӡ%g\?<xV&0.'<`%aYaC `{D#  `D MX뇇w)(G8D*P\/ם _9yJ+IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/copy.png0000644000175100017510000000403015224505336023520 0ustar runnerrunnerPNG  IHDR@@iqIDATxb`0"فXن[ I!,Hla^^^7===NNN&G0c ߿wҥ;?zF`y~ƌBBB #Hw9IY0ysFFG4@Y9bFN@CBx ⣋gjHʻ,,;-9'w)K))Sm+t3 }1qjjm6P)GfU5ō7N{e% $8$GFm]PKe="B)A3m]H&e+:n/WI|τUV5 ^ןך [ޠ>:t:Q?\LB4P=EfjϺ?Zy8hO(ّคx %@1qy(utB@tG#Y$ :z /m?7A yH"v:xH|»x<C"']_>3 9(믏z 3+}؛"ˢчa GâGAl| PU~\F|  ދPx3~830YVO6y+=MSfo/;еKO9JjZJI> heIo,+ s_7wV6D2m2;f!̰7֍T9H#v7yڲ8 ޛUUU$PtT}= x.SGuxhhGssMexU OY,sDס PH6m`L7$IP5zu5ZZLV9.D_-b>*ST Ip j|>D" -ԋ+WjZT^H3 )'4x=|Xt)l6Rk_6byK <-H8>B,6 KYOBX,!O9ϞsTpZ( APmCp5-KOF 0,a||\D`Yd7Ғ E Kp )S%)1v!j1x3pؽ2Zo uttnt9KaX)avOG]F `8 X(>ۇcٲM;vz)vy`~q^{ڦnT*իVct9?tܹ4j x_Sꭾ>5֭[y1xveN:yt}x- 䞬1#Qp~\)'q#U~t1 ']z]{~1xM]4MѺ!%B%퉅Ve"GVP"/@&I9!67a˖->}:7<w߷{Mz^i &_Es{MMMu ?\C6u =P$OÇ.@' p%D[؟X2S+B?.y%< ÃQEm?%oDž&'$Ō@EIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/bug.png0000644000175100017510000000734215224505336023334 0ustar runnerrunnerPNG  IHDR@@iqIDATxdoսmm۶m{?ֶmkFmzFIRtnwNiFW*esb;ʲc/w^|hZV2vC?` 3\A$e@ Np 3_<1zX\Sge^vL)' E$,!D3 ϔ2#Nkr"o{V|)<ڊi3>ᮖ|USbs&W^S]q<#F**++y.jf 9ϸe4"}IK+K-xbvaw}Txnîqr2(xbdȑ=QiףTurhl 6ȣ!8N8 {0,YWl9ZpЦۉ!Ç?˝G@'=xȓ*oNDxC>XS-.Hyzyrg{J>.ݻҩ@^}^y^{m[y RS]AAH4ge 0Fױ1҃yw_z:Pq+hh"EV`8V[:ffTA&ҸStq')T^y'‚#ߔ?kTsh)eg35`B! 4緀a-aU^p"0/+|$?JO@D:L9XΐsƶgZU]W۵މz:_d4<ӝ8"rw4ߪR8 $F?Ѓ:`-FWHtnMY'!sw8U{F@?"u$ >OGzߡg `K \!T@=owmN”ZQ1@pB#fsuΓ )%r_Ma%O]oh^ؘ`q5#&k q𗡐,` [z/Nٜ@&cOԭ!xȳ!+J5w^ (0U h@-"h@>޵> BS?IgΟ^R2cLMJX Me$r!,sI_k4eBk@a׻G>׻o7|RK>VMjlwg C[O#B&9Xc)[yA_3&a*0\u ᥄Phkۦ6w:Z@;%B TZE׍NWᕣD{UKT_Sa^F#;]Ae)v7L[!8 4< 3!)F5We dUׁ;5 uqVGCD2pU?r.6<#@F 1hDXA٠I DGbѴx;O1dlabUJ-z}-堞< λubdێ)Q@h2X'`aZ@X B!F`RVKZ1>ঌ 60t([!Ӽ Bf:tCr!`䄇[+Rt< Qwlk<8Ux(r(!`e--<}euXCA=lx!!Z f^P X*SYL~Sc#!AcV\ XԠ`gP0EK03@QFoy 'c9=> ¤81`b֧ǀ@>@j*@ #<.wXҰH0}駃0!r${pS|$>@ wWq@x/-63/gLz* rK#p]4.Ec{l q/ArX]vY,-9e8B=F?ʶdEո xsf߄fL(DSH^v`@v=}ƳX-ûF'73`">`MKcRn, #8&аY) 8q-><#߶Sӣ?+N@Ȍm} kbl5c%6u-=rXu=XX|ʢӺȫ) m! :O}I3P 0LJvbv S)K4CFr?e*.?2evPn9cdxG[yj`̂unNa gX@MxjXj̦{2{ta-[}uo~8ov(i~0l'lYsBp ARe4O栔^P3 MZ0O f7: J>&b)KG.Q#T)b`_fk(5T(;=?? `bI!@ Qlun)$e9%%M*KJF(iHd1>{Ay^jCQWɘKqr)L$Jb}$m`Of6OeH0SrLKefffffffffiMچv8&{klgfqs@IwߕlXB>p߾ů}cfq'\ohhmwoSCGG|X_YiEmEhyܛr"[ヒ 9pĤmۀ"7ph `H6wp)~Æ]vݛvښ/Q poNM9,*샂e9K ^_4~ȇ8J%|>&n&si+abFaM˖ /; E J``[EQ0Ĝ*$y E*DmmFI,ҢgcP#GN ŵˀ@qD9$8:rNWm|ؖZ96;޽{&):O? P(&Se˲قQ,"=ӠZ@SOek (/*%ŵ<tID>!@VUZDZ.@<wwKJ"H]]tDʄ>I瑕"HxhD"s3\$rrʰmh P(Hkk{XHMl*(w9+l*5@*j03eBET,0Ht9;C:fff~[ZZ@u X%) PefAQ+%$)$~IX 2B&pZ[}{?ڢ Ʌy&2TWXEYJ "x~ߕ.1| <3/z%eQ( Bi ^G(ӭx#s tX1M]83i|hl\ wm[>]Ջ"@*. s Ţ!1Qj]Ro{Q >$\AeNh8ƛZ}O{ٸqD33sLNOq(sP(3p!lzBU}!|>IVW} I^uܧ}g?tv-% д>tmٳG]-֢॰Cl] VvQWZ^?oS5y+:ЃaI# sA9 E@(<ɤ䖈|@O__Y ,*ˡ,%`B Ry9QZ[V O:S OMMNc@i(J"%Nd JضO/B= S2Y`:xGK[(@X)4 jc{լ𰥜;7́|)*#H 30A (`%&pŢF/$fjPdKP^ l;``e8.(BtGxš(Oz̓P|(9, Rc'WG\<5 ,֢ 044X\j`:ZԈIJ%UA@`y=~4r\u g>*z{gc5^*qVyZT̊%2X_-JE'DL'@eP{ |-gg£TsD*~Fyp-dPWWtv@XTB2n۶LWZCm0`/M5]']bbemr^&stL+~sytn(oeW1:g%4M&L}}K P ]'n͔S✬ X@sS#K;`wɱ'kH~AB 9)vƫG Q 8$<,g6$ lް'qT?6htJ^ΓVxCF&o C ݝl*Z6HKC-4Ԇ4ThL c1mdWё-gI jgD:0%9q_6xk;ǫ1/xk>q?@[]=N>#~yIwgsWȇ<59&ܕW|nN,GFmm+6.tY~ N;DҞ|"O9shxhp֢ 7^7رFc@QBL䧳} |,Ƀ "Un H?QY]Je :'H$v4-d^YfRB2/i,- L@ MBc۶Ga|&Cb4=1uG!S{"(,i2B ZX.:VF @{k}#>Z{ {>ٟJb}m.KTtӕ7_sͯ& MN~{^*+OQ䚟''I8KPFдHXWLxL]m|+Wq{=kʙ7nڴ(7zupuU*A,[C$R3u]~?99} .Wb/R̿gsKm}^"TJEau#Ře˟:9;HVlMfY7f=HCm=;4UqlTtAT6qK9t kz&.n/~_ Ν+"q?h{ķj-̙)xlyJryUr mS_߀9'X|,A|@/H: jrN0]28U|ظ=>>ryxE8r/0>V?HX2-JC) v|ds0~,~ښtQT aQUŔ*`yqpo%$9gF jf9yTvxx?W?%_߻#xcLN`NO`R(Nj0GZzi~ϊ$ >|BV&gIf`@yq.{BH(trt5.To;rW|p(M2zXvqW [vS:? G%x" ?1'6Lq^6=i<Ԩ`eP!7+De[{pq *QjCuuWBF_%+rߝO|s\B[FL8=+,Wtn5{=.JEH%K:hl"Xö*DKm9ioJ(認QQʧ1(KEggLWq`RpP7RpO9KG‘Q[w<}>&6 ,Q F`}ʪ}Z\E 磶N@R!k+amM'eRȢK6G[Zhni@A#I-N<ç> w(+]ǽtؙ EyABJȤ:ƝYz- #!yJ*:iioUAul6g%A44syҩ, >_VJRaukj y}KT#>}/όn7)[q^W>~HŚVb\"4тܤոn SA=HWWBFNuuH*i25>Ij~HM5aaRDޗ2<4Cձm)kiZQU3{ (FQq_y$l,E{[n (UܨS3}Mo^շ{ٲ=V^f$){`)a_x߯{C9I7H K ˯͑#+F??,+}]ɃgGG\-Epۖ!dMXc6N.Q'TYקSOr7S=B===ۻ|5k6g^6R(U!"UdM "\&=z9qxrllhllI(% *W"'piIޖtH'fQE7~_6=xXHQ`lrrN\|sk׷R֯_CWWeYW zbhY*EfcxxAsddx!b#DlJI0 6!)҂5>¾($/!yt`k %yuf~inRJWD'O';0p^MM'l+l޵k+DҒh%QӴob81A:;::n3xraa.HD'47M#[T u˧Ss['=if@i T4-e\@hN0xniy?+zmsE՛zM8IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/status-bad.png0000644000175100017510000000354015224505336024622 0ustar runnerrunnerPNG  IHDR@@iq'IDATxlH'։Oc--Sh)pKC Ðڿgɪ$8qI?ub0o[~M g!)"7%޶MR_ৎy}r1˯8OHcpB}V$31o6d=jIW$J\xtgP”sIpXǘ`qA{n{ f3LݏtR+mN^ّ'^."./~m=ge]S?ۺ6݇wRғYpZYXw ûԿuhHˊ$mV_~ǥ'%wʜqԣgSւZԠ (ga}1kD8;[5F%QƘ$#Gpp/ (E@J>;Cpd1=%n{N51 p^M풾C!=؇Chp ozq=M JPD/~E6Qp;Вh5ڲSm('eRo6lbmx YfOq=PHoSF\,(cf-485r7Hn#6c+Z,|?gYt)'Ԅn|zpag4f͚@'ƆHEȧG_~jWKn a:4?g* ^ aJؼU'>]l7hPK_X&u[i3?'1AXYg՘ (C8.2,Ǟ ~b>à?u,~=1 vZ-N[b!<,C6'9H!w> !vw ̧bzLG-UΑ `4B^/e`0)(~ZcwXGa8* UҵH/aoaQiBpըyArn'Ua0$6 [+}L$Wjm(nEQQ|\atY|ҫ*jJQj2LnTt=ʯv#A0 ZC >P!|sѿ 䪧 nBR1 ~qQyҏ a{{L*Wz4ϾT`+zx="062+hhI5k zo Ơ g64o\Ƿ՛ b a5Ӊs-}& i^5[GaN #&*1;itز;wdmIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/refresh.png0000644000175100017510000001175715224505336024222 0ustar runnerrunnerPNG  IHDR@@iqIDATxp\ǶD#4Ir233{0t^'>Nٲ%[f4wzt%˒mW= iU x j7w>W`%LE'O->x|IOgm9_^9X) K>zG뻫ӍJڬn[o-~w0w4`ЈyqA /+0#={k0 rOA+LM1UbwmwGS)&k==7Ե~sj`a$෱\$N¾|C!8 8 }QLEԒ&)66Z~;vRmUQr|CYW>pF SǑQSIx*s^Sx FVѐ;wHJM?3>LkА~eһt笱W?xoq$:`EJ>OrSNlL(Hax󏘬OXv%[>)FEHC$>oe[ "6'#f3H@["}?w,h'0Ϻyl @$C7HU 5翞T7SO a|/ XO 8<4ԏGa\X>ϕk_o3+XXr:ߣE "~!mIheϼiPΕއu(2Y%;j崁~%44+j;Ķ٭f~$Qȧ.Mfm3Z7o)i7r[\N\}o};olǩ!x4;'ܴz ">vٯRo`8A&X_sm}ƕy ckz[\G9m77PaI@u6?tu[O{ >=QI;!MgacBؔ凲uf+F !6W7FdT}-FtF?h罍!&vlcḵ/\~N96Dbi;;~$I0t}S0֪-ҫ`HboɝeeFG98鄓@btڗ㪭Nb= /83kͱ\rxO8!k0]ʜuYlXdP #7+>KxkKpjs{zXo,Qsm7' ጢB@Wa&18 rQ.kqKQ$hv7jYD /U(W}*XQ&{6D@9\}Wq9®.~׹+)= " !cogl?^D,{KUh~ҿQokЖX3#s)e,91@ WT0~ C=CtpK^z!(pC;&"g^ti]fwշZd>B FJOO׿dyf喞%u}]2mυanK֚ r-~yO"b%[\۴ EPt"CRq=eϡIїb4Dd<_e TۚnPhOn%o8?kri7=.jw;WpMb*~ЫuE{ɑW,[n={Ľ)ž(J*Z `b~խ_~(B74lWe u=Y&H**z>U ? Oah,npԽ,RKai'NƌP &)ˆĺn^G?:FϵwOHhB7Us?O4[ZHAWSk%~l谩@p/#KSǽo0X@d ` M'^9cma-c15$iXASХzK"vkjY!N+CdjN$/Pye{{xƬV3bKκn@5?g6nںZ⌒Ԟ){cmS%* :$oy| $b+5޸k;q xeM[ߡ0Saffݓ†uD`V8 $I8 f3{υ(@QQxI9vl|;[ Ӑ17ưF&/OoBLPkԨ֨T+T*Hr^*6LC@C*4IdƾfEح6VvI.?Zml0Tm;EP@8%#ɋ܂t(H$~B[Rk4H{ izk&9uu2+Թ4j:R5 9o{G  ""Ж@bj0nN]LZ7s6Z+g-Xq֪6p(jB "U0F0"JK>tM}!}5S2T8%H9MJb$UBnږbIpcUe 95 g+޼CCIE5QKv Kq4F"  0 H|If[!n0V%=alJĬ*cx55e2'n0#io{}EO!/_g!|Ž cԢ ' %U}=#1]%@9֖ʹϒHk/Iows/\UˉHNX :U2_lCŒIXi֐H׺L|a2`Ib(c7O akδĤrR:oޓ02xhS}N75_picl~<!TB1  Jp\i@v>\3F F̓ןr|-k :f}x%Β-'^mcS͛fsrN @0JÝQ~iS֭(/cloYrNvfn8#"{1,BQxY" x7I3&$yGކaa,0wnk`@&kOr<̎ثbsw1HǼ+o#t=7*U̡uNQ({r,MڹMW]< j3̛⌢`6`D +FcՈ *Nl "5 `G4x۞FJ33t, Eڑ]P~*6PtaP xi@1""*(}+^x`!ӦzҴ׍0%g. .m6|#B+hƽx wj<ЕCWt$3`.[?Vo%J]kg>8AC^BX޼ !0c>t蚧k 8 ѬyPAtl[+mp:Wn1sUyflsIY2kF5]yMbP },e+7귿{jJ#X r ע\uikV0v .d9}Bp n޼G@ԛ?;Lgh.|t~t @p8)OJO`X`OR/ gl84| Hf3r^9.ֻXIc'Jqտ~疵'vfy]An;FqW uv @qV19Q(uE ݳuL 8Uy+gg*YH1 ć.(rgc0SLhx_zZX&gj B_{4'B~~U\6iv2< :aLaA8!tR3by۷2Wo0^ʑd'koK \^qEedw @4^ Rs[gw8Bs4"0cz 1ý%k5o%,N; LqDZLsVX٩<Ց~ P?_+2IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/status-running.png0000644000175100017510000000455015224505336025556 0ustar runnerrunnerPNG  IHDR@@iq /IDATxtkYضm۶m۪g-al ImKL;{{}[!6>NȧqoCaSxQ"J3("YE#7ERT^E}N9)*Y:`_Phͳ^%WvPxS)*JbM@PSt.eCY4-Ka)Z j<SX5T B18 e PtƏ4->-8$CІR : @N#RLʓk"!))BBifZδiiƿD1bZ4Rtf1<2%L~-z` qA P0x<0[f9a@L(:u?i/Zko=iLgZȦ!! jRoy{!&@4k=άXTFD"'ټkmyuVR~պ>4L:&sI24 =R@j ,!~!M(" @X8Ӫ֠Y,q| Pֻ$"eeYn1Ex1[ C m h7n$R<=@h%]VMf BTRM>2,~ӼU/fp:pXkZ!5H;a8Yc_a#dU uLL{`!,xApSdԾ,Ҏxda9EYgMXA!_?f+V+Ww;/ӽ{{Z@-^=[TuaAۨnشi6=\W žiNU)Kq7\.GK/AtLJ@T5 2%nY"@[t#żn:Y^L[eE*&L/ųHvjeI;i$p̙3 /93A2'`q"="5U"Q/N5n7oI9> @v–+6%dsXgfa~衇x=,yiUo,w!x(¾QDuH@`P"Hx<NHHye][}4 'O : D` | @6o8$$:(9dB=a zlE ywyGk)͂VLN#\AKZ@b``SRRxѢE|{24 {@N aeWޮ5(><)&~[զl' 0 {O?{/E1-mǮQ \ TfdYl~,R @ᴥX'~-'t(wo*^ :s& 1 @Y[bptk{NpZZcK {@`H ,uM݉RT0zNL%CQ0CY!Sҵ4YvΝ%A%% SUĊ橸!v}.) 9%-IzgC,X*EM}txAXT3ū<$,H2Nȇ~؃k Ў& aq̈́~Z^GchL}}Ўq|,odNGq8jMd^=UXh}ۏG| "03-H!SE2êq*> ۯ{0א (\Mɵ˛$--h!q= g)aJgZXiU 0ksynUgaFU#0[`RMzKzD+CC@&?< G\{_~:P)~Bq8VE9og{&;egwşS;n-u/י鹽T7fz +[&ݧI(}^L$Kv׌a9s -W#iKbV%5Il-Mݝ^hzd{IK"Nv^a/(.*<7f=F-'7#k8:g?[ dr1V}:Z_l5tt)3LI/' H`W3Xdp #􇓗]nZi `oyC:u K/xqQUk`Q`'Y>c|B o|X g_B)Y|/ kqCףl } IkGzW"UG,x_w숬^RW"bćB&}4?{yyu [,LvX.F& 4>)jA<_!ݗ!R0˒`?kuFϾ5#C`G5@0!$+4M & TϞL_4oII$qO|EGf)8ʹh4j޺oL67RT앁``TV-+Y76^`**geF /dz*=< =q~̺P,eA \|F P]E z}PgotzMzj?=84L0yK;s t;%,h Ok$K /cvO ; `TkSk[@1hP`_Lv z4"H8ͤq<` L(dG6{Dk֩dqS\'HW1yLZw[*K:|I#:@ 0/6;/S>7did6)N/@|;1/q;t'gn;*a>Aklvd}mFfl]~}T8÷!T]FﻂNi(^b? 0'4t!zqa:&nAi <3_, /60kƐ٣Ce=UE@ժU"%UתfvA.4^MM TT\SzSH)řYQ=7CW^d/ٔ[ UMs*O6ILEݏ> z~`Kfg!|ƍ As3.6k_-71b$gӦ>٤q%AO|!Tr:CՉJ4;Dy9ʕ+wvx{՟}`d0 dzЅq!@Z..c%ezJ_]}Tr,vǶOya5ΌzkaV~ڞ'8MA)> pļf'|_ƟgLæ-=/3pEdSH^!4*p ٟen>(7gJ}M)ssc|ח!Nƛ9: !'< XE%a1'6?+T:=D~(+囯T ߯]#.I-M<٥+X%SxeNCL_ J<{Ĥ UUV%^~"LŔA]5"i.69lndq+8[y# 7o^lOQګfSb ~2+ʗ+A|TA'l#Eu,A<#YA` xaNTc5Qu774gw@'C xq|?2[gu +AkTM&#&mOW>i/I<wbuD&Jhت18k-hkP¾NZ8hNԼ8mή M'uj7l{ fd' Hń"DQľw`}Vdc'U(_N:2_mXhee~mrb!H"`A Jgx+v{p@L#3" \ֲC<˫,/jTֹx(#ᜥ_SlNܙCVbk\ cO5O?yoB?.i|EzF9~ӨTchBGcvǟMOio%~9[ˀYR\ܧ.A}d-7~ê:5N*pӪ5jwow@nwmBEg?Fn)d)CASZv3F{*F5xg1I{I\ʡh÷VM/H+q W5o q|)mmIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/worlds.png0000644000175100017510000001514415224505336024070 0ustar runnerrunnerPNG  IHDR@@iq+IDATxCtMwsQl$d{,6IlyT4쪭޽j__ x:|x5T 5 X{hUƠvZIy7yCעY*C+Uek}AkݮÿqРX0TF?d|s¡Cqx;w5QM!2X.5_84(=hEpʶRԌPl=ܾux >P*T10 !#zW4WZH+k+mbVQ[4ZͻtjG; H8?{?zT$ Fh>GIfU!X=a(TV h0. h歙3p(:Ѓ9<` @d7z}iMq,E0:U1,޵춭$"#愌Kعp9,daC5j0=ÝbEydBEWN_~~0`5k./q*/>EidM>^&Ka(19X@"K|O|ը,jɫ܀p_{~%@5<(=T]́rSňbRFB74[ ItDy鄔g. @K95w aeq|= arg#)< c78Eg=plA&9H|;30TZ+q߽/HH2Q,W/Xk!$C4k=ahch ! zYR]~兇o,yTwKn8MWm`g_ʔnٶkk0iqfgFKkd"A) m|Oaq(lq!Ax m{jKS33n= bQ IF}Ϲ{Z -D QڸMke4g|YCjǏ\`Dŕ.Df,u֯ k) VX8%{&UKeg]yck/ąFD!a*ág8נLY,Eqynq ~I{7s@ tB8tiKj ldVeV)q=wS9dn>J RāS+2rX2\r6z5K)L6Б,߿ {k^@ X쭼OQ눬]ڝrcqןD;bN-^O#C> ڎ1"ίt[O$,q2"$CN6c|f=kP`q)Zp'țPpmw qd>f $Nm XI ]̓dҁݶ-9V+QCq<_|񺐬w/[䣏[z}{Lp*Q MԾJD;5`y͉̌Wx2RJ쵣 -F @`D$E3NFLѾ+.νV !*XGR;:B4Yx;=^ 4q4;oo l8. yDۡ( (Ypyq0=V^|-wXgh(9 C!So{>vv7R+NwXqS>Kz XaH@SkK-7.P]n3|Ӂ]No2=VV`i-("ߌaGXI_𩵂7\$ iJ2wG]~#7.%^o5v+XXo'TBYz$/fb>Yq{0 K`5xCms-9m"T-^+43Ktҵ8Bm,TnUVAG q!*Hf*w:܇jJn*3-.>T`nFy2%. l1X҂PV604Z%p1g8~Z厍cf?@Wd~b}>}sG!2(A~ko[dAFj( ́b[ı3Ce&=*0tHŷƫHu '(W"Ȃr6J)2x1Ios9jP%@ Xzc5 @q {}tqȡu:*N&^OQ:"w~]K%҄$IJ%,JI\(coq ;1 ]{Z$Hش7~hR>=N0(NbA||ZV/&_›gOqb9B3aZth>D0["#d3f&Çٽk7^8`.FH5F@:w}Srߐ4 _/]&#ɟ|K\\`i,Z_a.uv,P^ ?Z@Op#3df0gϯ&SuV]l!M+F!hT_/a>k|O\#y a|3ѓB0istњ-9x<7yqPr&i f(~枕2f:KIҌNVd*s,CC/ AXh`CI._z^8pܶM~@oWv~$ItˮNE&DaSM͸|{=vt @x?60FDRRsJJІ-o+IbX vIҥ@ sh6t8-¥DH[7ȍx%{ 厴esǶAT0*cxe Za109Z ClʣW>6BL"EBbDxH7g"z?7g ^s]^9x:1DU[,"M4-uߘwZ#Pл YCj%bt5ݯ̳"f2{tJOzS\([#tC zNEAϭUfO C{6x/L͚RfTElfvZ);s|R:0 \ 3^kg 8UHBiʙV%t`_ߪ,_u|}c\-QRNѹ^l\k3K=!GSά20J!DyH^i4m}yI<47م(#Ⱥ@kN̷jd r$T(p|5s'g82 j50tTb²4[G% _=O'zi}jB$l YN 2lTED' Ttmwk695J[wy`sVGkB . *13XzFc,3,fWgH@@A5;. 881oywo^|wowfs-7ϷR9WMN5m $;Tb$M%^n( iDPcZ_JgO5{򟨿 \(%lIENDB`PrismLauncher-11.0.3/launcher/resources/multimc/64x64/log.png0000644000175100017510000000433115224505336023333 0ustar runnerrunnerPNG  IHDR@@`UIDATx؅n$z_WWWnӘ=t1̂ g!$ L˼;`Ɔ:#idi4mZs?c^V2>7rN)ϵWZSD;(3JB!(o]xb@V8 @ 3u RS4O֥?PQ<.?/#P߿ߟ@ 2ܚy}PIh~-"'<0lӸ-OeMqW]rc""GD8yiCeSE CfK'u_!fVOAϫ)9p=ܞ\[91 j2A0(YvC~A>nJf$>khr\SKIY҇/&H-뾑زf[Snbܤ@.ZW#scEd[P߬TÎITJ|TYR(P xζL Fö9u=CuArY\ٸ1T$¦r}U ̻CU7unڼmj 7 4жiO*3'ՒbDI#jK9Vtv߬CUeg` ;}X!dG9oyκ;>U/Ia+Tg=&q2#wVxiC%m +ubBD5 ITtH]SR#jRCX6Xtͬ7cEk[ kR}MxMu-=C>`D/ܶy1A zY cr= Aہ,VB Ԕ0:0sX\MG0 h9bFxrN>$[wO]I0NtZYyA1 c'KTmMK( 'MAE׾7lȬI\h-Wmi=!+ydm,:㿵󊯁6 %=U 4lk=+!R=׬аaKϴoǺB=(`†m>5}r.hZvɒuӒ﬷$>8=MvmC-nZҳa`߬-cL۵㮊8kqKSg㞂(3wh2Wvt9PmDf!v#YoY4a߸- z,,ꂡD5={f\UWT0m]6⧭h9ez7٣pSd΃`3`|+`tNqknr8%1fIザGP-U.k++xȬԴo=b!~P8#}G|C8(%%$clH*XӔpDz;HK̹aaP CL*K= ִ]FK05}ȫ=0n kƽmOzۖ.Zg52e5sot4k5]6pAg=PE;3}}OV$;^WPvˇm-λ-5m͹mY}`ێLzMݴ5@ǧ%3:R>鶮}̔#[BPi̤ iLfHї-1 ^=oUm+6TOJLAjOݞEw|o+櫭[&Q'Y3cGlj> QT nnyu >y oHe>e ) dY&pܼ>%'@i8ݼ sڈIA[3YkN?_n;c8)t Gj =xQ O@o@ɓЗx)FȾ]=y,@?=L{d+<ܛg! GvK/_\^\ [% O.rw.`7޺SJe ?pU{-&zxc|vݐ΀O` \Y0JMEx 胄pBh_gװo#S- XxkX*\ZB^:~^ ,h7 PA495geUp`{_y! /ߑX}.PzhkޓL|SN9E^p;4WgV*\.s .!^VmTZV聿hH~c|xڪ:>IH a:_'8|ۛ7JX䞉/ BLz_LBw&6ҽqGFFV)Ӷ0 3v0drdO]2@b le a証|-[H_lv}vgg|,xY@<O}"hl^*"GqH:#C?~CKɿX_l8JŤ2qE\t!k]P/ф.a;%Bkbr_zh -m_KVc=Ҏ:ed-HÿMU4$됁;Zw"u ._:a#I?2t2 `f3xqwtmەlh~]þx@\?8T-(Al u2s `f/3G:]Wh.6DNKqNu!/ Yi0/Uz̈jVLRݚ܇{6^_w}Cd Ό(=c; v+?0DxGV? R|lWm:QƓB|/=g `fY~qts"ozO1(iǺma5+c6]mKRfC:2Cp u1?d3#6ێlA1 ^CO]ֆ@0Z2֜W4QzeK+۶o^*mGwђ`0ƛb}~T3 C:./IԷˀoVHKU}@~P¯Fta8 )#>!%A[^an?o'X 6vF}^u|v`BQ8(k_Bv]#$<^IC+[nQW ^oC6g+Ό`U8u1ONL8>#M "LTyYE-O"/q-KTz;~#!kPR5jhe|D52?e)6]>q>^pՇė JaK茁3J{<2d05#۵40U`-Cd(AVdP,tZ9F~a߉Ob}03W-,yY&Hgd\}&qKUDžy'k6E_$jnZ'{ʷd&ꐝ;m}oH-gb[m">,TfS@:Y{0=LM4H]kϨDѽ݆yd9HA~rȴ™qg:ƼRdfa ̓f$F`ܷ}XMva< vZ03)t|<' T?~ζm`,3aBX;t{[7%@)1qnO t숂ei HUvÒQKb$\Sj=,^W?بwGW]JACHHvEXM.ɔQe@A>J0}:GHD7݇WӀ,yq>}uUUdI@\D}/Do.,"u1J4#5p?Wdh/;>}s IP}'.-I)v e 90cUq]35 j@;{sajTfkq"Q*2? aBtn,&F팰Cf\؎L@SMȌ.\%5FlA>} "]%@oJ$5 Y*}fbx_Ʋ/}ib6[_{;i(jkdFY:ln yʰU'׍g_.Fp%'rj&,j}nQEWK.)VN(CrVtYcս>3^/nZ  pIH~̳o`7sUdZg(N8ݲ uԵ~ߜg+{QY$&Sq10U KeގߍѪD=[} 3 EH8K3nW4rmdz;Ӗg`la u,`+էOe]IfEsֈD<:~-y4YKu`nW*s)5hGdcW/Dp _҉dg*=j>nwQ2 %M/ӠRABB,`z}=bB 4c]6@m z~ͽjs<^cX{fk,0T\+k5ٔ8 1>e4.$K X٥%&k1үڅ {6ㅰIaX*]. ~_t0sL:mXI;r6hS_#8J0}>2W/Ro!l%Ʌ/8MqS~+o'g6^Qs $2C-ă[ʱR$rl䩻02%=(JR dFzEmٴ>׶ ۷6(̾gŌwc\uǧ.하 4AQw,l Ҩ_'8בFB@wΑ$1e,acuzSG!юU=:9?F|֔p!l@=:iSc\{׷TTw|,Ry0^0VcϾA;Ds.2Wk'ր+GP!Pح"BtI_3vwoD;n'# Mײ |0 9 (t"7`;P.2r>1>ƝIcWc~7pi/E}s/mau,Ax2XEܜD-9L|9I21݄LAHS‹S ԉ"8Su37O` O2jm *Hfc*}TލcNX/闒Vaa]dž1ՍixV2dz}"1ɟV6Y~o0۟pf[_qQyX?!.ʹn?n//>}-2}UNckJڃ_U>Lvd2'VSW{Zo~SQmPoPU.wF". 18ZRqw$BN< Bܹ0 nmC59Ƨ3ʌme3|a iy<D裏J@k .ahJ9(k;{cnPMiPs*5LƂ G>{]?_ONƯOԳ?h73h@jQɶ$`6W.w52r/y7'r2!E$jb&GUsmZlCuш(EWt`w ˖cGK3'Myp4>lYB#I"wKŽ(5mM:]2 jei] &T7ρIפMiSW_~sK'/ŋo7~wso\TJa{'+5A%?` Pm"3ZqUشj2`khpEupjT4ؖ q;K`f`!+#&Z@LH\!mD[I@هI [*D" Z`ap.sO 82gw+vNG1#"$NH1sgAGggo{P;tU~w~ 7>?[i4Xj뫷Q1xόj#:I :fSԁ&MҺ1x\bF'HZ~s1O&\]/Ď$È}l! 4e"$ 306I8j<*u74{4zOпd4QViRzoμ v+x.]m>4}umiW1NatcP6wϝKb79~aG>&pE%T֦21<$6 DBV$fo9u 3 /࿑Ϟ iBܔI"&VIhfmS * ^⽤UwЖWu_+7UMZ#HhMt ,j[huW*&Q]_}l4wi[lYÄc^DbU`@bֶ&,%FB9VSM1EtEv(mb*feP`49$Hp`=Lkb=^ @B~>I0fmLe\賐{c4MU'c,ZKZbDF]:x]N<Ic_p%>"6 _Vs x`24ԬPE7V& ƯʲH,8LzV&^Xb0tl9!O+kgc}Ii|B51;NzܓM<b vwb7#)BUId G~nv5D 3R>;SqF5810#VQ>K>Ò ș0Ψo:pNM?8^?Wz aK*1g'6>?+ڹ36g@݉ntX#pBoǞ=>F E߄jՄ^4mR|v>j0$wz|q"Fi 8HyXJTjyهϙ_үc*ZQRà\ $v% 5}L3o/GQ|gƏmcBB㘱JHXHgUX5t5:ewZ]}?s RÍ|V_E5љtE׻'~ 茉3i^D$q'ӹV0ۊwX#& ?:Pql=~ps1ȞuTd̋ĖFևa N8mGhueDH EVG_e uN㦪;7R37jHsfȦ\;6/1MlAPKlwyKu9 ̈7a"3s=3ҙEt~ɕy 99`gI344,L槅F墆h'Gljf ]9Ss]'gw{ުd ojQtn릦;zk3u" m%^7!3^3"`nibbiUX0d m1)oGsn36 -q! -ҝj}@9 ?9$m'TnW[Û|~OϏʉWs){[7yFc`5U==ј,3Ecf$-@@*I[6M[{& v~@c ̂g]&@}̔+EAAȅV'bO1(s`1lmmaFip%n7uvNbӯyy@{n̏Z~*wJ^n=w;Uոj@ ?_Ќ%{W]]9ʞ~4ܰ7vll?ޠ #3.4қoK C@`En yfz: q,5=ѣ}h6~?W]xLp*滶νze3h>8T1]Ǟ?mmK./Wæ@Nt湛:uKB5ԀW"hXj RS83Vqij\сo@@{9zͶUd^hރ4i9Sl@y*c{.Y3 ;rԇ/pL_( N+*Ph;`|tIHy$WgL;i5܈'y3̇3;&̛(my/{D'vGu w'_-}} 4x|ol|p|vwv-IzttϴXg݂x62^;9goґmwJM7:x~>'m[!2U5*|&'D'Q+i\MM[e[$_Q1ʿ`U ºq[z5ߟ 0G 5l%27K|x#;ݢU5f̩Q?ajE/#ehaE>AٔHi,|ใp~7#mD#XF[Gyu+i:D ,xC%$:3%9}=hif;]  fX)܌O,ߪ/NQmQ`(ڄ&B[9)p j̪:YZk⁖B+3{Yhĝcߏ_E55ʾ[oly_d;it&ӽC>pP}W[z2>>dvޞw Z8PT>hkw:tWR 7-!y=li/1&msl*3(5gqi$4ӛ|Щ9lʿkضzV#εXҫݾll4(vvvCas @=_w.rfanqI1P= &|xDua15{e(3#1JZםeԤOSgw6y&I21Y8ǑewnR 8 A|̼L4=Ͱhjw$Up:QEE%x_ntTwݡ 5p:9=d+F Ͷ?Xt1_Ww x{&ABx:e .qMt8 z]\]ujֲ#x!RcdnOOظRnaJiJU EFR4jfN>b,DGwgH# n-W³Ldӊg6wy;ƞm˸sYrkD ?r)KMRwV!z(Ez眳sLp9H5yJmo~6\&FOAH眒&=\"QLـ&ij-?ˍX4= rWEF`5l :}9>l\iUk[`:jG9f[Tb{qȋMg>byz,ҝ(x9cu O!1G2=y"津85!OȊLs|o;!vFT,+H$ 5HX&F!#US納17N9yїGl # VWa[g3=M 'q¤H0z/(!UĽa2lG֡F$Ar\},LAg T:R=(Fчtj0eCZ='eY}>5jHji^x[a^`~# OѨ;H5쏎9>9R-k) 5 gU BYN2MA'U G(PQyy|aiSuҞ73h<)w9}~tKvBS|rvQnVq3 nBx~gՈbFaX *^yZs#H&E sV;߻7G3gyH^̓oo^zj(Ԉx#ґǎڌIg12Ž ᒖO{j${5[rØOj(-z\ CQm=Z ?;T(E;{t>3H[ .HӾ*eʣ]=|9/[ @PPٔz ,7 鬶Ds#?HTp+?cGY5QWGG52 O7?.BTm zyd٣0̰+Ԓz5@6Q5 &ŵYP,(T/>9= c|w?dX]o]3L9=U2̌<І ,##Arg#esO< fd xFq A)zF= $" T4\eK A ,U%ll$A_ ÄÓ0q5h^I)Cwk3X酂cCmicyu6VWP QDX8~ϧ7E*2]-٨n?'{OtD΢4'##O22{ֹ!J.ZIIQ'+ Q~9j = όEI[lC~:y 0mR?&Mg2I w3txh yNRc 6gk>݇WM [g mX a*6kp'''j,jls]`LӔE y%`p4\䌰4 `wA31?p5Ϣy|˔!]| 5(Ns *+\z!ć{- }4IC Dq/t&nA]ԕG堶[VYX(Y*4*xr |Kxs+ WX*e]x*FBy =`l3Pܿ6xW8NFw>@UV "%#k2u AvgNJG+KݗyMEd Ħ!,޵3Z[-&G[|>ppkR!׋@D3$FJ嬟 H s:2:OiF=KݣLMeo4 GLGBPO31Y "$2xtzZ[atQYt녆ѧ|wGB*#?E9Q#1hExʮ' !uSfŽ?+@xh@Tz\rp2dg i'%m) 9CuvD HL˞-7QmFAo.OO]׽^ZVTaPTRQ[5es6@m6lc9XD`ඨBiդ4|qESc55B1i4<2ǷkiA|\gGCzZl 5"ֱy&ߏ=8֞"OȜ?(ghۨl&pyf7gBZRt* 4P~0ݪH_H#k\7@s i1ai!yG^kT~LD7  A766"7f`lT$K,%Xh9.73Z::t`c[i!lz?St3 ) Ե'ÜbCdg{nV;@$ "`T^<đ>a_7$iN Cg@!4D( ?q7J7sr$tW1PYJ2Ijv2ә>nG 9% ܣ2~Et0xEtzg)N VpEq_Bt[/nzfEk7=)ɴ35 <"TIw;_;zMZ9Ă>Ȗ`0ϊy 5 43(CV焢S)&@󴎧%oO29)6-B{ 31C 5"U[ m!(X2#P25,1Ma^MJArBh> Mi[M+V Znqŝj0fc4B9ےoJNWCcg(- ߧgL h"l:s=>ݛVV &+zm)=Uhhhf#A$OpwvHD t I'沲&̃- OQ].fZOZ$ 0Y^Iv"Ek/G:Cp v-7NthJ‹/󪊻ZSc6(L8B[ slM-hߙTB8Պii MC.X{gggGS_x/Bύ~cBn(VS""p(1) Q=Vq5!GWAat3bk3mUОW7Z7GMGN}voo 2rQNf׹xb"ar4oxFYO}Z`c*;p2!=4\6L-=0 ^y¥.k }27 Υx^COo$of)!͒P̈́ISh4c:!L"z6#}|x/mI*[)cml&A#\LmK#ay1HrQh7;Cv}A9ZkmHlȫѢ9؇~Qy&nWz-KP>B#c|즟Yԍz|zn;'SNbjy<px<мiA;puzӡ'B%H .N2haŀ)0r44wj\a/m4"(H9HԦ#%I|MK^dBOwUc˼8 wS&BzKZ0U`& JnL&r@VdaN@rGս CΠyvlu$ֈat0b`$],NZEu0 ]D@>Uu[t[i \GVFHѰ 1 a^#z<hX%7Ej ; AZ|/#/gč͠r:sht7k{ Ekm-MF"ΏR5blԫ/,_<UgǤ<ɽkh$ ĉΒ xOWQ?t]\ #$4OiٱBv6KdVÝwsE+A#b{}:}ȴbfrZֺBT/:?%<<*uh2ѵ.QT&w6rm2.$kԈ]謫nx@R o{7~eFFg <K:痔v@UPtRrbMOy q%! R_3rF!=fτ0 4*),TԠp=nJ}!6K˪ikyCU+P䅵_g\NGL4Y Ifxq{os!{TIX Ճ^eAd 뭩LƔIQ/…k.h|2~__xE AoCYVjd+i^VU~/N}nwU y/KcDG 1 |A o1[2ժA$ 1ѢCsy<{uk-hi H$^7q5^nOa O&eP=hR|R637_W>F )V_ 9{?H.dQړAY?{qTȦl*1 qa mS>/u fm}ִ߯E?gy JڒS͟5UIt%7C>owyO5.:$ehNhJ/YWASFiI\ gu\T'%q~0vi[P B;O5oe Ϟ"@iыqYYd vtn@5(?|2/\ܣz\ ڲO' r!)^ozc05u;lspEC8c夃 dyިn*T_!=V`?xxğ=xKa0<5w޻?FQX(ڙw>;4ONiod.@d.uIԩr9hnT09<#FQވD8\ k$Iɺy>a:A$.  ?;A/Znv,u\8ZOJ*1Ni~Fҋ44rHZ:a|}L{ }x6ڮ I:hMv]_5V ޣY/+b#״MڢMr4OZ1scQynJ` b@*x744*轎-iN+'TBy(C(H@Eo#zʯA4w"څ !-(O)0_Vxs89.y|J="' OHe^BHb zTq;?V`b%5agMu%!Psx sVǼ[{m}u8-bA9mx~u^;s{?Ovp.TH@PcN M8|lE~/`^Y'^H#*:|0BRMkŅdkHA 4QE,O*uVhQ>-piqUcPR'0g?=&B rSlTԏ{eLEeضrF@W{O~?˵o|P?N_;v|tMZ(YS~o nKJ"+)*[I)_=Na^˺ dS'_}~,:9G7?|-{!}Yן>OvȴsEh rM=K0 sMǟ儺nLf.ւ(}v+'5{R_mV0vtT DRϪOMD/]ZWݣ{4Y oCI&˞D^T.kBI KIխn1O=ڃ=MU@Ab?OJSa_ګ ~;G^bیl[4\d~*=5#6J@DSGxꝒftfecxR['1s늲jOh@ݠŬ/ohZw2LXIcdD?Xk+ "6= pek! $nDf}l (-b0V4 R EQͼML@DSj9zDaI->D8fĝؽ>6D6 1>AH]Q!WV /! 6'nE}=)YҢʿԄ޾,k¬~3za|yx-._))\\UՃ5q/ ü!WpukMåFi2'-f XD+E2xp\=ac|XpϏhmHIhd8  OR$ %꽚? |I%KZ=RVEn YNC}(J(59h$hb&3[6{m~;0XI2N'c$i/SaExk+-ZYN=CzDlJ!bۖ]*-Q՝;'ɬ+gor|˜h[T6zB_YK2r4 =icc߿ "0Mݯ7rdp:(t{KSCftwSa$F Pt$gt8ډ*:Fd%9tZPOq?Iu+VyP'b%lr6MmIV[gqcFB 1FTx$sOMxܨ h$`H)LO#r1o:Tw0fdh&0EUȜ0sv4"'Z,t Zҍjdwxp!~q NH?'v[%qUxdtӌӺdt.&P%Np3<"5*uA$@hn4TR-+9ҨB!-A[aj5ww2n+ݹ' AFz׺-^K7u3O EA 0kRUCX-zJY_$~'hG\u3JY5jD"MM(Ǩ!x_u&!*'":tzqmVρi-郇)\l4DV^G!wy+oK&_V3=})UR_/U[dHReu[Ɩ;ؓ5^s[z3<#J& QY0#v!^U#n26 T5q|FieX;N9fV fr5VgjU[K4g" ΁CHI(!i{^iC \KV:[xyx<:Æۉ5vOl l]dSu3;_ϥFI]a.eљX㩥=?}n9HF@ÝsyojJ1AFhI=r`H%+Ɵ 9_ |?UC^>噥o.5!OtG-Iքo1c :$_?9MpLh,lFU'6;WTPӄJ|sҴ1ƪiE97c-LjKGЊr0 «eQb6OPޜP))^)2ZdL;H9.%!m+yCW8'm2ֶ @[P&LSjX0&#ZdɎ@h<JhK(yfd,gN*ӫսҼ|eXBsע$[, @"$ ?!to0yx!*&-q 򁇛+/ahh^?6}EU{ .0>p|ʎ%'vrgTmwڮo>U lc ><8R_R's'Ѳ^>z_T}|R\LΔj4\JIZU Bz.w Ԅ@Ur%@qyz/WHdK˙ݙ!([ 9Hb".lG5Jy9mqk'A_[PFi'eZ*m%^܋PhA>+K1 m ڼ "Y8Nw"JXX=A >u\yw1OmtmZ T#Ӣ z%lohn Ǒ5Oth}y:z{B{cJD4"X^, @l{\Vo\v'P#QŽW ~:?j%zhO4b4̽u xZ=z6y,ӎt7b!MNN.%c[<y;٭ųT48PlG9QZΘ/l^w;SY τ=;Rϡ!H_Wɟ}yR1 #jwDRbą r|*njqc~v A`qx{k^;feMφpȕ)E^I΄4U#m 8_)c'֛RyF`>dX+?4ݦ43Uܻn OU|Yd8,jj1}K aL,u{FZIkG Gr}yx'K-Mρv爯ټ{`6º.ͩ>k9Zr 3 ߚ{#3 N rnc5`R)!KJ[:kQU@,e7x }|&#]3.4? w&z. i8&Ph`ӷn"n Yoz֪~|" ?gf?^.޸kx/ g#9we|TVAA3~Ik"VbF3D:'\W{/XZc*:կ#`d$d6˽]P/y+[C%dwMÐnT E*98gO*ㅉ6~;#]TB*<`7H (8n*v9# |؏OT%'?*}0AXjX }κA9ex64<4UD"nX.l)nA"s{_&lgᶼW@ķ NἤM0z 90@ V #j"ypt*OxdbeC^o<dd?DX12L` URar*Si I&1v=zDP|{ช35[Zch hL @>>?ͽZO xd4\9K|nԩ_xzFUQcxƾ+j1'x۹ L'ͳAӧq,ty#ʑGx_*Hӯɔ- /ё$@@Ul1.",+JM˅8Oǀ#J"`,i-']FLj`ħWE)>aRJ#I|"8$;vE0dg8/ѩC|><8|p=&jS~e87j-2@{*aJwRй M*`[޵\{W]|)G~2B+JZϺ;LM0|k%lc /(Vg#aps0e1"5$8@I?ͪye,A38tyv"{Q*y,jQMd- ߱Ŧ(3Ue-"<, 92JW{yq/>oͨf4u}{)5N=̠ ʰ8h~ ˽%9ibtk'n[q#*I)Q oFqtā $QہK凝Ew:,\8(.勔ͯ|{=@@hƥ7څjGñi'RiM(*a/*LgY]vCJNw2'5$"kUm?Kb!Qߘl^o,AC֬nJFdɈ`ր^ /R } A(N"> \4ETN bLݦQ{_/:9"^zl3lI>^*Ts XuWqC:ץZZqhg۶xn2Yjg}Eϗrт3ZPtwoK{5G|+w/zn1kƝQo:̀lEb &J 8ztVǝ}HF53# 匌rx^s`D--Q'V+ۅ?4IfyYΟ1Up3j6~[1E6% oa|F}N^KY\򈒒BT1T8X׵ʗYwmDHԘN3/o:s¦3qt&s9p8(-1Z\yw8&a kl%| SI/.y0~͗d 49PgԚY~ITpj.>;mVհ؇ҟfTx`qhL:0b6]&Ȩ :sY'2XH1j} @C7Fz[DBWߤR=MM='1':da'`1;33|gx$5mG!óy؇=֌5!cŹSX_CQN7wo:T4נn$b;{XtYD8OEo1S.E.8fdߤ; |b54+'YD%#} 3JKLUNNA>?/n_7{s8>7)}n M#G8G~4X] C#(Ԛ5{#`si6hHp2JI+; aohb[Xbdk>5Jdmjtt&ӃdxTFS F ]`(&G(b$#7|)=Q stDv ?ZV*KCstN`g"~`iģ!y̅Lˉ좧`Z̮1h)|`(SsqQʞ-4 ^ްM9&lNsP'qdz*@V475>5&`m=D00D:k[]&_mb  5 d[+QL8}lJ6\ώ0:3j3~{j5_V47ؑ?gMwmڼ}s5; v^X$ Q=yv9_vƖܺ=^8*8`Gg.! uӑaNl펀lEĢ̳C҂slEE6}~_;xH2w&kYo!Ej|f!śoIfy@)/[bk/WVV9t4fQgZmR۾vg 1I&8?:iFvMF0Z8Z6Z؝Fκʕ54<j5HI3D삲ه#?>@F!ʆoDn5 VkZ0 ?.n2]j! w]}XįmR|$JOgR"::mھK5䋩7RKt`ࠞM\Z?PmIA-3Q9dw$1vdfG{*y5l3K!q"7q{,"U,O!‚ "Дnd=P\-_;6*XTg'd"ofl.]J@6ooftA !jY>)L>HrݢuQŠ{Dvy!d4hpl=Z ^mbAm<$A ~wyC~'阨hj<0g">j~T刏&kh8~FhWMJ/ƲFSzNsGЀG7^`095wD+FBkܓ?,\A X3uͳ P;So@"8RzՆe0>8fw>*|SEX_Ǒe# 5$\nlTZL ٩\yiN}a7+2?E;`R&#KR O_MOY)%oD]#A6>ə5ޑM0Z1lDt4;Hso|(D@+b9 MRk͑b΋lѭ]p|3#׳(Fk%)#^,= d.5G`npFd: ]ϟxQLԲ!?>YEop) 4uyv݇^}a74|?3k، Kbg`-0zs&eMYNO[Ц"%<>@s$ҌxKI+{9v%pM>;մBym{U}g]JRo :lY% }u2SkKz=2A7; =7hyG*^M(b$nmM\n$UF>+q47[pz6mŃȉ˙%@)E1 ΀ظ\JTlrYhе&^(Vlnh"8X98KM"؄&ɉE$(M7;5|H, 1bT=E~CV0(,S548~i31oۼt!ƙC,[ν=!#Lpy b\7tYMujEr]io2mMD.`S|Yvp2w9 aXJ„af1`7s&3v3$2g czjYcu 3M,4G%ؕu)oHGfc:ܤYT#z~v.CMkP1Zų9IΜ́`Ji/4奯փ$qqFL|j[3<]JT̞`&gW)]1 x;{.u5Q5g\1Ҍb/?~$JGGPfONDrF섌HF:im邩0!k棲}n2 T?e@ 4A;>F# \mÑ!~PtF̚GlyLRi}"?1 IB죋rG)Px]R_u.=fĞ!XQB`2zEӋwj^,0j6ìH2,0o]Kv) P`dRvO.U5"p!7VQGK~<ql@hT3`"CU2K%#~gI6US_QSz.%C;5SbPCa9.ork 5o?ųhD槉qQH$ue]4&sᶚiG{M?5&Ox+`ZǙl\m q%!gY~w}WWKI9);39wfk}XywLsi4OvNI%X  ˦]2{CF<3>ӷz<2 ݍ~3}tuQ0GvgoC}ܽ񌠄m# @ ـmRɣ_f3яbLĥbj"g'#d#\ Cop!ѬZkM,#+r"$ԁ| jAP'cfI163G`,{7|OH=489՘P Cr"G\lsFe|PdzDp9LY 7Dá$@v;/>L$)3yX&qRZD}:p~ѣO]ˡ(=)%p CCpI | C>/cc걖ə s,o5vLƞT)I4喺`15G|[FooV5 eCs&<~OHd1Z~7cIW0 GN`yAh_fA"Xhxo |zCWcCcjl+Ѿ̓Q&QbxH/wp6x9*Fr1:c؇RW%`?*l*gpOT{y%ifIv-lj<}$+*Wvi W3ҩ9.Qp~ E fLexߌ~%D.K#@ƀ,*mr*/dd`8\ )"AQ ʙ'qRs{"NUeAVG2dMvp˶4P5@Qs] OHZOp6 WW}{ PNB KbM⫿}ij?$#:㸌jlw~YJG;{ xA>>Ksˑ:5HADwTӑhmvܙdR~ &!V6$DTN!"5oG\Cn14z8)+gEhYZvO)7jgB[Xy$Z95 %6e:²pQ6|s ]@DW70ͿIA  p.fUgwtO>j:յ 8xb Nǁ#22 ҈L4h.Y71]}Ų>mJ"ģme kkT6zdw";=h4u_Ssw n5FYUSlzy1Dl8gR#VÓцx4 R\-H%#yxWL \*d $x82grQp\!T9׊UG:gOO- dϒ׹asG`&GZt,: q+IP 'I*İ)u|؄?8/IPȮ=Ppr!_R>sJ[wn1HZȼ{-|0 s2XYKM9Tj Bg/0Yxxt,${ߓ60eX2Po5G9 9 $77*wͱV=i_&̖o`ӓ$֌%XUƁيx,'O}bSj@Ѭ8l';ߓޭoIkyCt<s6 ߖ0Tlf _Emwު;۪s}ϔ]LsO8/aHuamK)?cE9bW?~V[GTi"KjMÏQԒS>l.zԱVw>C+ QF.[hdr/FFYkP[FP.AL`l_~#&VkgB,C]N5YC\{S\'y.Pf*e7[a0\ܐQb0"Lamn[h&ZEko6Fs'[k&g~ ?l!OT pJ_C-n68dm?8@IGJCԁ@p?}PBxk$ Wl"Lj:_Y\$oM䊿}ɄBɘe򾍪Py6q&Q6zb9Ni ;-o o &{C~\j8U'K!xO \?rOlIg}H&՜[K N|xicy;x=>vH?IN3Zu6mc Ƕ,6"3U@504x bzqJ`Fro]A 8G1ICY97湬-ݝ?:u^P ìY Se[}hB'p6ͽ˭wƇT xk-ρy!Egqsf pڶ qs:L F\őH ?yW8l~TSsrsTv,:/lîrok98X&D5 :T͛# _[F߂uThlo5A)>Vi} !{*!kX 5>; e5#mj)p= D@\G0h)O%Q;s ֙Jdk$,7e#&)9Pީ$yDG` .g<޿|qUs*ˆҀeKGpFa|~ ?n~GD⢍@ & 88Fn!LH^|kKŤvW#|ro|jNf=B 8ϲ, ,oRrȮ>Fy~yZ|f9cqiYuk?+0<1c 6æ$I^pO[zs0V U-F9rA\{ːINOKMvr&iV$E\drmz8'o0ʮ<]f ~vH-FV=8\/9]M/^GpiFZvG0BOs([@rY,-"(tS >#>Zi26G_L)2^%iM|'#1Җ![SʲxW!/S28h-7ZF^%ʶ3Ge,Z`;d]cĩT'Q1V\D#XdnrnRqH&@#'f!Z)h>}v @rєmT`L5~b/@s8D6һ@ZuGEhQbŷROnmd0:`2Mv|qo?<=wf*8J&3nwV, bj;g\N)0ʹcGӼ,nj?c켴v؜8 [N8oGj}E޺!&ۿT5@D/[A}nĦ]zF4>4GBwJ#5y&Y 7jy#kt oF0|e#9[C.%v^eN5~ - #K(zAhEqSlߐߓ_8ϵC8pn.zG`dMG:OxԺaE6< ܿ-nGΌ@ ;/g3+F f\ّAO G .em{!J9_wΛtac gKoߕސolxd1$yp(Ӡ[% _fԿξmoB\]fKP9&kWcp-2c#wlʗ3Ϧ"dh3N<Gakt\APAΫ6Ksk[:PڥQfAr+6VL1sl)(cCUn1}@Y,Mtvn?%K+S:4Uh2o߿*}:QhbƼ 1]}" [9Upde`"F81(dަ[yK ~2S+(o E&hao6ռd:)&dN%# '9ԗ sl7!U%8:FݠzF]w@v{s<շt÷猬!G5oEcuΌӅ8rKHnp q,~# TEK-Y \=rc/,D᳋Iۉplq{,Z cGc,է:2jp`8‘-?!C`V[GtmsߎaUšPDq[8v,#Le&8{ 5b.͵^$eCq04,+u$,ZSEuyh%\6"c){32qLʼnT,B;$z35L5^:K8>d.U22ǡlUv 6/C̃$y.]dب Ch-.:e~V2 & qtycՕ%ɴ\ӌ/(7\F"(2vx$]^.dO ETs}S:kW$t-wϿş#բ.; Gp[6ې}T{|f,b[(#6R,bg۶-鹽NO36yLwKY}=IZǕg(Di5+X tss7V$~М7~HOCsSsTX=c3>+,) $< 4H^_nzRd?83I,9-˪p㧌( cӨ~51ThЩI$*`Ί0 G*nvpѪU4Z,iaf*gX|s|U r"c6=՛vↅ%9ZZźp5OpNuM&~ yHAym |OkP**GX:~7pXH`ܖf-['@"I[|;LLM2~D5 ,ip } +t؁[qgG}0+ ||,t`Ay.HS&rbW'Cg`1%U'C 9 }@*t{:DUǿC7BQ AD::\6dy'h*V5QPyIUH{%ˍo`1«5*Y8Waf}Pr񔨂툎<]|Iyr~P'#Ήo`\',1GCE ̥)է;# 2,.֓vT?W* :{LȲҮǣ7H_> tqۃm*@+#*$t x.⮾0m)`g1 8qIeca`144d:ra.z*#:AcK}6bQa'>]rEG`ѪMnɚLh5aƏG8 o| [(87"7H[vy+6·Fb@xtySXܢZ5.7o9([ Y;6= I;c n;tZ%DzP?! h5 = V, < ,8?;E=/_, ,^f"Bѯ <o$-M a`\X|KKBqU/\b4ʹ0C-ߋ㼷G8\w8IENDB`PrismLauncher-11.0.3/launcher/resources/multimc/scalable/0000755000175100017510000000000015224505336023016 5ustar runnerrunnerPrismLauncher-11.0.3/launcher/resources/multimc/scalable/shortcut.svg0000644000175100017510000001270015224505336025412 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/multimc/scalable/screenshots.svg0000644000175100017510000024130015224505336026077 0ustar runnerrunner Golden Picture Frame image/svg+xml Open Clip Art Library Golden Picture Frame 2012-05-24T10:08:07 Golden picture frame, Landscape http://openclipart.org/detail/170182/golden-picture-frame-by-tasper tasper clip art clipart frame golden landscape photo picture edited by Paul Sherman PrismLauncher-11.0.3/launcher/resources/multimc/scalable/openj9_hex_custom.svg.license0000644000175100017510000000021415224505336030617 0ustar runnerrunnerSPDX-FileCopyrightText: 2017-2026 Ronald Servant SPDX-FileCopyrightText: 2026 Ludgie SPDX-License-Identifier: Apache-2.0 PrismLauncher-11.0.3/launcher/resources/multimc/scalable/screenshot-placeholder.svg0000644000175100017510000000730715224505336030203 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/multimc/scalable/datapacks.svg0000644000175100017510000003470115224505336025477 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/multimc/scalable/status-good.svg0000644000175100017510000001471215224505336026015 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/multimc/scalable/openj9_hex_custom.svg0000644000175100017510000000766215224505336027214 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/multimc/scalable/atlauncher.svg0000644000175100017510000000207315224505336025667 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/multimc/scalable/status-yellow.svg0000644000175100017510000001316415224505336026400 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/multimc/scalable/java.svg0000644000175100017510000010764615224505336024476 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/multimc/scalable/reddit-alien.svg0000644000175100017510000002114315224505336026101 0ustar runnerrunner image/svg+xmlPrismLauncher-11.0.3/launcher/resources/multimc/scalable/bug.svg0000644000175100017510000003221115224505336024313 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/multimc/scalable/custom-commands.svg0000644000175100017510000034246715224505336026670 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/multimc/scalable/checkupdate.svg0000644000175100017510000016226115224505336026027 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/multimc/scalable/export.svg0000644000175100017510000003732715224505336025074 0ustar runnerrunner unsorted Open Clip Art Library, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons image/svg+xml en PrismLauncher-11.0.3/launcher/resources/multimc/scalable/launcher.svg0000644000175100017510000000662615224505336025352 0ustar runnerrunner Prism Launcher Logo Prism Launcher Logo 19/10/2022 Prism Launcher AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke https://github.com/PrismLauncher/PrismLauncher Prism Launcher PrismLauncher-11.0.3/launcher/resources/multimc/scalable/new.svg.license0000644000175100017510000000012715224505336025751 0ustar runnerrunnerSPDX-FileCopyrightText: 2007 KDE Community SPDX-License-Identifier: LGPL-3.0-or-later PrismLauncher-11.0.3/launcher/resources/multimc/scalable/matrix.svg0000644000175100017510000000503315224505336025044 0ustar runnerrunner PrismLauncher-11.0.3/launcher/resources/multimc/scalable/language.svg0000644000175100017510000002047315224505336025330 0ustar runnerrunner image/svg+xmlPrismLauncher-11.0.3/launcher/resources/multimc/scalable/centralmods.svg0000644000175100017510000027064215224505336026065 0ustar runnerrunner unsorted Open Clip Art Library, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons image/svg+xml en PrismLauncher-11.0.3/launcher/resources/multimc/scalable/news.svg0000644000175100017510000003577015224505336024527 0ustar runnerrunner image/svg+xml Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce convallis mauris ullamcorper mauris viverra molestie. Donec ultricies faucibus laoreet. Donec convallis congue neque consequat vehicula. Morbi condimentum tempor nulla et rhoncus. Etiam auctor, augue eu pharetra congue, elit justo lacinia risus, non lacinia est justo sed erat. Ut risus urna, viverra id interdum in, molestie non sem. Morbi leo orci, gravida auctor tempor vel, varius et enim. Nulla sem enim, ultricies vel laoreet ac, semper vel mauris. Ut adipiscing sapien sed leo pretium id vulputate erat gravida. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras tempor leo sit amet velit molestie commodo eget tincidunt leo. Cras dictum metus non ante pulvinar pellentesque. Morbi id elit ullamcorper mi vulputate lobortis. Cras ac vehicula felis. Phasellus dictum, tellus at molestie pellentesque, purus purus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce convallis mauris ullamcorper mauris viverra molestie. Donec ultricies faucibus laoreet. Donec convallis congue neque consequat vehicula. Morbi condimentum tempor nulla et rhoncus. Etiam auctor, augue eu pharetra congue, elit justo lacinia risus, non lacinia est justo sed erat. Ut risus urna, PrismLauncher-11.0.3/launcher/resources/multimc/scalable/technic.svg0000644000175100017510000000512515224505336025157 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/multimc/scalable/new.svg0000644000175100017510000004331715224505336024340 0ustar runnerrunner image/svg+xml PrismLauncher-11.0.3/launcher/resources/multimc/scalable/server.svg0000644000175100017510000360147515224505336025065 0ustar runnerrunner image/svg+xml eJzsvXd+6kzWPzgb0B5wwAkbSwIEOCvhnHGOGLCNjQETnu6n/5j1zD5mY3MqSVVCEiL0r/t95159 zLWRVPFbp06u+NTZ5Ypeab5VV1JJOSbF42a7Wuo222sx/G1sv17vdbpt9NXCxWJMUZMyPKTv517o g9fVdqfWbKzhW/hmAb29YJX+qlVi17WPRrNRW4wtXO/vnpye7Mcs/XrfWoTHirVuvQoPfjcbv71q u9lWlWSptsiaAGVapS48oOZW5PyKKstaTEmtyVl4wGj2GpVa48No/nMttpLS4CcbU7QM/KTh9l7t otrxPJNJZtCTSfwM/kwlM/Cs1Sz3fqqN7lm7Wa52Omaz3mx31mLm36VG7Lj0AXdKsbtqvd78R8yo l8rfEvQ981Ko1avQzZ9SN5ZDfdb3FfXF6NXqlZPez1sV+q/KCvo69YJLvOpAUVAq+h19nX3Z/4Fv LqvdLjQR6kPjdrFrmDDWzR/8GHyHr4WHi+pHDaag+bRIS2w3Wz+l9jd6DfUKfShqDn9mUafQQ8Xq T6sO44e7j3uMOuz+Rp+CfpABUtU0fOS1WCqjxtIZ2nh3dKp/1ar/WIudNBtVMgR6u3tZ+1cVzXsO /ZBvL3r1avuqUetC4zT0VZ4MwHGzUq3Ds867hXoJ9xtfivtJHiiW2h/VLsxis97rYnTlZHoLBvio 9HcVTZJCKjhtVRvF5jVu34qWlqH5eShMSUPDcko2pmq4aC2WTdF6FPwFbQx6Hb3MSkUAO4PJOW3X PmqNNdqm7Mtuu1ZxJyylAoToJ254Msf95NkPaSF0ttutNmiLASrmMTf1cvL4Euq0GxWz+YNGu4Mg DpPeAFDWmx/knvM7vgOv91qk/fjvF5iYs3atgcqUTvCd3MtZvQe3dtvNXmu/8d6UFsiSPit1PwHM 1UalA+sSPbRSa9Dl/t5Dixmt1wL5LQZrqPRTK39WYXG2FkMLLrZLZag/dvr2VS13oYyLWrlULzfh N5j/Xr0Za5MvBhd1WUYj1o4Z7V7nM1ZsNutcU8+qjQZaktC2d9LgKvxKegCPXXbbGLTNWMt9cCab s3Xtv7hiwEgb5gbfRMOP/sRD+F4HuhRlyP7+eWvWa50f1JIa/t1tGfl7cCFnuPeN0wYBid8gkAed HrbIG/7dL7VR92X1v7hioEWDKz1zy+0CLa5hTP9PrNYs1eu1j3ap9Vkr+9fsFFqmj75DtQNQXi6h +jPpsPpRX99rjQqsrcterYtKwjtorNz8aTU7sM2yCiitqNZLb8126V94lXVnspqih1ZAOmhV36FU dx2xbu03/oK9pNwWuuJ+CRNSBzSVIgC1XmqU2jH8ff+E1brdXrvEiGYJPeD5yqkh8wJUnafbKytS NmY0OKq+2y5VajDUwCVdNeD1aiX2Qb+KKYuS35ewGaVjRkV6kDakgvjPxpdVMAsGXHohX8jBpeEr DZcKl1KQ4SEbfiy4TNuAS7fzds7O2lnJ1uyMnbZTtmortmwV4Bn0z4RLt/JWDq6spVkZK22lLNVS 4JLNgmmjB0wDrjxcObiypmZmzLRkpk3VVEzZlKFJtmHBM4ahG3m4ckYWLs3IGGkjZahwKYasF3Rb t3RTh4f0vJ7Ts7qmZ+BK6yld1RVJl/PQr7ydt6AqVI6ez+dz+WxegysNVyqvwqXkZei7jRoMzdFz +Rz6l81puQxc6Vwqp+YUuGQpW8iirltZE5oD1WXz2VwW/dOymWwarlRWzSpwyZqtQd81U4NGazrU B49ompaBK62lNBUuRZMlTc7AIGag+xkDtTyTR1Vmsui5TCaThgtYMLhkNCdpOw2DmTbTMAppHXUA 1ZnW0GPpdDqVTklpFS45VYALZiZlpWC4UjAcqXwK+pGCBqa0FJSZSsHTKnyqcMloslWYRxXmSYVJ QAOswhCqeQkGCLoP7c2iZqDS0Rsq+qeoilIA1hamH02uYuILZkbR4crjC42Ghq8MXCklJcGHii8Z XXIBXza+THoZ+NKdK0evvJxll7SFIK3CmwSeCJwImDlAZRp6ImNEIiTmAItZjEHURIQ/0wTMYNxl zBQBHQacLQHmDMCJDnjTMNIQ1hDOChhnOsUZQ5kKjVMwwEyAGIJXDgNMy8M8wuyoEkYYoAt+EL4M D740jC8VI0wG7rSAwYXgpQPAELgQvFxoqRLCFqCrAPhCCEPYMgBdOYwwDSMsjRGmEIQBwAoYYrAO McgMDLI8BhnATMpkOJylYJJk/IPwZgHeENZMwJoBWMthvOUcvCHEYbilFRdyEmDOTpkUdwbgTsRe FrBHEJhOpRj+ABcyQiH82IBEy0EiDL9EgMjhEK2ejAeNKUCUqsrwKQMiCxiX8NmHTfiRKEB1ClC0 thlMswBSTUljsKbx/ymK15Si4P/JPwe78GlLADYEYouC2MIXgbJFwWxiIJPf83AxaOfdC0MaDSUM pnftop88Hkf2kxV+NO6HjDD5LSvRXzP4JpsE9xv3YY1ODPvJOr8jwpLBn/C/hClHmlIQ8X+Xpri0 RcE/aH4VPL+E0hSc/y2JTjaadBNPugXTTsgQ+STfMKJkABJcPGTxZw7+yuK/AB0SJlUEJBoAhAAF N4YCJu0ARoANJWcMNg54JIwdy6FuJkCHUDnyk3eoXY5+Zh3Kx6ifCyz4lAR8pTG6GFVUMM5kB2UK wphAJxnQCMww4CQHbwRrpoM1A/+Wx//nPYjL4R90IZqSc4mrrEmyhq+sjGgB+T2Nf8e0gV7o9xS+ 3G9Tzl24MKTx7Bl4xrJ0StJ0/5Cd4S4442vScSUjmcMLlI0i3kckOnZkzFJ01GQyXmRFcjuKRReh 6QwGPwi4+5Lbb/xDLrGrpGvkUvGPeyneS0INmeQ/abLF/SlwjGIQpOF/BcMgjVdHFi8lHaMMrcgC RqOK13YGL/4cJhIGJSQ2ojOU/iD6hElWDtM1A1M7G5FKvC2qmKhmMIXO4Y3AgM3VwptsAe+8KlDf NOzHGuYF0R4Nu7UEmzbavgsYxQre2tOw0Wt418/B/q8DJ2ACT4B4gwLgHfELKeAcEOepAS+BOAod cxcm8Bm2pBXwElGAxKYwP5IBjjYL/Amw05hbMTHnYmcLiJPBRDkFvE0GczlZ4HeALwLeB3FAiM+2 JcQUwaUAXUhhZjwDfBMqDfFQwGYBP4W4Kgu4qwLeKxXEB8B2mMbcl4bYb+DF8rqO+TJTAgbNAoEA mDVY6YhAq5iBSyOeB9g5JEDksDChG+ifCcyeBVwfSD9AMNHMoK0G8YNp4Auh1xIwiFlgE5GYghhG w0T/kPBiAxtZwORXwTsXmg/EmSMmS0OjgIUf1HhoEaoFC0WWBNIRkqRA6sLUCe0viOtGuz3MFZoJ xNUhCQNzsjngaPMgcOlY8DKAxzWxIGbZNuZ6C5JdINtCAVNRsqkWMCeGZTh0ZfClOVfWuXLsIlSa YM9Fn8Khz8WfHwJdDBIUIgzmJcw0mj44ZEjUHCQyLFoUiwyNBI8awaNEIWkwSMJVwIgkmExRTBJU 5igqOVx6kJmWOHDmKDgdeKKBwegk+ExhfLoI5TFqYowCSiUKUwXD1AWqhoHqQtXASEVYtTFWRbSm MVoxXiXUIAxYBlkTQ5aBth+2GLgUtzxyDYJcCUGIA68LXx7A/RDmQEwxTFEsURi7QHah7ILZC2ce 0DykbQxpdAmwZsBm0HbBzeDtBbmGIY1xgyiZS8sQdBzwcCSNAciFkAsiBiNTwjhiSJKxTgBhCaMJ 6woInFxqxyDFQGU7RA/BCouHjPZlsFLCiy6GL4owhxIyWohAxsFMcpDGY42gjeJNQByPOYI6Ricp pZQo8hj2RPTx+Et5MCii0MGhZJocGeXR6OLRJagEkS4mCSoFXEoOND3gpESWxyePUB6jDKUYpxIF KoOqCFYRrrZdEPDaj1jArBQKW82XNnsoNKXS6Vj8xWhLclJWs8gqB7+A1J5Po1+UDIBPAi4nmQcu G+gv/ALEGLHy7Bv6+obEcfrsyjtKFna56hfDuUznomNLOi1RjkehMkyKXhl8MQEoS4UkInsblBsy sXhOJolwRcAXSZg1YgJbhopzRM7LY32A4exWsF9h1QESMZGomeL2LCIEw74lwUJDsrSzccFksI3L 3bpylH1yd64C3rkUrG1IYTUJ5aEkWO46VqeITBRjo9DGRZiofN9+xe9WDgcl0a2KkBdGWggD5e5P JtmbOL6J7UqEZhh0NwI6IdF9KI2JA9AEwrnmfS7dc+U54RFdrk7OVWxQ+VOiwij6kwqoVAxjAqzK AUOhQluawiNNVS0uRDSJis9oCHQHLwwtBhYTkVTuyOhIhMSipIo5atUDm4yENUZIN4A0BkhzgLQI poMfE6shCIYYz82UGCmMI4akHOGAJMwCESzxeGKsUFpAFGWHBEzx/FA6k5IwrLIYWHnKEpkeZKlY aexl0NG2RrBlUXQh9bMqOeAi8NLJ1Kd9rpQosAsCPS//as43TB2AdQQZR3DWHEVCltPXMhmbydyG QFIYrgimbInCi0MTU7LRS8F4IsIVjylGdIhawMGVRBXPSH2Qo5o+lwblqfrGcFWDAj2yKMIKLsIk B2IqViRlKMzQlcW6yayDtDzVUxkcvTKofstimJOITgyDTk5RmkdVbWkq+Lnw0wB+WU4HSPSCTNOK tmBLopC0qD62wOTVif77U+B/psA+TRPVQOE1K3GLGS9UYiPJ9V1eNiPLsSAuY+IyK4x9wfyMhO2C 6CIrlDJAlB2yqKXQpEwTYaDy1DSD7YaU48pg42HaTknYgqhiK6JC7TYFXA6zJiIGT8c2xTy2KWYx P5jBlkViW6TWRWJflKiJ0cRyjy6YGbGhEa4U5lpdWyOxNvrbG9MSNgMpjsnRNTrq2BzEDI9pZnjE 1M7f7ogNQ8QsRMyOxPBowt5ODENZanhkZkeZGoVEiyOxNxJro6wVJLwDmNTUmNOoeUzzuTJ9V1q4 UuJ0UzswviQ26QHTblEO2eibfP/pz0gEAfjqh4AIAi8M/ICQkjxYKOAGETD0w4HYnRkgUhgQIiQs CWPCRUUOo4LiwrVEC8AwKTCIRVq0SauIK3Ot0gQfuoMPgpA0FgsxQBzLtMlBJOdABEAiOSihOAla 9UFrPxsKEQoLaRA2HAHICw1bgIZDF6QAdAj4oIKaFyBeiGCQSB6UiDgRkYKxwkPFDywSRosXL17E 8JgRCYmB1TYcMZEwbjKOXdklKSJ2XPQQwuLihxAXB0ASpTAOhlwR8UFSkMNhOi+r8D+S/vAvShZE QuKgqGDhEH5Jx9RYKpdMA6MUy6lJFUQOVMalFH8Zqwyj4wiqMSaoxpigil8nYmmMCaruN2pM05KI yY3l8tAAOccaNLkCJ9K6fDopw/KbXOtYgbh1moZKQ6ObzaVTqFQV3svkcal54LfRLRkIBZqVdA6e Rg9rUFxGjgHj7DZr7JKgPUALmZsSc0SK6pvk75yEvZNU5p1E2RvXosLsKTYWJVVqSQ== ce0olteGIhpQJMpUI10AYZ6Z3toWrCaOxYRTVLvmkqwjjVlSxqZ66bRgIWHCfYEzjvQbRphZxFUY ZiRfy4ireFY55aDGmUbygmLQVQsqEmchSTuK52CFoKgMFNWBWCEoOfpAVyPoVUb7q6NVRx1IFIIZ rA7MSdBmoom2qe0khVV8WazSQ8q8LRcKrnktzMAmmthccLjqJBDamD5A8zWz+RnZHLD4GNlyUghi tD7zWsFjXmNKac6WITkI8jew6RRHpqCAVnhjhmhokyieTKZ15owZqqBydhXOokGDIoupmyUBWaJR I88ZNVzzG0GTq1jmlMoAHGDSEVpSGBk5QCQCRQFDIo11vflBUBDBwNtbRYurwVtcJYFgiGAgMnmB 0wAxSIiWV5sHheSiQgCFFxYiMEKUhhIzdwkGWJ7E8PpD0d7ltXhhoiP5GCp4q1deMFK4YAmEi8Th pc9AEWABE+1fHAECqKhIvMpQ4ywiK0BOMBlBpAPbX//3qDD85HYmrhNwZ6naDIZR6kO4S+w4Qsft ggTSOlYz2RjIbPtD8DWRax5SeyNShlSTOlFHAkxTeK/LAzAtgKQCYETaa6K5trFWEW1qOcCXmbUB TCkAUTanS4AZG1qbAmxkAQ4mAECBaUfqZh0muAA0IA0zi+bVhgWv4hnNw2JH9F+ltD8Pu4hl2UCx UxKsCWSE0YETt7E1JQXkExlCdGQmKhBtFvZeJp7LxG85i4WWNJZLmM8ycQll3sp+rsomlhRARpA8 fsrMS9mk8qLjoUxlRK97ck5wTQaWXuI8k3XOM9lRD3DKAV41kOb0AoJWQApVCviqBLwaAeoamqKO x9gr1KJux3nqcpyhbqAKdv+0qauxjl0/iZsxcfkk7p421n8bxL9Toh6Eaaw5V7Cjn01dOHWsD81i h7w01p0q2O/Lxsp84utFfLwyWH+L3bkkbEywsIZYx1rkLNY3p7HzlUIdcf5A4Q8U/kDhDxR4KDgq ENkJKFWwqJlBfzCh2O8eFrthQHNqCoVuZuQ8koyz6O8MDUuVfcTryG+MJ0arvmK0isTonCBGM3aZ l5pEp0TCQvQzEBmOhXDEaJnnvxSZiOkq/clQxjxDjYE5+pN3/I2J661FbXoOMyPhjy3ksTDc4o2w dK2shJauSxZsrCxkoU4s2CnraDvTjlJTcVSYthD0pFMtZVZClUPJiutOEeqo6QoLvKQQIDziFlPL JnFuznAijUYN40zOZT/MO50aK4kLBZpRZ3gjkrEoRAy1kDffuGp7qo91rTZ+NhuPxUYcSD/pmboD pgS1C6d4Ic6LKerXr1FXEWbsNwS3RlfAg/lyBkcIJBMJcyBZxrU6WmPXwKDTMfVamxzjgthhxwuN Sf28zG9QXT6GFmHkMzTcw1VRIJMv60jfbiHalUijHU21RafZY/ISG0hlSaxuII2xMIW3qWmZVOzE MaEYOfSUo+kG3IgFYkkUSaFMV8ErKniPy3SQXwv1BKaOLZLjBMwUELajgGDevxoVLl2/X+bzSxwn M46zWt7UJeYtSbpbwEuXLd+0s4DdRYwXMtoaAxxcdbyZUjRLjjKIQNpVBjliPtqmsciVwls3wkMe i16GK9Q70rxGeAAYSHcyxmYDRC5AwmzAGEyAlweQGBNAoOJvW9W5lagJ5g8+ptNdkw6zJDkrQAzr JOsgLwTepbD+TLCeYQWKENcpseg7cTsx6Wbl3UzSdNNi25Z3I8mz+FlCRCVq3/KjoQbdz1wKKtJP au+m5JySH4nRH5cTgkVvca6WiuC17vVZJ86+xJuSd1Xn3dQ1iTpL5ql3JPGLtB1PSOYBydweXVdH 5taYx0On4x0Z2iURh030PvuHhxsmzMAkwvWeJQTCVUih2UcEAuGB6MIReSCKyixVTxJ9U8FQJKqV JBpJosw2qQJboSprDesocthf1SK6CqyaJC6pWDmJdRYmmlbiAI2GBmku0NT++9RVTBFK+DqqHKJc nepwCYw7wDpwygukKGeXdfSdhLOjPkcp7Oim/R/Ss4kqMNUJqxIVYUwNZniU/inJE1QDfA/hAYS4 ZSc80A0X9YQuc8FnbogoF7wsCYGhxKcs7YSbYe9GLijPjb9zw8tyjmscFlEk6m7HQsZkKrg6IeMs qNcNGs85xFkIG3dItCXEjWeJuCZxYeMKFzhuOiSbEW1GtgnhZqSbEW9Gvh0pjtFwoOKElRhSAB4k /0ocpxVB/B0s/Ur9+94A+VCRk7IWIiOS+1hOVIiQh77AtluZ5P1hht88sqw676mpZCbrSowjvIvr TEFPMug9hf6C39Pw86gAYmym76WySS3j1jnCu7jONOzZKFdSElmKszn6Xob1IIva7LQ1gzIeOVUO /yqpEVmg0TioMPOKxl6j1vMsar/TUE1NZhSuxqFfxTUC2YFGcXZx8hqZBnhPmI+sknQhMsKrRMvA zO0ZfjYU5iKQZvb5LH6QvJ3LJ1PchI5eBLQAu3ZgJKCmK0pWw9DAzh2kHBg/gJ0ziV61x+gFjKcF yftqQfJ/nAn+OBP8cSb440zwx5ngjzPBH2eCP84Ef5wJ/pgN/1iQ/0DhDxT+QOG/3Zkgm0+qwAQN 6VDA3hpLnPb1KVCFvKGY13CZZq9LAe9QQNgIMbtRn5gUZC7nJaRUqLHcIxpJnKlYlIxEkTrAxsrM m268vSSYJrmAe16MIKIEi7ZkokSORjlzWmtOwuRzQjmChSQkhzLdoGNnCFOOgOEOpc5kTk7upGMq OZkAveKn5cgargiac8QN0x1o0ZgpOePtCqN4yEXHEAUvGtVxDeFDyzMBTiI550dn2dkkvB5NGh5u 0U/iP2IzDpZLOKE4MeB8HHiKxoCnURS46EKRddT6efp7znGmQHTCdBwrLOpYQSLC0Y+JEtVJNDkg l2USh4jL2Ab7Z/PgNg9+M3VHhB8TZ1S4cXFHhh8bZ3QkboAcjx9hjNxRcsfJHSl3rOhoSZw7hOsM 4Ubr8QZqPsIzMPRX8on9xUPY75LU75TEuyUxxyRFcEzyuCZJTgwli5/MOHGTYlBtxKhqyYmQDA6l 9Q+NDAiMdAfYExnJG6kHWnl4bJleM73LmNDky6aEORPCm7h5cVWsIgkzoKUdFsVjPZNwFlzCqWSF LLiKkHSZGc90mrKV5VzN0CyqKcrAKJJv3mXRgskSL7v2S9eCSWyYtmPBpJlKiRmTT3LL0tw6OTRZ NmYu8ajq6hPcWF7JSfTjZmdmuWDEHDH85U0lxHKBoLzNPrmHhAuvEMo4MtaRMY/MDSVNGEjKQpL5 MfD+laespGvfRPtcSmIzhFPrsgTFrpGTJSh20xO780RS2LrJifF0SThZLUkiowszRhLRujbnDM03 y+WadbYNZnuGT8kxQVtchtm8M4M65hLYLGZpPlmNMhIatU6zC2qSuIl1U6Vw2Yv51CpcMieWVdZN ZGyR/KmSLwBMmtnYmykqKLUQl3zVPxeRNyEy2Vmd3dZyssWyDdnmhpFt14ZjymefLEdvTnIS9TI+ gM/+zBaHu0jY3yxXb9r5pJl7mb9mQOJePoEvTU1bcLxh+xL5kpRKbuJod7D5BNLst7wz7Gywxaw7 9C+JS8nD1hn6nSkJyd+acGW4dL8Zmv3HYeUk+rU3eVBayAPMLm8qIf4eyxPsm5tITCQsM3dE28uv c+y6f4Ysrc+TUMiR5WbJ8qQUFfwJFWqDYB6F/dlEcQZ5ZrvyZmUMziVq+6RjJKnvYBeTfNOJirkY xVR3bqI7N80dl+RO4nIxBmdjdPIxBqS341IySlxaCP+8jJm+DBP9qe64lHfBySv4fKWR0sUOMhtw GTglx8iU9jEy+WXf5HMfOklj3ayHkpB8k5vswOn2y77JzbnkTLoz7T45DvszHIo5DnPu/Et9OQ69 WQ7dPIcOFDgw8HBQWG4gb6pOLzCCIOILGP8UNt48n84sBduS/WbLnS9PtlSpb85Gz1KJ50/iFi0/ g3QOQzJV+s8k9b8UJlPIWNk/n6Ezahek4EmNPKfCDEqBKz9s9QekvoxKFfgElzmPy0eq39iFs7oF uHxgpYqjpJJEpw/qFe6qUFxrrZ8BX4xmwCoqiUsQ6ef00a+j6t/1BBWV1Lfl+bnOB2SEZEE6pke9 ojqqFNcMzjvN8LnMHXWVRNlezfE25QZV8JIQdYC8WZzXBSqSr7ZKzC2tePxsOF8bZ+iJ170z+Brn d2NiRbaQQzE4hWKKpkPU6IEBeaoT5cOdTJrIsOCMp+KwiIo7qhJVnbIf5nbiehuYdKgtpnOiyQhV OtwqDXwhY56VBEVh3pFNDDoFThwMFWgKNGmqmHkwg3+wOCThtINZmm4wz6kU2aebBvNPzkH2zz9L n+phe4UkfWPqCEUVoTS+jlBUEUrj6whFFeFIYSosq12G04vJjkbMFOIr+rSGVPllCkEVKErQiano 1xnmhZETx67/gDJPukBvQEte0LSK48iPJD+WeDQl31SBeTEPXFAauCxFVkbUJXKaRKZv5TWumie8 j6oPJTqE/rEpGVFtKATHBaRilAbkYtQ9Wffc7Gl87jTLTcgo+Sbew6n3uLyMIXnTfGx5uXRSUzLp EGsfe4I4EzMP3JSGvJGRKyz5gk+ulUGJuJCjclZVMhrKAYbO7M3CV30pzyZU3lgZz6CPeS2ZTuez 4yc748r6X5XnLOVnS00tYnvt/1Fb6mAulTBNPm6mfrZU0T05jE/14VIDbamR+FTGqSK5nepjJC68 k6limIxO5HP/0E4d+4Wa5PQWx2E4hYwdJACOD35joW8Kje3CQW804o3EvDkRb1y8G411k5wwt7wb 4Iaj2/jINsonBJmRGTR0qgz0NyOn/c3IjnzD4ceVczTBjGzwPCE9rQ0DSqKpp7Ncsmn+5BcSF8vQ 5XqtuvZ6gjPCmKtimvMc9g/sP4OIJDln1ntXOnJxxyJj4QEJh8YyN1XRKz6Dd0aCPbRfYuzhkxgJ 9jIYfVmMPBNHD9pEHUiCB4lTMoGdhYFXAMylsAtiRowcxDsXczDVaNygTTyQJRisDHZCzgGyTLSF wlaqAp7QJuuJFXQt6ETGYKwiU8VmKTx4uzkDCZM78KfknEBIcUO0zjR/usJhiAkihOxkHGsCM4tT gU/CufiZfdzgUOZKgcwkbjvSoOwRUlIUg2mUk59ILBrNmq45oXosdzo71U93sqi7fg/sHEDLEWmw PabAfK6pfGMzGYceDqE4Cf5l53gjmR4boTp/KTTAjf6qOMIRS9HObEIp55wA8j/zwOi/j38kSqaJ YYkcLaBRWUv7Pxh9+b+zQMX5Hx++x8tcKmdu4M0VfecZcAss51hMsPlENG8xtxTLsYwWOK0JW2Ru Cg83soT6oUg0+ijrHHXhZvNAy8r+44L4xwXRxxt1PAeadHQHGhcghegONC5W9AEONH4CccbfgcYn wYOLHwFBfvnzOaFY8knvoHFoUvsisHUOUzyqqPOIxHmP5DlsZTzeI2LkdY7DmIMygg== MwkDjc+a4WIt5cRc9/kO6Q4gxIRGbhaK/pRGXC4KifMbygh+Q+JpDJaP3C9K/hQ4EpeaQkxOIWbH 8fcY8kmkLnkUAf2qgP5E6u75DGIifow0SVAJuAlF3IT8LCW/izt2ord7ZLyLP1uiKUYIIWO6GTfR iKuhoefIU8rGzvp2UUlP/JYooWMHfzunymOIeqLY/7ORvKO4RY8uYmf9ROwsErEdd2VL4Sx07umG Kc5a51rsmNVO42x3Hgue5DkEMdCKF2iH9RzrKdniuZ7MNuefPYUZ1sTcKXz2FEOi8qV7cbbaoH/E hs4OFHSPFGRXSjhckIhDxD8i68QBk3hNNxbYkLBZ3XPcIDWw84HBTlCwc+ggHxHMbLjYncKm5lvX nYIYbYm5Fj/HHclK5p7MdQbHg2VxTJiOpS98BCsR6wfZzLM4mQuZpv9FIYPB/1Rq1srSzDI28CZU I6VhBtWk9tKUY9rzC3v+/5UU4znxW/HV7LkpeogMkKXaPZK2RibHlJIML8xFNUUzvajUXZPkeylg ZtZ23FaJe6RB3VcZEwKXhP1Ys6REypEQn9YUzQpDuRPqQUk8XG3MqRBexSQLFJ+EiI/yJYwL8Xtl jpXM+zVNzypTaRIZhfp4FRx/WLIgTcrdwCU5zrHE8dJ1kdXogXtpwQGTOMv+EYoiC0W8LwSldu6R OezQHNty/5nc6TniCTp5uhWyjVGTHD9x/jCdlLPfAgUm2YH8TtpyDlbysfCIAgN/rhJm6CSfY5WY GY2OLM2BhxZVaEKGkHQMQbkYgnJTcgFHJDWm/1He/ht+hC1f8uz5rjrZSZfGT3bwhh/xnycwQfJI F312RM7+Kh7/5R7ulHfclLIkfSo744k/6ck97ck98Yk/98k9+8m5yHj3u7BxnozceVTek6jcp3Xe 9in5GT8F4zETmMWYiQy3ElLuSrBkliVUjJ8wKeuiO3EUxDRKpCMmITEpiRw6RVwFsNIwRTzSqe5B x07nyNFcxZ7RBvYW17DDsoJ9jk3sGqwBWVb/PVvhv6dAkg7Xxu7weadLKnKppp3KC67JKSG/muPm /m9r38QLpBwBC8wIuORhL4n9ohZGvmz+ksQ/B1xW2EVOah/e+y3UrChF8H/LCllj+42KOd6oKPlY FXG6WJaCiLMppgWDomNOZFk0/4tk92SOWcU1FdnwqbyOznNjjg1ZIW9dxOdJ2ShkGQUxZ7PMXYEm KCPnxyVzJPUcTf7GVTLkiyNnMxtzFCPoMl5Omo2zdq3RrTU+pJOW8yj//cpK+At5ckdvdy+7f9er HWn1sNH8RwP/EVuTFh6s6nupV+8+LcZWT0o/1diytHpZ+2nVq+wROXbq0dPcluCLiwjx6be6JLt/ /A1/HMAvX/DVP2Lp2HHs4UmOVeDb2wsJl1uRVs9K0PzYuhRbhYbB/7gL0Ge3AwNH4axUr3a7Vdzo s7cIzVx4uID3Ot1282kRd/L2XxL/nXRWHsPRBEZr4aTX/KsZK5d+WrVmoxorN+vNdjWm4JLPDKdL rOXDzOgRNJKfxr4Rww+MAh1FJrcu//55a9ZRMf8X/RoK8nwZUDhtq9Us934A21apW4IFs8r+BnSh v2rlLgxLqf03+fv2+OikWan63lyPLfzzp96A2yulbrdde+t1q51FAC08qrfbpf8zRUygfO6p8met XmlXG+QZNba6DyPj3EUf3b9bVXJ3Ya7Refmr1O6sw3q9hNIbH+Kjf5XqPfYs+r4T8BzSgpLHaEs6 wl//Q0enAasrwsDUm+XvaiXKyLAnl/+z/XqrNSrQUCVC3wAcl9XuCe7E4P7xT09o+sccCSV0JCL1 v1Z6q1ejAH/grP5PWuhrf0Ve6ujR/zCiUffKvU63+fOfpWT/PhyudUqIjUJ7HiyxqHD8t68LaMt/ UVP+N6zSzvs//ot34//RG7ocW72oluohgxppQP/T1G5gN/6O0o2//9PdWFW0TApEJwVknf7n+P50 6rVydd+K0ivn0f9s3xQ1N2iSPqu1j88oxNN58r++S/+oVbqfUXpEH/wv2FcxXv7TOxhuxP/wvesP bf1voq1/6E9U+vMf7FBs1Wg2Q7rz1uyCQHNUfe+etmsftUaUnvW/899CZC+bvXa5ajR7jcp/XHoB Hvs/3YSfardUAWFu3Hbkx2zHTIVqT6Ogi3tYQLC+ryixs3a1U23/VY0Vq//sxuxKrVt6q9Vr3b8d yoobip91VLYgtPVasaNS46NX+qjGzpqtXqu/cDn2XgfgVhvVdqzFqmn+VW23kH64E/5CuV5rxcpN JD3/M9aufsD67jhN8n2j2evWa41qDOnrv6sRH+5Cr2lDZK6v/KPtUqdbba/8VS13m+3YW6leapQZ M+yhPCmNEbtWqVLxtOCn1Pn29LnTanY9T5XqNdr0LCNElVYtSb5K02/KzXrbmV59P6b3us3YBW5n 7V8MIw/H1UqtFGvXOs1671/I5vDEocVvQgtOnzEqujFXJygLoGGAin03muVvGMvYR7vJIBDwKGpG qVuNgcyP9Zh0KDLO3q3v52PH1c6n05ESGtrYRRW1H/1K3kjJwhunvW4L6g9/h2tSPtYqtaCHndpP r15yH1Gdyc/Huu1So9MqwWot/w09q1Xg6X9VRQqCniu1u2/NUrtCbDnkPorcyAU+E1Oh/2wVD3z0 o11lBGPgs22modYymVQm+EmFa8DAR7kGDHzWacACOrsK/Yspapb+ygHv8nr3xWw3W0bzn7d39A3g EpJyyIM37s4PpZMnY7L/K9e16j+GKXuP45McnHzr+2elNqAfQNU5swomECIAC08ZGWCER28+a+XP s3bzvVavHlb/5igLQazw8GXvDRZZoQm08AIh0XkBjbXmV7z981at9BXv22jUYljEHQfg66gN5Wa7 Uq30067Y6kmzK9xW+CXTaLp0O1ZrYLrc7NS6kUmNGgMqFUJe+F0AP2si8m9S8n/Bk/++R08pKb8M ovvCU0WH4GddOJNHCA1ZvSaE3uAJvUCmyNPMVh8rNSqUboVRKvLSEWqC3iYv4R3X+5LiadSpZ7+M Qtbts8uog00ejjba5NmBwy0+5j/e5JnIA04eH3bEyVtDDTltu2fMV40aWyb6pbm/n8tYsFQqyE8i nt4+nXtIbNxszq+XbpcPUnOnK8ZOe/fnc+2jMXVQmFpemDdrpWRnVrvas7WZtZ2r3a3j9Pba0eP8 8U67V84WbPU4F1fS6RlZ7lhf1seyPLuz/pxc2tlYbnV2OofqqhTfWT+aarOHDrrGx9750c5Gunpp 1ja3ylYyOf/RV9VR5Q7qy1qF+Fr2frdrfT0Z6fuVZf2nedTR9y+7n4ktbaZXsNKzN8ZXff5Gilvv 8sGbb2Gz2fx79vr84VEvmsnr4Er559aedja+C087a53kT8JajvcKC7uVdymOB6vw+nLas96fbrJG fad+u/ZufHbNz+y9IgzH65xVVo5+dza2529IOdDkjvn88dyE3+Z+rf3K/pSxkvua1S9XZhqkDbel Sk+K578WEmW7nDlfMD/TL+sbejw1lzBOll8TO+b8VcGs9pa2rg9mPtfL5dI3+q2WsN+PPknNirxa yrZrs69rteeDilGPb8+vtBOPPf3ocu4XtX9xZ/3gMyXFtfXrpx29UZ7/SWwer69mfx43a9nsauc9 pbfL+0rie01xSixbB51rGLbsfDV7k5IrazVztQTzqxxvLqwsV4169uyH9ODuKL5j7m/M3NjL+UwH 5mX/QZvZyprN58TGdeVhTX2becLFbjXi0KEtbWkGTcmDdqOdN9A4bRnfi9oKheZ15UhWnmaOrdXS xlxhKnHfRrVo6MYzLgU/IsXlt+n9NP49sVXYoL9t3NiH5HFz2X4lhal36j5A91ZObG3Zy6q1/bFJ y7nZ3FivfJ0845l0GgzlnRoZWgs8ZBw4DXhyG6AsbF6gh6pp/F1myrBe8FBb1c52WrvXvsp60fpK WO+rh792qTQ/a2hvV+cbe9bLll78LHf1s9nysV5UUzD7evbpbgbeqdzbt6/bPWeICGoFmD5/u4Xl 6sn2Hpuwu6b1fmlX8HhCsaXFxNbM+g2ZIVSyFLdflMS1kb49KOy0259X6bXjm208Q7lMra3B5C0t J4xm/tk7lGLH+XFn40QmFhUlxdcTm72FglVXDPkgswYfu3KNlLOlvTV31ovdab140O31D6VnJrlx ZxN/255C3xWBjv0ma7p3nHoX+WphYa61aH5qF/f2m7yxZFXb7WW5era57jSEDIczGEe7+suBgtG2 sfJaQCv1MGntf2VLZO2TCc1d/v4c6qfPxnHBfD/IycrhW6VgVn7uMPH0mYNdo65t3rhla92jzXtj rziz6WmDFIdWVE+t3e94Fao630AUJiW/r900+1vrfa4Mvy13d1vTb/l8Yj194hmR9f1O892sdWoa opYrD2fq4sz+ntur9era3A+s5ItFBK/jxObB/QGr9OMR6FgK7saPlna7L5Uf/fL1wFqtpadtUsD7 /J6mF4+bHzvXxf1Swc6d30rxteOEyhWB5iDDtpaLQuFlceqTvi2/ynYZxEdMKDeW77Z39ezzVEeX t3pZ9zn7bfVhVj9b3n4gLUSEWYpj0szfX6kfbyzP1G71YnFumdt9FPmtZ883zh6ceflJ2Ne3e2xD WZqz3vVUlaP8zl2YfXrf/rQO2kuv/W8vP2k3ycOs9V5sa9bB4u3lbnz3IC0XHndS6G4T9q6PXkEv P7zDAq/8wiOndwCfbe4u9GVNv5jGXxQW8tqivbL8nraM47kFh0itbsy/3HxmL54qeQDxziZ8FEz4 MNHiKujow2B/ZtEjZgF9nKMbW+i3S6CWwpPo28Kp8+Ql+rhgf+Ii8MPcjUvnxrZQvXnmfBi4FtIA XSiM/IkfN53qTfZiwfApe8upWRer13Et3oZuOYVZ7E9y12TlkIaYTp/PhBHbtIS+FEzPiBV2nHYb rD34O1LsjjNY5+JEuKO447TGecS8ILU4JZKmbIgvbgsl7qC7m+i7TfRI9sS5oQuDwSOC1CLOWyTY DAUaby2BM7jl9j+wyT4YwsNGkOze4ibUnaEdsZwLp0mXAkD4ifAbMRcn4dgh3zkY64NSIJBILS6U juTo68D9k3TI6B9AUrPh6Qv+DU8jmUt3xM7ECsRecQvJ7YHzYZ5xtbj3/cASOL9nzmu7TtdM1iu6 zNwR8yw0Muiogk1nvRRclDh3STl+xIwbaLcWt2XZHaE9wePpvubi5cKpgEMOh2SHkIYDrVV02q07 Ay2SaO4GbpfppTAnhHHa/uz0yC7dPdo/3Wl34xf6ZS8+LW5vTZA7Ogva/Y/9g1i2g9XPZlyTD+xf E21LS1bZNH9AQDC+gYepx2sZThhS8iAMHRraPLCVi+ccC1U86CX4DZp/7gpY24y/RIdYO7TvE+Zu 857bkR1ZDTUqgQUWJLguA4txvyF0SJ7ZuZ5fMKxK/eipYGXuVU8t2fX7owJw45md1Q== K2tvoTGjH95dNoT7pUetfb53ubOxkp22DhIzGUE+BVEYsYM8ywYiLD8sxtuHVV2yQbIQ+sqxRgkQ n4q/+tn+4qP1BiKzTxFEWHf5eyz7ZTqNC4Pwwd1W8jyxfV36leKM886VJsJ5o6IugVM6Nbv2fSX+ ob5ubFuAMczyIP5/43WjYI0vSjj4xF3DyIF5OVX2Hq1SY/cG8eDfBVsBufVSy6uy0pyTs58zVRgT TVtyuDmvHOQWpp8eaUZhw35MOlxWEouCUjxQGIwqCqLCstmrp+qh9X7euVytnT0YdNGgPqfUaZCS wqXbSLLtzVeKIuIq/k0VJoI4I8U3Eq3118LC+2FSlzeK9+ri7PMGY0/5cbI+Vha3CUDOU79N/fDm dhaWT+K3b9j6BSApzotAXFNAVinoVtVZ3Rd0NvB43myuuAsg/109NEGUqCQKry9becs4uvv2NBNq oULVx0Nh8ay+jtevq5NYO1BXFjzFutKPKPus0kp1+cGu5MrP8sF3aU99XV86IxKfvFb5qctALZUD TLj8VhZbDMXszvrh3TRIYJ9TzuyvIRXMqX65W/kCjG221Z2rqTxZHsuzqzD7mws9tQrC4Nw7ueXA HUv0yvKCmReFVK/Evi5vr78a9ca1Kb/nzX2q6OjdwRppGXtGumfIUlxZvuo4q/YpCXTz4VnPrx+u ODeKTIzees1mi0tV+WBveh3GO5lYf1v7zEaoGY1Yf92j10wB6VEIAYVR779nzM/HqbXE1u7Li1B2 8tD4Pk7MJrbO3tPiHDwb3+rmlHvD0dKoVnXq/MSoVw3FrE09zANBNc5hVcqzn7pd/v6dw7OR/2rn C4Xnd3u+oJ/vA4HfPSdSmaxq+hylzrcHBshsb/nV44tHHZWdcvWS/XjRFSnOnmTrVyiMV0eErN69 FWcH9F29WDtK12++Xmi+R1Fg8A3p00gou+Zh3VMe0OSzvfo1v9BWjZr5+WDCOteOX/uL7Z65qzex dfe0RB/Z+gUW4/hjaud3u1xlkzjf03NvrQ8pnlT2rpcREopIq3BnvSenMmRENw/aHXn/YXfD2aLs tYPSW5JQ0C3tRIaqiom9zce0yrEd6t7jvF40Ty+t5EVlZWft5LsGSHZ2LBd5RPs7t3dxi0T4B3u5 edzT86v1JbcwqrJEmoaF00YR7devsEu9XUHZZdXduRmSFaMJk3OZerUOD5vr5uu3kQZ2Qju19suJ c/iuqFBegFa/bH58FlaAPVuYzl2ut+7tt1X5Az7ua2snH+s1+6049ysyMhpZlQ+54tzsZeFlZu6i 8HKmd5Fm/c2/8R/xb5i//DTS1e5BiXbGqGfPExxzQzYjbWNhp72S7+nnS9Y7cH0ra9WWWy1R9yq5 qZPC4t1ZF7gnpeLcOEpsHp5UrMpPftmtGfq3sADbxNwNLOGNtHBj/nP1s/r8yngYbn/FBGdvdwpW ZeV559dQ1oEgpWbslamk5u2a8NzW2rt+eOj3CPCW7KF97Xd1Xe17qH4ZL7x8w1o8ezh4tMvbcxnr cH/qMn8W/yrsdA6OvvBzlML0Y8isTc/CvNBluGZDK4zWsgcdzKSy+rlz/X5loElu8dwhLSyx+5Rd XNUze0/7IoeqUR1sLlczSvZzQb3Sz9dv4hwTTKcxv2AddM4asLq15G589/FFb+xelQobu8qCpzAK uWRlJ//5qNdhCVvH+kVxB3hLnvOmLVsF5vVoSc8+rps7aze/texNKlXVi3qzD3Jq5vvXyCxq93pj b+m3sFEodziobG1qKUcDjx5nDOYR+ngUcLK97w8QbWYa2lD92NGas7fGebyprTSWiw4ztZnSi9fb 3zsbW60ToGNXq4cb9tt8JvCha9gIFjtoN9QdcoSGcm+mYOpPH/Cx8lKwzo7V/gI6yzvNleIe1HK+ tP7pXRZOX709dUwYfDkP1iPwFJkTGOitT3eagCYfFs3PTPcM8ZavrSXjq/tT48u+2UzDHnHRsRNL 2jvPnMPHSuvFeN65nu12heX62sufHdw/cd1FJDgVL/8Akt2hdrT/R5jg6pfdh5r1PrdXz2Xa6zfY eLRefT779MGLhjawfSA4S3MF08gvIUbtBLg6vW2XXh8W+FpktbzTsz5mHu9A/Fgo22VtcUOXtw5+ PIBdr16pZevg+OoWaOneCmD6YVdcPmtYaUwYrPLtyTvW9O52gbe8fbUONlOq/Xz69mS9FxtJt1hk uNnGwiVsBGsH1FQHokI/jXzN66fNcit/knk5gClpXACraReRlSd/8SGuxS/CBsFvnw6LhYqY+dZT 3SVLv2gcW/bb+8tmfy3wSHohfwo7iXxul292cniFCSMmd5bvtfuLpRIwMvO3nj0C90qbLhzPoTkw rYOpd9m3lsx979RTgEuToYitq7StZ7bfjwoLh7t5TnIKWamRYM/sL9RUcF81a5vbOWyf4e1rK1Pf ztQuARPRWbH295H8smrUrerG87R+dn5/AlyRccxveXn9B1iDG+CUqChBTaf3+uVb650YsNTN6pXw Dqes2FYL68vTjlIj77CNZFiy1/WjIlL2z9ovLWRHxqPDbwqEazirIfvEIxKZFeCjDovQ3etNz77A j8ObFgfwvS4W5h7mL0Be2tWs/Z96Cr/Bz77wTq6++3TfnF87eX77hZ39aL5vLeLWgPSm55avfpDN 4tvHGKWxednS7Dlrv564LOi9j7bncYdTZDOtnQP1Oi4sYtYgsX2TeM5evM+X1aV270Rd1L62VHtj r6C+Tq3oauFJv1QLxirQMfU1lTfV0uHyKblPbhXXL1TrRzawlKQWDjPn+E/Vqizp5Dlr72dLTcjX 635tWK/my9fUHAwrFXGwCB0u5jEN2T+9+QD+8OMFP6ltFh43sVUbmTSRio2ZNIUljGpJzdnlaQ0a Wp9qF+zcxR6GBb9esr97F0fYhrv+/jM3BxV8aX67uMO8HVvveTsJLGJyMXTHvZHiduJ0PgHz+7wS 9uQWEgEOZ6HmQjK05pudjXphcQBv+eCSQi83g/Z488TOv+rNz9OlxFbnPu/uZ7hDmwvni3fG+dnO yWptZm3DVZOQtZ8qvNRXNeCjtfpO9vbD1nO7nS5yoFgpLBysXJvK1U/HtWHyNfvJE5StJEIF3tQo d0Eli/PSHpCCprz+3qu2qe+Es0M2l6xVbiMU9mZN+1Xyl7AWLwuFnY+dOvDqxQodFu30GuuUlnpI G3SxtvIxleKYZaTII8Q1Ln8/INFtAbbO1ozWPexuQu+Tq9zDtedlQKXVWcquPScuMYfuDjnjlFKL jRRQ9HRJT8/PN/PKzeEqP2z7pdudzuLttHV48PRLyKOwd6WBkXlvwcbzuKqtvZ3Oed6V4qFvw853 tLdXeOk+rRR25qtF/aLe/OVZPyb0MleQs/ts8aXX0C9nzE3rI6NNbyz3HkEW07Z+l2wva+hsGc5u wfYXurV0qHR3NpfY3MlOW9ghBbXVzF5Wmhu7rdnnFVedK8XXKpVycU0tLZ0Z9RWluHt7f9uAaeoa rg6APAJ0s1d4fep9IheeeUzlKJvnmX25W9JzK8lp2H1WbgAHV8DD6I23j45rW+dLfFZ/sEGYc9wR ir3WLzPPPZjkuzgwb+c98e6NXrzKFvHYAdeHRi//vfN1BD3/uREU32iUX2+UnfWW3NHljZMWLwqj wTLmN4rWUm21vLF8d2jAHrCPZmOuRhauwyxyml7PyLPJcYTwj4ZR1k8P90pYYkcjoQpNOp+2y3fV h/XSTfPLWrW/4gU736zZz7efVyC/FOdW7nwef9WuTh++CO6KR4XHwLILi931JmIRM8bx4mIt29g/ T3jcyfAuRjaF+d14YfbeXrnLfRjpm9QKN/vOktN+D0vbm49WFQSkzoYi1ueUsrS5/rb2vWk91e5m OE6JKwfzBbBj/wQVkTo3P3cqwMPUzRu9qb8v8HN1/jgDfISR08/ul5tu70VZDNCB/C7m7XJvugbS 3VMGS/QuoRRqvs8BJd5DvhErTaY1uc/v3Jwn5jDr7sqfhCavAWf+taw3tqfOCq8LTypaDI+Ye+Sa R8t+eYaddO9CW7u9eoO+VAvA4Ty97sYBaqiodaDi8gfSsS5CBY8rPAcra2szT5H81TL5l4R9nMjD uksswxoqduzy1h4UdjXXms1f2Q+z+Y+bNrRrL4043WfifQUt/L+3WESCosYOmm+x0xbysuzELBwy E83rGLtirroBT6tfzbdk57vWequXGt9iVIr3sXb1r2q7U0Xltb2hNN5nW6WParvU+KiGF1lu1lHY AhdtgYNmkCM09HC/8d6MoS5R59yzdrVSfa81at1mTK8036qxM6uQhHKaZCS8XtH8ILkOujk1neLd gPmnLp1IhBGGEpUGDXq57JYalVK70udrKz4cKR7Db+BYPcV2qdVicQFhDaI+7fuNcr2HXNbPmvVa mfq3L8CgXTVqyOHWby5ZESQQxGw2KjXUuP1KtdGtvdcYEMJqhwXTrTVw9/iKA3o2IIwo+ktCoFKE 94JDosI6R8YFbsN40GFGSBthhLlhEgKD/KpWyKySoC+93G6+lbpHpb9hdXoidv3eM+rVagVFQ0Z9 1sDxk6FjwRVc48jIgIIv3NgQNz7M72k3lgOHcgweG+qfX+RCjaI2qthsDe6q48POrfHAh00Uw2M1 /9Eg6Wq8Szwd+vJxs9F0393/AbqqvzX/ovhQ1Uz0ivtezmiDXi4Cnl2aGPb4brv0NxcVc1hrVAa/ hOvwfSu8bagysWlhQ3hR/Tgutb/Z2kiiMQsGQbFd+0GP33CRS1rYC6fv7wic7eaP3u7+o9n+5mE9 ROfPeyU3/ik9ABLlT6iv6jt0YXW6r7qwcN8cCGKj1A6nic7s+BU/6J2gsQgfRPIODH0Eign4RzxF FNoKa7vaRjgoOoHD4YtN7LV3raVC6ZvftHjJxHAY5Gl2WM0M7Z1RKnE3keHec0lsKOkTRzQorihs a9yvA8PTbZe6zTaKvYa91yy1SAR2reqEfF24wV6xHxT+9TRwx+Y3GbhPgsCibdsF4Pduqm8okDIi jfBbS+qANeEJpwxdeoj9ASa8+Nn7eWuUavVOX7iXl3GNGhYcyDZddap4iQERd4KVF857wAJVYzUU /dgtofmoxTqwMmK/vWq9Xo1VarEKycEK3wCL3Yx14IFS/a9SrNeIIdYrxk044uWqToA0LzDErjoo VPrXpzpabqvawI9AjT/NCvC5ZeAhYo1e868SKgxeqpP6ag2+ymX2fgXWPTwFg1Gv/etfpXa9iZ7s NeaBYQfGsESwBqXXS3+j+G7c1H1k36x9NGJNVkx9npbQhNoaJTQEVVrx//v/xDplGJtOudbrNpOR Wcxyu9Zyp8hJaovawnVkwAJAUlu1LHKq2QDaGDHWP4zHphtlA0mf40gvfVz6UGXQZMV/D/2iR3Ry ZF24R+iG6QxnJNHTd1WWW612kk9KI6Sdjj6JvoQclV1DG1rybYAogJ6ka5Q+lAsvsA== zsshvioCzGa/0RQp4f3nUtj4lYYeIQkxBxX02cRbFk0OkE+H96EthLgHtY0v0n9GOt06bV+rFcKR oUF5q3V/SojxEShuNq8GtfRTjMPvKxS3EWdmCHmm3k4COaRYyYcPSpft8cF9hV6i4upuOhVNCewA N7d+wEPlUeA5gzfoOcLjhbWy9fHzTacE0YwQaKEHqw2UcjNk5srtSrJTbtVDhVH0ULP9kQwDMqqt gzKP96VA6K8Oto2wskh1KPC7FL51o9kvYyLcRYlVQuQAUm+50QnDGzzzXq+1Ppvtf0UYDJpN12+P QY/gdAkDW+8kWXirl8ohuk5UYpdjTAYO718DkU6g1uAZRB+Yo2l9QzoIoBUCqQ1BJ1CDbq1bj15u PVzzIz7srOJAGLbJtjgAPGQhtQeWhiuO3Lp2OE0jlSLm4c2RW4Okf/wsEAakOhy0jaBHkX67FipK vje6yUq91X5vNsIWC3nMJav+GzCq8geJaV5kBNXc6b11wmQzscT2AFWH+LQDigjPhu/vrU6yUf0A 7PwVxkX+s5UUeMWAkupKGQs+oQ8BBeviDPZ0Pnza30kijrVR7YTLQfBc57NUqbarIVsmrtGTKibg OWAOxHoDnnvD2TJo0wJGovpXtR5GOdqVdsfLCvkNe7NV6YUMFS6GsfdhzcZFdUKmDz9QDllNpKpe oxy8kqAMtj9h1f+Awgbu1m554fYH9BzsZGEaDXjkw8us+WuE4Mm298ngnREaVy+1Qrf3nyRLDtfs fjJuZ2CJoTsaHr5So9EM29/dess/f38HKy3gQbQ31hohIwzPuARyQIoez2A2QnUyjnzRdNm7BQub GKmIXopV2kCd2gE1IM7UrSLYHDq4GRUs+3Pb6ILdg/2yGtu/PI2ZzVIXpL/C6e6FrmaDBDo05M1w PtXpcKlDRAhHGuUsrzp7nrO9OimILq93b0seCwl8h2wzRT5jrPN0sfrTcm283Cs43RZRIYnwQPca IFU7DJjs3rCKllOQUwVK0HcI7BW5k3afxqfzFPuzi8Kts493304cNcsiL+PWghRISFbutEpetQV9 mdipunzDuXH4Z/e0cVZirEXKffWsXS3XhFxiXpnfPaVJPOgHzRo5AAgVz+5JWLfHf+ObXCmxlbG+ 09unr1tyZe5yB/+5ld+b/XJvpIxcKqfdaz/3ONkI9rDgXtt7qZrt9lapsVu//nozXo/PdX25oTyt byvXOWum0rOluGXuPT4pS3q2kZkyF8/TndQsijF6W5VXE8dqeuN8bT21Xewa1nt+93tv5mKzZL3L d1vOXTWxeaF9Ts23ir9Ty59fe1OJcjI+tfxSeZhaVguXUwt7HegL+uIumTrKTyW2NltxWstnN7XV WtxoQZMPfmiTSxcG/e31YBv3JbnayfzCb5etvkegf51coVFUF/J3s1IcxknB3Th1W9Z+7HzkoOZc L7G1OzWX7qidY1Zsbjd1sz7zDn/u1uHdO4t1/KjTbq937ttP62en8mr6cgG3FVcKtZBqd5XH9O3n 0bJvpU8fxl5gpZr6tToVVOlb+3k1cQO1iNXSSk+MxZmrVv3Ir9LOzLNmBFW6t3WebVx7KkW14GrT i7eJ9fLpsV+l7d7r2sJSfGPq1a9SuSBvbwRUqs3M5tbf8xjJPn1N3z3KhaJx7tvT6UJrLX5aO77w rXR3pnnkqZSuF1zt/MFR+jhogK/aj2X1AFW62De8u9M3qXimOw+vpZt9c7o2C7NPqz2bn/fMarqY 3a3jSmE1vdlipU/tp+u3i4BK114yV5WPVbdSKc5V+7xSPAmsNLv6fjnvX+nG1GK7s5bo+Fd6ln2G Wih++/ramd+6UwIqzXwuzhvVXf9K04tPiY31H66nOEmcO6ulzMyv1jv2q1QuHJ4bAZVqM/FMLrMd UOndC3IlbBaLvn2d3p3dnDuqfl75Vrp7nrsOGt69uZWp5Cep1H78LgDG+AGeXuzM7yzjAV7qq3Tv 5Sfzu9SSodJsy1vp0f7JE630bmXB01Mpns0kk/dutUJf7w356Oc861/p/nQvd3RXzvlWelqv7rmV wryI1R4mf1vLAZU+JOTLo9+ef6WHqeejQmF7yq9SmJfifm0/sK+Xx1rxNahSS76Wn/P+lR6tzBQr L0vruFIp7u3r9fNmJ7DS64XqSyuo0mP5ZmXb8KtUikO1duJmJ9c2fQf4frn4HFjp13TxyAio9FGT n0qvCVwpwpinrydXte+1+Mmyb6XPz7cvgZU2q9sLH36VSnFU7bH8ah9Z/gNcuFKmb7vn+36Vttsn KzO00rfUomfRJPJLdgpXKsWV0nR3V6RKa+1eypRRpct9lZ5uLvw+27c7UOlm21NpYq15t0wr/c4v uZUCTUbVxt8fpslWrhp3yr5IIC5k++RhF1W60r+nnqzMNlLrZ1Cp2fUOr21/JXGlaF4W7GUPKZyv JChVSs2umYciKawmNnMnD6jS1f5Ktfj05t3eHlR6MOVWCrVgdko1Tj9JX7fXzpOeAf5qbtrfpNLt q6MjcXhhYr8+m3hPBRbqzOTvqt2fKXWt8Ua5i/77vbmpVOWn5X83vQiLZitTDbrbBjAc19y7IrVM 32/KRzspFd/vJ+H32/LRxXY66K4uH1UONb+7GMn3pnw8dZMLetuWT8v7F0F3X+RL87sbcPdhWb68 npmiI+ZzPykXd44Xgu5m5Kup36T/3Y0pud2bytK7/ftL+mFPvr6a2iT3vQsp/XAgX3+sbAfdPZJv ZvO63108Yg8n8o1mm0Fvn8n3M9mnoLtf8tPt/VLA3ces/PT1vsxGrP9+Xn6+Wk8F3d2RX7SXfMDd l7aSXDrMuHc9I1ZaUPKv6nHA228zijH3XAi6e6kcnk8fBI5YuamcfKu1gLcri8rd1+Gc/93Mc6O4 vtG997+rds6m5hcPTuiIqfNbC3vifX0qubezRe56aZvarU9tpA++uLvmytIFL2EtVIoJo9k5YRSG SGDp5QtEdwxAXtXwEy6p+LitxHdWu/NmobidvbPuC3dF697eSMJ3csE0kmXTNFYPE7x89hqvk76k CCF16k5szaTmsbCH6RiSZB5c2rZ6nGvMy6ubtz20Nh6A/L1vOJLozGpt820R1tC03cmdr515eMv2 tDq/ebZCNgokyXB0nK8084kkmaZ/pem7W79KYRfD1U4XKiId5yvFkkxApcC8giRTCqr0FVfqIlno 6/TuSp6rtDI3N+NWivl7p9KUZ3gRd7/OKt2t40rRvJABnl7g+5q+nHUrBYlPSQZWivn7gEq1GcTd P7q8Ja3W6etDYKUwvD9qYKWYu/dUiiU+Wi3i7ytBlVaDK82dnN8GV4p4Bo639A4w4hpegio974PS zNoyrR7/RnG+5jP7/k+uRyox/XgU+JwUF55cPCNPEnqhHmmi3kdYuIUeyNLzD6argsE8GiEubGRT jMLAizvK8pWy7Hw88OI4jDca1Ba/ml4TDVTEhdOGcxDI5pqocaar/3KrR6noMnHygSb0hmdzaQWn TjcsJN0V8CMe7dJWfvfqDP6ci9OP0i3HEZPZP3eXLjz+YCn2U2eXI3Bck7fsOP1YPm6S0SF8OSPH bg8AgSYZQG723ZE3Vj+rVhx9ACS35X23UX5Nch4JaNKyjTWK8B8bUcVP4YcHvRc46O6Q4w/aPyw3 s/45NJn08GyKh4h///DHxYD5Az742J0/pIXzm8GX3hJrPJFa/PrXdCZ5wPxJ8YEzuJ4YcrD6i0J9 oYUtRygsEth371qecWc6paGRJb8r03eDVg4bd6yDDRn5QTCNtnLwjrwujzFYIulJNvtIz91yC9YL X4HT/yFnw0bd2OUK4JT5MIASozuvi3MYl/5jd7fcDW8Npuz4g44dVoz6rkr78XrKj3AHr0oiuvh0 bVs5PBJWpdi5aF1bmYkw0HL1LDmHrQVUQ9I3yrtnP4N7NUd65Q/2Z1uudr5vqO7Cb7YizlVL3CFS /jjfPZwjGLvwJS72o7U61MAEDMtLm9Gx/oHZkavdq1WKHQfJRELxLeytE1gUzP7AwsR1p7T61t2b +hveZynquisgNdF+4LpDNGseeL3jZYcmLbiw4LWj8HipoD71jMNBE4o+aOOJorkfG9A/lwYSmiwU tmws0EbxLUsEALZUkN/N5ENAJ0/PMfuFUzfyXFjYlKR/+6bkO9cOp7R4xilv6Ud83MehV7edIIYg Asd4yu1iu1666t2YwuZXGMXKrlKand8TOFgf/ikS9/Sdnxo0WJtvC7hJvI3Pr1GqcZ8/8N8rndYM bhLi+r7zM4GNcveNaPP3Pes3f4Tnx2R2JnwGufkTWboI88dRfu9gydXSy9VkwIDo2Ff1ejKFuRuK pyigycMX9j7/ezuxERvA1w03Yu9rC/cTGjEPRRt2xKhei+lhulu/HsFVNW6fA4kQ8JZDcMcfe9FF QWGD5ujYHrKh7EVksH05BejQ9/SEVuUesnIcDiMoLwZi7GMvNbt+eDT86PCtYTTC4WBHGJ2XZFRR IaQvg4hCpIbIrpQ0UGoJbMgAKsAaEsJboqaokcWn4IbAoqfeHRFkQ6hq7dd/1+xudUJ2O0CJ1C8b YsOj6EbE3lnBTXLV2dCar32gAiU7HIuSUIGg1uDG7qnTiKDUcInHomcAqacKbpSH8RgkDwc3qTsz QN6PSgD2hyIAhIfx9tDtX3QCENY/Ka4+dRPz4ww6h4MgTsHZX7yoDeDltxesrrJkfBWG6p8UD5jB z+gLMkwmB4ln9/FXlPjGGKzQfd3RXUQbrPAlHggGSpPFJZ7sX+I/B+ISDxDSArRLvIYkNbs2PT2m LuHnQNR1pVyPu2GFim3lcC6KIsDRMiIKE7CUoGvK/NhdS21fF485G1+wgiNwC0rN5n/lCL2SwhUc B1DLdXPsDiEOVtxVh9f7bCsHPZGee4dFijowmeE0j6K+hqP8B6EaGz9SIGgaRFZ6e+2s67LSVGud 2r6KR1AmDmalW4d+OjNxvQweu+0rJVDXFbwN+moUD70boT8ieE44sGt4GxxzvWxfbS5GwLkUD0N6 69C7/Y2A87XzKaSB53e+0ToUT0fS9YUSgEO8341DAPC+DwPj2ep8B2YwL4tGR9zoApEciZedh5pn 5jlTLPxmwncLvspuTuKLuO5uL8PtE36IcHgYd853z3789rtRVGioKKRtHbxeIuhboTCRBx2JJqNF o4yjiyfzguZtcWFcnT4uZcmvFEqThyknMXxrBG9bVs7yRHq14pYiypXRdj6xsGQk7iKapWJxsZ/f vC1GtTCA/DJI7wWFjbvduHIlatnui69zRrCNL3AoK3PqaqR54c3cYRQNRJfMooeiwXdrHBvn9R8b iqJdRaVoUjzEigds5WQoGhoxVNiELEhQlB9FG5JTQnrp1ATWPjLE+9Kioda+x/chDGMDy1mJ2hok I4eUE4V8DG7NqseLQDC9fDYTmwxF/ryAOGGJQAkaa6440+GqL5dCHb3oUkESz5LXqQ== C74LJuXD+VyZcGM/AhHycmYe2+vt9TiGXG9RDpENt+9H4OpRYYL3g19R0kBngd27Fpamx2GlifS6 LLo1jVAOLiWQsZfiw5UT0QXCUwqvhyHlhIrUUVvjcTzysfCGseTewiIqsqRIu2Fpejfh3Q1L00cR ZJoZzo4cuBveDM/f94MUaRQnxt8//gbuhSLGouyGj7/h/L0fQMQYKzyrynQUncsAel+aPp7ILgbl RN59wnYxKGdsDyJcihzJF25wOUr4XijFI++GMGErYQ4SIXshk169u+HawnLfbri2EGUi/PbCPk4J mvLQjeSH5bphBPZ+bUHxWBL5lgmMhbsgg6xvICqEr21pmNUNhY3G6fppFV7akVZ3BLysLWghIzaU cAkzeT2Ay5QG209vb0NV11E88zhNL25UsIoiAqvp2ZZU4/ZuBW9LQi1e34mRhbQ7z7bk8R2lvmuh nmunQrs+A9XQ3FCKXmqBYDGTA2Sj6N62qLBIW0uQQ71YlDYpDYlq3E3PRZhJbt8P8kJEcxnJ+ZbA IlBGfut4vaWCEBGtSX1qYQ7JEZZXi18W8Ju8WnjxhTOdAZJs5fRTiu/Wr0svOL9KIT+781wobp3Z w8fQhUfQ0ajnsWPoWOP9I+jYiI0bQxceQUejnseOofOvlEXQuRLfeDF04RF0NFpw7Bi68Ag6R34Z M4YuERpB50QLjhlDFzi8OIIuOFpwuBi68OdQDO8kYujCI+h8I7lGiKEL94fm7JVjxdB5HJI9+7VH D/NgRXJ24fi64DigX88uFr1R3iYN8J8CXof59Q2ImBJVTKP5r2JL4oMl8r+DxylItj2b9/Ul5608 UcdJVDEFj5M7Sr42catvo+9zLZoRRK6wIDzkrCPo+UfHUzMcmpJvDwP61xc5F7V/ok4JNSoiOAc2 iTflED3/qIM+SHMVul6iBM0NYpsFuAZ62z7bI2oP3fqg6addMbo2mjOIzxzYEWxzUkS18bM9vL7K 7RqLFbUfH2fG8XmgwW4BriCiJ+TgYLfRXEE8MYnW2AYXewDPH31gQlxB/MWQECRHiNAKkWkEFyy0 Kt/UpoeJGKwsjkYe3tTeIA1J1DghaNLLAAdKKWqQaSHUlj1Acdan6UVaSDWamiRAcSaqDRGnu0y8 O1z9/S4aGHtsdoKEpjmLiyGZV3NGjwNDcXzdQZzgEHF8z73wbXKYOL5BvNcSr7QKblLVGwTDy/sR ggu5JoX4wJ9eeAI0B8XxDfKBHyaOL9iYPNT67Ndg+vKW0QsbEGDjKQpre4ILW51MJ5HmavN6QJaA YTrpa70YdcQGePsPN2KpcTrpKn4Jb0mSQAquTh97g6JnIvGye1jnGxox5C72oIiwAdFyrIBgu9je YIKDV7cykDp/7KHIlMkIe93t4LXNWRMWAlWoqIi5qKJ+EIXpbnXkAclRnJkOGpPwQDkpHmmSPZaR EIknwCMChbgFmxujjedWJ1yEkyINx8DQ2KVwAxalMKhD6agdGuTKv2C1PVwkQnLnozuE7BsY6WMH h8D3ITk8/Gg4CdphIL221/3JqHdwk9z17sOPDbPiYZyiLFfO/hISZjeUeseH0SYc7P6w6p0hI+M8 0U+DGsWaNJRGJt10cnb5NGo0PPU3yZtTZfRxGhz3GtIoUSOz2fVqZFBIUgSNDIexIBXiz8HYGhkp nppdW5gNl1QjcugHgRoZj+4igkbm52ACHkTQtfTC2GFoHo3MSPmUcBjaMBoZ/4hUFIY2goOwd5oc jUywFi7SwEQMznH9lILCc2B0/MJzhgrO4TS9q/3McutwUBxqJGb5EK39cT33ttfOB3huSZGlltT2 VTZC6KivSse7Ix9G8J0d3LVFT9cEv4toCD0cPm+YX05IFFcX7LoQPa7OJbI+uaGix9VF9mUMlpIO I4aQBISOCmOD/JODlaADg+s8jnpAI+e9dlT4bnGQj2I0W9mgeLiI+frGjIcTbUksIm7S8XAjY2yo eLgQD9UJxsNNwEM1QjxclIjU8ePhuGjBKNEsI8bD+VPLScfDuVFpw4VqDBcPF56HZFLxcGxexIi4 4PkdLR6O1uKJiBvZrLN73ZxAZD3a3gb02Y+P8OctUWGRyEcEn0goSp2Ed/qSfhnJYhXKqKLQwzHT kGL5BZczdloLXIooPAfFiQ8uZ5QYe28k11V0U95AiuZmAPbPpjW8xzMw4ltL3kAc+9FsDfIiiLYM 76IkhZUixDINyJoSgZ1nNgsoLNgWOqRr8nyftDgCN24O79jgx42jkMHxswHjUugiHEd6xeX4L8Oh cnfgcsZLdYFLwRibDGuPm/Tmm0wxOBNFiF4a9a/Pqwh9FyRQ83nhokSklqYvIoQvDKJjN5OLSL2Z ZETqzWQiUh9/JxKRqkyvTCAiFUqZSEQqKmcSEamonPEjUlH0migeihLf4FhuzwIJSprqcTIKiubw LsOXdv8yfGlH1WsN8LadUCics4uxYLh/SyjcGLmghwiFkwYL7hMIheNGTJuMXOkXChcmVwYQrhFC 4ZyzBf0bNaFQOCIlccFwXC0DQ+EiMoZvHehLMGkaLiM8CnwSHSREy8jwcXXf4WKR12KDuXFfmw0q 7Dfc/BPVJ9DE9v1ueKq3qDzMnTcReYAtKUIeWxzCFiG/mpAm2Cca3UyuhphwhtscoEko07fgdTOS D6rLsBs4s4dPfY7HHT7EcuFiNovOBL+cAn7paGolt/IytXS4rkwtWxc3U8vXz5foWPDi1FJR19Bv Z+g5c2rl8CUjr95+Z+lmtNn85hu8N/tFdUpisNtCSLDb2arMj7EQ7NaZn23yB5yKEXaZz4W32Yvv gLizxcewYLfnlcBK5YKhnQqekGI0lnC0mDfY7Tks2G0q41cpi7Db3W3fOX31RmOFxJ1tTD2FBICd 71wInJIn2G29fnUSUGnmc2nnZqkVFAB2FxJhBwP8wc+qN9jtaS84wi7+vXr9FlRpKTTCbldeC6y0 3fk4mAmsdOpxRrsKPI9vajnsaLz9Oc+souW6gqvHv7FIvF7F7zlMk8UnT2eqUUqcPt2civBcu/fy HRfy9KJe97GiTFkMby8veLbTMJGKd7kNyqXm4WDP5n68bvJefWqYz8aAc6zEMJdRDv2iUhI0auc3 aqNCmxR8Rouvz9VYJ8n5cb8+2RvGPElOmDp6jlzwSXkRx2l+ejifq5Bz0QaeOCL49YWe+zb+IXJO Ud4j5Pq48ag4GHzYCNc/4qsQ2KiB7uHRmhTlnJEBg+40KdgvfMj1Ir8fpR8jNYnXebZErm8S0XSB 1oSJRtP5cdtEbznJaDo/hZefpne8aDq/WLrgzJOjRtP5OYAE+CiOEU0ndIjG0g3KdTN8NN3wWutR oulCkDzBaDq/WDqsVZhoNJ3fDDgUZmLRdH5ys28E91jRdP3tOg6zvY4YTecXSzcgZmSEaLo+5oY7 IXdy0XR+s9sn748dTccPFuOig85LGj2azkUJr0+edDSd3/y5PiSTiqbzi6Xz4ZTGjKbzK8rJCTmx aLoQC+8Eo+n8YunGGrGBwTnDjNhw0XQDRmxC0XR+sXR4F5toNJ3fuuJOyptQNF2wl9oko+n8Yul8 /JTGjKbzi/3ykV7HjKbzi6XzsyWNF03nN0NeX+vxo+n8YulC5MrA4YgcfhNkeZ9ANJ1fLF2UHERB 5gPUpGgCoNcPVvSoXOv0Be8sWANZjP4wOj8p6Wt/0DGVUQOfGLWIyF0McV6dH8MThbsY7rw6P/cJ /jy+gdxFtHHyOdnW6wcbcZw+B27fAgQC85Cgc+8CTqIduklenj8SLn2bFHoErRgpPKhRQ4XGLgZS GNSoYFeK4cbJm6sz2g4iSkSp2fz3qigRHXizi/TbK6OpwUY85s4zYv4H3Y3AkovH3EU8Z2TMY+7C dRf0oLtxlDH4mLvx/ZOjHHMXwT8ZBdKNecwdojADD7qLODDBYUoBJ00EuCmMesyds1eGHXTnBFWF H3MXMS8c7Awz44LhcIJxFq3DoSKLsEYxKJp57XzsWJ5DvHUG+ipED6SL4Hk4yNcazfnYEbA4ijOa 92/YwkXH04l75ShRafhsugHEOsyJTDgjFUYnO4YTmSeeaMEVlLlzrC4Dt7qhNjqkmvcGdo3inQ4N TQ/QDUf3hsI8/6S8oXD0/7ie4ZeBjlBDRj6Oc8akI+/jcsaNqcWl+Gx5IrWMWk7YCox+TuKEDpok RQGvOphaRg2s/aou9AXWflUH2pKiav1QYQOCV6QhAr++qkuRiBmnLvKcAMIP5mUjUowyL0iGelBf N4klUfShvm5OJDeA6fqNjBWZcjXJEwyvJnmC4dX4CQLwSXk+/PsIkY+J4QMZ+r06UTljG3dJKeNn CSDlRGTiifwSXE6Iy9AQvqr0nMSooQzRAhnuWp5FSPmxCS3DoBPuhouvHPWEu4DYt0mdW01PuBuT G494wl2UyMfxT7ijJ+WNvwxDT7gb9qS80WTpvpPyBnnuDYwP4YpyUuSEZjmLGFg7+IQ7X6+biPET j79R3ZvC6BgwdwNC0qOyOST2LZLkGCWwVpn2PSV9yLOeYbyrEWIvIkQ+TiCw9sajzRrR5wqXE120 DtSN43LGD6y9GZAXbsj49r5E1aKP4ggnevU716BQqavgSGApPsQyfGmPGMTku4vdBoYxjRDENL/5 NiWFBwtF9bBBhX0GR6hzWc2jCO5Q2HeEVRmotxRHLFR6H0KufGljAT6yXBlAUNcWkhGCmHCW5ghh TNCoEPl7EGOI6JiXNXzrs7ji78ZgDD0n5VV6QVM77HGPZpIjBX2WkWFjXN86ESw2zBduUIwrFDZG aipPnqu3zqSOezST2ZARGy7G1bh9iXI2hRQe43o3fIxrSA4i1KiI5xiEbQ7u6t3KHDfc+gQfEjoH R512W+3O0iC9g/wdiu0roo+dqUQ5eYDC+kwc25dMHeVnnamb8zSO/vbSnsZr34mdUuJtfn2K57BN q2v5gGPupucC4+HavdfkiiiJiwfdbagfwSfOhZytl76791RKZt8J2PpSAiuVC+8nl4GVzikHL+Wg SitSPOwcNuOSq1QMTevMfFaD4uFye1u/sz9OT5HWWoxy9A3DYwO8GXbiXF4OivzTYMRmL37U56Aw vJCAw+lCJx1c6e7y67VbKVr7QrXz1YXsZ1CUYzKs0qP5wEqleLtztTUV2NepnaeVojCr1TyrHv9G J2LRvK/8BD6H6Rh78rn30xhYojbz+2zfng58LvNJcQebJFn7KEjmUfewnUwjs/DTt3UWeu1gcuRs dO5u13eigcitcvYgRkEfrOHdJoNOMTubLwSomHw1JCEH0CGeN/QUs6infA10xJTiEUxdMGsF33w8 UTldjoexxnatcsfJ41gV5gkZPk4+rlWBBsEBUWlLgeLVsFFpA3w0h8BToJfWkL49qH8DfdL7++cn WaAQtzA/rWGaxEXXjjfoEby0oq6X9ZBT6Ulr+n1jvRlCfNJhPtsT0THb/WemjKCDvQ== W+5GUG5FOvsJGnod7gozQPzlKIw9ASvP3YqvQ81w+jF7RL2WRz+GogDHNlmjGEBRk+JDLSNFAUZK UuoMi+9eaU8uZaNNc92MUZjoNKJ4bTso6u53sLU6CoUpFSYnI7+lpsdRBgvnJJamjyeW7AoGy6vn HzbzkEen8p1reRwIPApG/9NLo0e3BfNtUXJEiKGAg1ziB3mtOnbk79yAPAdDhEW9TXmYN+/5laI2 Nji6LTjPQSRuXGjUZ6Cz/nDH6mHfUYHejxHKGXjmjZsTMnIoZ9sTqBMBDEER3KiwQWJK9HahDCHh h24PUVgg4kcZsYGxPMOM2Ig5R/xHbOAZx9ELCzzcTYgUZoX58od+UYBRYwCl+GD+MDgKMGoMID5V duQowKgxgETPP2oU4Gj65GGjAKPGAPqfwB5QxMgn6rneUKNEAfbNUEAMIOepMkIUYNTxdPbKkaIA o8YABsnI0aIAB4ujwda38CjA0yv/XoUdysfikf+9h/L5Izk8amv4Q9R8JYuJH8oXqoWLGDg8+FA+ KfI4jXMoH8f1/RsP5RuohZvIoXyhMSMTO5TPJ6v5v+FQvoB84yHjVA1cvTukNYNzQ418rl+E3FAT ONcv/FS/4XJDBZ/rN3xuqFHO9evvGn+q34h+Sn3n+oVrhYLOrxz2XL+gqLvU+LmhDqI6TA2Kr5xM LASNSRz7XD/nDd9T/dCITeJcv0mcLzb4XL9wnYMnkmvkc/28XRPF+9HOr+w/128EveUI5/r145M/ 1S/8dIbo5/qN7KE61Ll+4af6DfCFi3yuX3jEjIPkMc/1GxQxNJlz/aJHpU0o3N3nVL8gPX+EhDfC uX6je0EPc65f+Kl+EzqPb2nQ7E/mXL/I5/GNda6fU4rvqX59FqsRz/ULd3Pznpsw6rl+4ZJasK/1 cOf6hYanFCOfZDTgXD8ylEGn+vVbEkc7148F7vmf6uerhwl1uPY/12+EqLQRzvULCgVTovGWEc/1 m8Daj3CuXzgb4J7HN37cQ/CpfsOfxzdKSgG/8/jGj3vwnurntfGNeq6fr+3KsXBKUXn5Aef6BXWc LEJ3FxvvXL8oUWnjn+vnxG/5rpxQOjbEuX4jcOMjnOvnAzTuVL+xz+OLdLhmhPP4xs7sQc/jm8C5 fuH6Ly4yZaxz/UbKqDP0uX7hArU3w+Go5/qFn+rnI72OdK5fOJsjTehcv4FZmyZyrl/4qX7Dnsc3 mjar/zy+cemv36l+o/hc+ZzrNyAYHmNsAuf6hXvnOGcMjXmuX6hey8QW3gmc6+eEj/lKonQXG/tc v3CxHc/LBM71CxfbqfwyqZingFP9RpEr/c71C5YrgzTwo5zrF36qX9Rs89ECYoNO9RsUKxr1XL/w gFjiETH+uX7hAbH+/Njw5/oFB8SiU/2iaBQjBcSGnuo3HA8TfK5fuHHBLzvQKOf69a1P4VS/Af6W kc/1C0cEyaAbMVdKtc/OiL8L3hzovsDOSwpW7b5fra16VbvwXYgnq4/jvXC+mCdwUcBTy6PDgsk5 M91lLyi8Eg1+CO5WFrhIYeRt2zDpiLLC6IsvVbPd3rqo7ax2Nw50JXdzqc5vTVv4ERRPtZconpXa U/HHxNwUUgRNzT/vfU4lN7/0xPrWL4rk2li/uEsUa99N2ba/VmX7a2VNLhyeW3KhWTuSd8/z/19x 36Ed1bFt+wX9D02QkZDUqhzICoAwIoNAYIKQRDhGElbA94w33v32t+ZctbtbARtf+75zzjDqvXrv 2hVWmCtU9axZuXP/rVnZ/vDRPPiy9ck8ueereXrn86p5tv/pg1k1B1/M6purh+b57OqkeTnzbMq8 fn/2gXnz5sUH8/axPxDN/86/uGDe3Zt8tLe3d3Nub/+X3ct7h3Znde/wbZncv5B/AiB4fICdnRdm P28uP1i+Xz/cWH398uOZiz9NvHh4vlz+sjDx8Mntn3/69OvE2bN17v7kuS8bE3dC/enB+3+9WLpy sf0e397huzOHM1/D/XdcEt32Nn/r6dMJc35rU2gPd0/VIW1duLt0fx/bSVfOzC6shbGfgNQddtOX v1xa+M5kXQoyHYe/mXdX3kzt7d2fnf6DkYapF9NX/IUb5tbC3QVz68PGz+b2g/vb++fepPfcX1nP tN8tvPbbzemr5f4rM3fr7RnsSXxkbq7mt/yZPzP3YGvmmDQdFZ8jP9n3bnJ7FG1FhGQ4uJH9GZ+J g5/OTH3+cu3MjLu1cmZ6Y/bRmemXk0tnzi+VB9iA+3P7Sc15//DM7NyF+/jiJebuBX5X8+aZmXsv lnsTZy7+dk5Gvf5bafytP6XppqFBz9uLi/by4uf1OYvxvb1xeeXMHpfp1k13r8inJ1/tzMeP1+TT 6m908M3cr2cHfNafr9sHxkwO5lQnn4d6nGyf/rV1UZ5ZntL3fbjw2zQup9vlpclZXM62y5Uwp4+J Pji4ufHrb9XMxTtmfnt3ZX/+7vPnr0UfHIi8tI5ey1Ojr8ZHcO3q9NgX6+cXr3ZfLM6OvnALL59d 7764Mzf6QjDYx/nRWx7Y4VdvZPEmvpi521cvjmjjb769ODP2xdibb98ZyGzPXBSj9su0tHJuyr3e /9chVv/2Yyuf3xt//dmZQzP38Mao7beEi0K7NYNnLgre3lqgXoFSLKJVV4oopgeysA8fzCHGPEMT LJfPLJ/g4uAtD1/5uXtPfvXy9FOZ/nNXzuH7aXnBYNvMra2MJuZd95Z3svDp5ezS3PqVny593Lnw 7NaVm+FfY8pTVevtF4+G3uuRgH1nh1dqpzz/Yountce45UrtNCxbnPq5PKrl9q0bF7aeLt3ZbD9T KaN6YTv+feqml2YmDm9NLt+546a+vD3X2GttPQwHvk724oJhxt4/mMUUzci05h25fDbXGP/9C2Pf r9yZkk+vrOLfufdv3LV6x8lavV/3/HTUAi6OmzzhofHanjGp/KsK4JehArgO2ZdPX8LKMQUgsg8V 0BTA8d/SnfsJg5xsnoUo6zagB1sXKYtieJ9c5DxgX+8XnGWO3/mMhg34689vrt96O/UKFREi9gOI 86T+5ObW/q8odl6+2KTbnp1zS7OvpkWmr8506wLZF+ggtMUBoMoL9arln6mGmOOtuRGs6h3JB4lu 6JgF8Y4u6CEyGyY5KrM5OXmpk+NLHMakvfnm0pWOdoNyBXTx+De5vDUgM/QmVEGIVP668OXszi07 83XRLP18Zdec1AfihOsiilKwggygI+9Pc/3G0RrRCrDscInh8V39OoIYFJV4PMR2XDY67KHA4uqT m5tmc2JnaW/v6sbR34UFKLly93nbLcjb3eLT92EExUjDD/3e65p4N3G8icP3D38TM1gO9Wecf3rp fh6z16Sdubq2zWooQof9Z0cOQwD/hrWH403E3Vsj8wczqXvjqRSFGe47BOnBFsvT+DQY0uYY51eq sN/ME1myg8k9lWSxPy/HzG035k8XJ66ceTd2PkHDI/pbzr+IYgq35Qs7GA2cPMZd7YoQ3ozBgCtL +086BMCjEOq5U37XuPvNWZnU/bO3NuemR7/lPDzloFV0a0Ji+DPQwxMPXh09UuLi2IBe7fn33YAu HTla4l347Viv2289nzjb4uDab7rmh+9u+OnRDzDLZO2O/QSbCMBg1MDwDAgcx1DPtoFvPLjXo744 e+J4iOl7g+/0my9tCzE1ef3V69MWQgZ043A0oCN47IcXAqdIdk38Mt7A5e3HwwZejBogtjzWBA+O /BvMoBHv8T4M1+U7vTjZxNu9vzQMNnAkNt4OOfo7w/h0eFoD9F+2j/7i8i/fmbFjbPjw7XGGHY3q yH1b+0MNo8ettDsF+F95eOTO3TPfbfEHBgmpfLh3dviq7bFXLd9b2zjiIP4y5Ky1X8a71H5t/Guz hQfXp6HZ00g7NZ1stg5uFArfyKCMUIqgx+1faFrFOq1FNcYLL97SYs02Mxl/2cflXGe+ly8MP6Hu AhYbdvjiUTtMszzTLhcHNNqD4YNz0zdXXyyjN7/466tfFzpbGSbHgPjQsGJdxLSO4e2j9nUMb1+4 vnKt+6JZ2mZnD9/c6L5YGYP7o9cTJ1+aoo0YN+q3b0yP4e2xN9++NQK0b7jxVGgrcw3c3H5ggGGm /flLAHS3nzmCZUZHGwIUxLw4TUTd8N/DO7OdXTy82GzXw5WBgu4LV1+IcD18bKAyAec3cflCm/XX n+6oq+Smzg+mehMNR67d4VpOjzsQaw8GfItfKL5c3noz9WHxU7r1YOFLOVfGUAEXFohSmzqexh3l XqXFx3P/TItde8/MqL3eRHp29dLi/LOfLr9b/JTvfp1/Or/zXBH6wquLb5R/pzZefO5cyqd+yF7v xtnr/crMGBBXr+T94wGRZ28CO9inFI2vn11VXD5CpuLJ+WWgqzVhpSt3ul+dv7czwtvEGc3VnS8X GthQjn+429WQCDqe7gTtzb66uJ/PPvxp6OLud17pQCPGzT+LYYoFPtLNG7i8NN0hQRGzoTTJE6uG si/Shpg3KrAHxK329pvBSxW+0+Oky4Yx0WHm8qfui7uTlGM7Y3bedXN8l+ddkLo48X5InWm0ZxYS f3cwpmtmPl6d6/DtijHx/V2klu7asVtuL/yWhFU+PgC+FV2ytjsgUjo3pacDjcIfu/99rVesy/3i YuzPPT78srX3YO/zx887/Zne5d7c/B1rn+1s7t7a29p6uvVfB0u7G4fbWzsH/Uv9ufkni3fulLi0 tbG7udWfOfJbSWMxzSMROHJiepm2X16ZfXdrNz/3ZvNkfG595/aX1X+9X3h379H8/MyOfX35ul0t S+c2D8VDWlpc/uU1Bf2UCCsl5LJI8cHC0od6+9flc4+vri99MGvXxuSH2PbC16e/wfFaxiFo4na9 3XwFp+3Jmcnl/ZkeCWtqKRT5H01GIMoyCnseS/IRrF9LE08mbtXzZ58uvv753PJ0jV8fLry7m5/d +PD06+Wbzxfu/dzD94tv55fTu/vzy/7To6Xr00+eyle/rd6YO7i4JF+cec8wzlHhmLTTv2YcfrX2 pmOYezujQMRIaMVAfmkrP9NZobW9ptIOJsFabw+anhUx6T7ZScQ/JpsyjZmSi+OA73ylQ8hLkZBX e/QFhxIp7t/QVzTjHHvh0zBMdXdq/It6sD78Ynr8i5XpjeEXs6MvEBd/k7eGX82NP/N18ePwi2PS cFEFYYz2bHIobPdnxr/4GDZ7E8OvBlS2oiFuGNVIy7O3gIfvWxXQ5euPcflovO33H6cxs4+aDG+c z5ea3hbvWQPUG7NXLeXl0Rz9D7tx/Q6m7VFrduPRMzbrx6I/y2/3Jpplmg2IxT2dHY+TXL9xecj7 jxe+DD5enH+48WFF5OXunTNPRszJpR0qqBPnnnaqYcFNLd+6clqLp7XXKm3/rMVHK1dH8hkPHp7f uv36Zfk4/+Tw/OebLzcfGozKjvh3HG2svfEiL12IKIwNfel6HjLLc41PuqVHi3Dlng== z3Vs/1zafv8aZbLPbWdMnjOIs4tPXj8dr4U5LTLcKYChVP5VBbB0uH55uTdBFbAw9WXv4Y25/e1r Sy8Xt199RwEMdc2PWceRbcSu+v8t6ziyjb2J/z3rOLKNI6n8563jyDYerYD+o4jgiLFPVDyekLX7 Y4bn/M03cSwG8+hEDMbtz47HYDbK3RNhnIPJ/WEDD08GcS7eHoTleUR6WrBo9+LJeNPsvbFo02Rd 80eiTencmYmF80vHAlYd74tDpJFIWQYivsuz/qfLr6zYiKUZsIAmQ0kDswyU9nr/racj1Mnx24Mj 6b5zF74bwei8cMQwvhfBuPA3IxjTOz/oua7N7PxxxEjjRTw/f8xvbwP65ehpoNP3pn8sYpT3fsRR 1+3o0rnehDYxihjdeH5xXdZq+hbPV6W+GM7Jl+O/XXmy32/GQ0niinMZ5C0nF2Luby7E1HgMDUHB Ua7yh8KCunPkL/fh2P7dqZm/E5rU/SKnNMB1+eEm/qdRudEv/U6Z7zRx5ATY5XczP8iGM2PT0pv4 wxaHnRfZyI+n7ncnsn40R++z3+8hBiny8mccM+NGr1I+11fdvrt45L65i2Nd2vGXH3Zd2nbDOoXJ zaeiuG/sK1QbHqeq+una7eearxgzKGOxjAdbO82wvvh11r1d3pxBxmhWcRvyMYrGBy0j8+wsTc+F Lnsmn1C/RjusoHtohxV0a/Ij3pjVy9f7W4rG/fkrV97Rgzcf3tz4OGYrxzt6zLSOfXHU9xz74pnd HH5xzM5+GHvLONyfPX/n0xBAazbZ3l45O0T698eRvtiFIXi7Pzv+xddLYNz7c8Pq1HjfKFheTitY 7vuaGLPv30wM2340rbe8/2ohfI9mhzrpvLtwbRKFWI8GDXKnRbTyyFBliobZWH5AQmt24+Ur20V/ bk0PE7KKPdxUujScmKcDfYuZOl+M+/Jk4uZMnRLXzKyeP4IKJre73xZTUHqi5HBYGvN07kdbPK29 3mktmrEW7dy5/SvTzy7vpWvPwv35/HpzsiH0l0/OdNnWV26UZh6xl5s63PyVA6f/8nxmHIgvX4XE Px80DP7yjlXGX9p40IKiS1+eufbp8NUbYit3c2r9bVd5tJKPJYa63ULi7GqdgkjEPCXwYscWT762 W0TQOgc3TI4c3KFPekGTKVv/2mJlw5SmflDooJGe6SEWFEG7ceZ1k6dFopmHMwxYzX1+cTBQ3Pp5 5tI0tYU/f/nmz6ftz/DXV+/fVR7CP71uv8Xk5rOG5Y/VRdDhPlYSQa/72uKsZj+PFkIM8a1IRl5d Xnz367x4r+OxutuXpsAs9zi3olyKLgkc4KIgWGHs/H9f613uTSDe8/bmzuZ4rKc3MSGUJ1sHh19x Q3y7sPXx887K+r+39nq2r/838n/8m2vfutJHMMn0I6gr73uT+98+Wm/yVH9FdPnbufm9g6XPGwef d3fW9/7dvwTSi3srz+4s9S/1R/de7k9Kb8xbuVu+mkKAiffdR7SJD53SyHe/lNb+a/vLjnw9u35w sPf5/eHB1n5rdH5vb/3EXRufPn/Z3Nva0Xtcf+7OzsHoW/xz8O+vW/rt5KeDg6+X5uZ+//33we9+ sLv3cc4Z8UBkLFP9uSfyup2PR5/9tv7lsHsY9P1L3793Z3273dq6126e+fvDsn84rD/v+pPV22+f fvq8f/PLFjjlRwZw4hHS5b+e6c/Lfy9+7x3Knwc9M8jOxlT7ZpBCNpEfTC7By4dY5bssH2wF7ZQP L9alFTNwZE1p9t9y+bN8+Jfw6e/90L/Xf/Xa9Dfxxsc9HwfWW9+3Ng1CDam/DZLxxZKUQ7R9HwbV +SKEPDBRbl7rCSmZUkkqNfW9H9RgglyXQQwiC0KQ75VQgin9jZ53A+czmqnyzlD63g68qZYE453r exlecNpIMlaesXYQbI59a9KgmAJBK9JR6Zp8SkHuMH1XBtnqv9IDko18UevAVQgnpJK34BOf4Yc0 cCknIcoIZL77SV5asyMhOWv72WDYnvfnQfTG97Md5FgL75HhlX52g+iCDNOhV14eknE79txFWcYa QYloNQxKkg9ymU3ltQm8PwYb+4vygJc5dXwgRIfu+oGNMiQheJnt/moPM5MMptXIZNYc+lW+qwbD CBgb/mz0oqxGlFfjKsh7ZJXks8y28E7/xIov9j4ow8QRx8j6xupxkzBD9hlsEWS1bCGpRHkX1ryk onxiXAJbyOByUrZwKZIvvLeWhGyMLLEb5CTtKeOIsiRf2Jo9l91UQ76ImQwoBOcDCCbF2lopkdMr zFQzGcN64UALxjBJZrqtcd8JAyQZefeXX2BpqnCDLx7q2419LZ8bd2RyhwPfyboW+QvmcEavk3wv vFGs9Ah3J2FWeVB4w2MUcu2y53UpDrwi8+VkwoVXnA2h8YYnMzhIC5dtIEIjlylj1uXSBaPfpyF3 2ApmcAORHSy4fIghgmBzyY07vINAGSvdNxncEWIkKyl3eOWOVDg23w/CftID+SzrHxJl/ejCd9zh jzCHSbhFGvLSyDbWPcmLhCTjiIG8YUJV3vDRR/CGExURle+SF3UCVnDWkVCicJ8Qgi+RhBpkcZU3 YmyqBzMlrFACdYQQsFJgFlU0SdlSpldejlmzJg5Clje4fhFhlM7JJ97hsOrFSr/0r9Kd8oYVOe1D YtPwLnxuvJHAG1bnTxY2Z/BGCMHzWuajgDmGXBqbTIsuKUXvCbKk4A4XnCPBUU2IpjMlt4cgKhli wLWRSbUyCUIQ0VFC8PpMLI09hHWS13ax8hD3GPje6l1jDoOJM2IoguiBCr2EWXJOecMpbwRLdeLA G6bGrOq5yriOr/uQNcYUB/qWfKGEiAqHPQFvZelSksk0wlErPcpxFfaQ2Y4xt75QvFRIqUGj8Iew Aa8CmV0VQPQ0L6I25Vr4I4kdWiSnQK+DD4wRFpTXSFdCreQDiqQHH9QqM+d1OT1MjpfvR3/xBaSk ihUovNO60df43BghUkl4q9LbmZDgqbmPTQJm6lT4ZxoMAd7rb+xuf9093Nns739a/7rV3xYAMQYS bH/+a880xNnmWl4ZOcOWM2KlvxU8L39p6prZFU0j3DO8JeOpjZ6oh6T36i1HGKFrxpJH7fDW9s6N 3sL7nmldWfgknZt8tgO0s9n/uLe++RmJUjul30NOwc/jaFke+tgjjBHjJNLb/Z2NVMLyhoVtctfC AtDR154+7LqBm0EQ80dra2Rht8neAdyZoWa86oMUCq2n3K9wxojYgFBsLUQXYtOwkjmCBNYW9SJW DryKLq+RVaMRICFra6MoxhXlZ8huNqqBlSQGyqlYgzGFECMMhbClS/0TPV7EBJ42a15nbbaakxMm ZBmD92JhZ4XzKxZDZg7/szQI2q0A/dR/sd2b5Zde1M1scSHLA+WPHiyCwQbC5TL5G3/tXQt/+V3d dMqb2jKHxp1YSVkLLKoXtIPmsXTOyuICcpALRQXFHGm8c4VetWK/irxcFIGgEdpAUZLBVlmdIGtR YGXFpkXwiHRPlHKlZpNVgXoMwgE+K0kQtwVuSoLf2B1q3RP92/izJbTllDWcBTQCtGhz4kYfpOFa 8lj3BORxCFjKYnMj834xCCcfLA6oMgKk6aMB8/s/eePCD75xOP/DBzdGYjumqqIImhc7ETP+YmmH 6jqfVNdpqK7TUF1HVdf2qLom6A1j6jqourZj6jp26jofV9f1uLq2nbo+1t+VH1B3bqpPGZCu80OU hsgCMMG2wFk6xgnJ6kvarI596L5JYmkT0Sh6MVKJh212O31oZBKz2FEvywE4s90TiyrwTRZKDF6A bwBsJVMhgmmKwHBh6gCAA31nITYy+wW4UtwDWthEybV8d4KkyTAEivWrCJqIuTxBSmkUvcGJZ4NL OB68tu1a3L7FHv1IIkBwqVicJPKYIk2OIoxSRaPCBpkCu45uiaRbqPEKSBwTgY3gpMq5gRfJwYOA wdcELKCkoBSrd+ADp6d0hLH5ojaW2RcPEjpAMLARbp8V7ZYxJOgW6bn7rqVzzdRhUQbihQOSzMKj Kt2HLOPCp5qS+L4y76JniIKFQ4DMkjCo8ESgWsQoRdGlRA1aqtgN+ULWK2HUAE4JENGOccSfBZe+ rh98cjKqt+7t6QGmSUM3qNaxUMbCwvzGxuH2492DdTTboivjL+nP3d89eLy1sbu3KROCrzn+3Ebf DT50Qz994HOPt9a/3FuXt/4Xgi+Ti/N3bre5ffphd29bvxoGUuY3d99vvZ2/UzGAJwf//rL1dtQn vSs3XIWYyxOgqafD2E6LuNj+i81Oqk9daPcnC33CNssqpP7kVP/Fc2GSpR7UbGnfi4wWYVd4XLBL IgQwb0mUjEMIQoTUQhqFHVNGCMIgFmEYt/Byg4hFKA6cLa6BqNsAHEsPQDSXWC25ttKCsK6DkRIp zjCCBp6BiFYlLpL3VxFLyHcSGYiIcgDpgNPg4YpwGg9UJAIrYq2OZSnaYQg4fWDgcSEIhHGqLj3d D+MHmLYiklOEUuEiVf6t6GaB5wB8Zbz6QYuYD9EerTWYciFkhAxAgOyfmLDFH8GcVrWwuEUe6DL4 HKEkraxvMNKYGDSofrBlokl0yVUYNJEnmZVZL98LYoOXlSw0mCwLoKrMLyMQXpYM8R76RqLoI7Sb zIVMo6ylhXzC1vxVwcz+f10wdejHRv6H4/5Pi2Vb51MF8c/W+Q+E8+IhvdY4kk3T/0ibEWETECqA 975NUnaijRFZQmhwhaTqLUlOjA8oRaOZRaCCiXoTWFpmVZwoL8ayL+AkakDCQjsGCLSsqEavQqKh ExKhA0gRoZNSBKUgdGYQchKfvCCIifipYbwNRrsUjTjwnmDYeeerCg2FGIqCTjulCmZbKBpdAyVU 6pImaB5BkipeD4UTyMjAXXEA0YVBSI08FoQ9SKLTw2EJ6AcpaDQDd3FWSKKWwF1ifZOSvA6NgMtq WzkimAiSY+vilDJOiedCSEoaxrd88EVJorItwxoFEXI2JegikyTG1utdAob1riBLKUhZuseJA+8Y eADFKgAiKciwCvrrdXiEqCQUfb04kA6TaRvCkA6VwmdSIoYFsGLcqyBIo604BAilN7CFRV+eXIID iXYEJfWrAJJcvVICQhxCScHbdo/wV23QTUcF5kE8k0MqwkG4KlGW9Rsmwsrig1LFh4VG5eKgBad9 Q1hF7mUcxrT5tN6yTZkw2yYdKk2Y2Cedc1lusUfQ7ikZpxRBkdT3zhPMF4xWFL1QGPwkpRjhwMrY j8dUCRtVXBfRpKvarMx7BUYVY4yJ85HXwjZZp5JMVSE+qehUwrMTgnhkVgkBwSbMrYMYk0SQB1IG dNW7EIXDoohHiJayQE/X2M3kBA7MMk8lcP5Srk5vYvgdswP2IaXUxNkRmNPuiUXHFaphOxgheil/ ncNcZNjFyKeS15Yh5HpLZkghi1RCgOVVgJF6C8ExXmUjbXzUtwjjfGMLISgFXCHXTA== OGAF8PY1NiALDwraYYtMMFQGVyHlAoT0raLxileCaDWGiozwo1KsB86X1orhQ7DcmKekIUwEuiEn FSkPkQ55syytEeGsfJatWPTbkWKpWhE6QUJGKNHpu22bSYEmNVuleOAMRKeSLhuMAJ4SVWtLo1Rr GVP0YCxSLPSUUJwKF0ORIusV8Q4XlSKGT55qoi2Uou6iUBg2EorMm5F3VEhaTnpPsZhz0VXi5DFs xCCcE80o4/2Gdp0rpGSrIdCa2Lcg/LOG71OwpAgbedwRZdI4L7lrgbknofgiksl3iNHDtS9YV4Qd RVXSRYpUw9JTseAVsyKwFj6wcDAEJ6h6geMV9C0Z4aqNnlCC1Rv4QFCcVhnPr/Q3EgaKBmLQB6x4 lKTUdgcyYlgai1wQPBV4e+gDblgBxSGOLY8kUdqgCPiMhc3WTPlNyHiQLQSkFCUYJYjy4DNQsfK0 zFhJlFN4y0gxCiULWyjFlkAesJVWKIqGiDHp2omcRpFpeTWMKqV0rQcKwHdBnBxPCPYWqYBdL1xs xAJgdAoyD5GUpN0WMOCp4+QWgYmwqNGQZYQQo2f+oesIUokisDCLIcejFOtpWOXVTsYGhV0M1bJQ MkI4HWrmgMiwJajXSwqVeIG4UPMlBCo9oXUzl0IpXp4XpV5LbZNnoGVL8wNIcR4+hixXoUrDGhio bEROTWyUEgjsYwrtqWSrIpBEa42QA+IDQhFL4AGRabgAsMAQ33AHlxBgCooZix+kfbmOVcXDafBW KAkefSLA4zVN0CoXn65H1jnD0iOHVlqmQK4DRoplFa0kNjtZNZQFCyyzg34hgygILELF8a0xy4JW 08WmAO8j4BRTh0V7GhnckIlBalYoTCqKuiB6SE2ga0sBsFVx6fkeIwiTnhfuAD8VjA0OD/graQR7 EQ6iQ7K0IPltKLbVA49mNaprEHSmuoQSgmmin43lfIiIN4oDtMIMIf++AtktIbNdcRKpEAMDtt2r U1B9gRcLRsTwgsbKsBDIj0lnS+RaFm8BJRLy2cKHQJMi79om01a4J9juLXA6hCISZJXiRNGQRyzN CxqGagQfxSFFdCF5zarahaRE8qPzrt3DFDSwX0mNwkgo72mKRQSGfJ5MW1PhlNJXENRUS8jgWDFg WaWlJjEBuHa5SZR0jjiPeQJKnTjkfMbA9SVFrB5lTJioSSahJdzgyt5EBJ4zHWZjTZP5hEAPgDCy onJdQ+YYPUzGN0p8LQR6go9CH/JuMoEUBG6NNxSsEeoqKLjSSBCV2AdMcrUpMdHKBRSkz3EJ+AAY gzVaxSMGGlgoYrcc7sgsVKhYDzgH0INoLNfhaJDtR3lEZYoRl8DkGcIi07fKR8BHGVEsWTAoziqi kIumieWaWV1cA3dsQLWKkpcnMuJGvIP5J1zDuq/hjoAJEgpzh9INYaIqoiYUa7igEdnW2ic0UgL8 Lc9mZVq0W0hi0ENBv4WHce3hSiyi36YWUizmOMqd0TGYUsAyawgUi9PHso6I4psIhm5FHEgKr+IO rR0BHpEpjrR1CShJtI/VNlIUJJXpTETcUaDSgJFi1iYIgIRgnSxwRM5Y9AASUILt0ETU8phslIM5 UqsZSOHKivkjKBQoBO+f0ydIoPIJF5uNiajuyYgUoBso5eGlgLCKS/FDWJMgTgTLTFBuYFgEkxlQ QueR4BKcTwMLaIuJqEzMCHvSg8xelxDsity+TG4QVK0mWEwZs0pVRL2fAO7AaaIvHLJxME0G2XMx p/IOyKcHk0QF36uQTxmex4qL4lbtX4mFhWPVfAR9ng4rjUHVFggqYAsK0ujAOQjEA6DoNTXlIkGM Q2IvgteJlEK7g0xPTUg9jgXPRNXARrCxYKNqO91YrXIaMTQI7Bl4QVVy1MQjKN41CqAV+tK0IMIe nIzcaXFrIbtxaA1QHiTaOI+0MgqURMQwBbmz07lgRAlMbjVVgBFBaoTLVtXaOwqfbTFD2rrMKh+r YI/aFRPtPdEgC0KQWI2Gd0SmMjLUuo0NTYfC5ffNPCQmEjBLkZ58yohqJPJQyY1CbZhDp9Bg72BB MEI4aXgGKApvRowBiUYWRkR4kfDZU1EfB5SKRAIyfoZMQtRGQJ0gZ7mFYnANH17G61GowjsK51X0 SGGSEfdgbTP8NUZMmEMWlZBL59CAUqhamV9ILWEB19Nqs1WzNdCcGRMMBscSF9UM9KQCqmiKUU2J yWJXAWaoTSBe1IGlcFYhkOxo561ZhQNCSaa5PiUiUQ73zbV+elT1YTWzIoY6TJRXr9iyUDll6NzQ CFFAA0UHhgNj4WilP8g8rcEPK+hDpsik5uGZSh4gC0PXIGAGYUKdnXi1LR5BLnFM7wsAVAawCYsF Sk4sjLLJsFUUWqWi1Xq+jVkTfA4gJTQvEQASyq4qI2UUEFILyzhqx2yBaUFHFI4MIm8RXO1Du8Wi YQw2hsaPyhkoaVC0wog7tbWwbm76ICcWEAiELyqVdMiEEm3zlaw1Zbx/gONw+6U3OTYAwyQRRmVL 83sYhcac4F7q7ECzgNrINbXUXhfHdnYCAL7jPBJiVT/TArjSimarJRDG50YhEEVysCgE0HJG0UDS tRiUSaFuCxU3LBg8Iyhw8Thp4qLaAFZ+0oI5JLhgA8TVoY2jDSqwcN/4PZLC8IQqDBrcSsM7vGJh gohETOMyFxnumLitMH7R0jGU9xGTytNWfHi8v4jbwNbwzCodRSTOASaoP5CKhT1DRKRUxfFE5RFZ 2aEbi+oVvE+DGUzTGfaFGc625sBTST0wUmiE8FTzdSGBlfewmha6TdRrTBqtWAW7GUQHsGiIH6cW 44koZk1cD64i4EBGsBcP0JZHZJDYYkUZSoT9capxiJ4iIG8XCUke02hZYoVL+Jmx+TUMSNAFEAqD h8r7coeIN8JLuC5oUqREfGxUGhmNIguFATUGPYR9QtXMErshIhey8o0qqSKKIKAGxTQKIxIBPjFl Jged0QAlYpVAAxbgAoZ2Cx3WAJCjFCTOpAtC4YIDOgjOxnUyZICMBLgwWDDK49CkgmDQKh3cNcQF ufAoStNIOMExSkayRjrkWgSmsvsyYqUg8BKQVjctTGiN4RwwN61xSoT1ZV75lxTnMZNe0Yg8g+Rv 9FpIt8pWETMDBT2SnhWveNohis6QqahwiqINCE1YZhQJpOQdjLqGDKxlNc7ICHYcZtdxib9wsKBL N/AAHVq4ZVm9DfFtUDcnopTAU7BvqIilU5a4MqXpC6/+CCnkO7rJNDHA/rGgzhJG1nLewdu49lyY 3Jg+qK8IfMLaXOTCabexmIYlC7ExjVMRAwNyRrPXsHQ2KmlkkQCfFBYulayGJZKrDQMMhFeG2o+r mxsEge6DT7PG+C7CK4Q4to2OfgNdYNM4wnBOEPtq4XD4SnDFoQjhhCLIDSuEpWYOhmVGMLRV0x9e q7OgmWF0NAOTSKA7At+xUgBLlwBgGBAS6Yr6lkhB0MpEestcXFyXGDQrG72Wtgmwo+dLbUgzVKx6 woFGyCR9QqQP9eWgICkM9hCVjWvqMGVBIjSkURJdT4aF8Q7PwmWE4xF2yEhKFrqrskjsF7PbFLlI i+kUPgJlIVXFAunYuINimpGe8p6uJhwwoPBIvZ2bLQEggskDU/J+VjJRmLjvARSU8pdmr/FO2CzN PxBsydjUy2JSo7CVYDWTg1pKLQdPjsyAiEjQQm0WkAklaRkbK3xZEYvkIarh8VTRxc6qzBDyRgCU lJIUNWTY8BWk/8TFIrKoGg5HQrA4rg/jjqRUzoPRGnqkHqHeO55ZRebRQ3ChlYFya9PbwDbwhRAS R8EGjU1CRThiXKgq7iB8depTgJkN1U516tOlrEVBK6BQacLgWEZXGBI3yp3CpqAgQOyq7rygn4TC YK00sEhv1s7BQbotIOyGQDGUABFeZFScMf5MRuUT6mUIW2QDNxRZCZbjw2WJTNI6KldsvwiMXxv1 UsmdLIXtHHhW5yMVgugk7/BAHkiNIUKFFa5EiszRFAVrluqtikrIOhSWHJJC2G21sGoFO1ayV20V US6Ea4bWKxgXLFqr1rgjqwA3i8XcQByAZgldAcUP/R5UniJZSa0hnczcpYHkLKvLggYgNctbqMCK yhuzvGQKBExiy8KiPAP+lY0sjUVGM3h1uTRnwuQZsthUWijlN1AtvOb2j9We1p/DeYcL1RJ1QB4E Gt62MThVSy5ZLVn3SJxhYqBBtBVfMwvrc8i2y6Xy2tIYYAjal9r5X7prhr2pKWgu26O+H33JQJmk IJeADiOcDEJClpzPZK10pMjTvSssIzZO45ogcdcD7kEQExWGNeic+2ZGWrxQywQKnSqrAsekeXK6 iSixDM6qpQevZsd6OGNb2CvQPnFILR4hnJDbwEUr0GfiBp0VfbvVgIP4lZlLblPW0FfkwDvZCyg0 0zFonEoIpSiBYAMEGHMdeMTz8M5y0fpRsg1jPEanT6CxFl2nmlqtEGOeIDG9LoPT8IlRXkwtytGC cKxLwJ3sCbkeRTaZURub6Fpy3ej5tLSUTi9tPGJBxeqMF1bnQLN7lhpFzUhBadekTI0ATPIkmVJa QwINmD838HDxmIB7Emps70eILjEdw+057f3wlZE16UjyAFxshLK7gpKolgw1Jq69D0tpeVfRRC3q O1jTAFtSiGRYJ2I1BB+cFqajmgRIHPF1h7pKEjQjhIz6N70lKSnnoLdkZnsAVUrhWHKbFTBL6Mpe qCQLlqARkC8EPqi6p0PmlsuKgHXuZjsiN1taXYmSEjLOaAcKS1vKqNxmwN/XRjpSirPyve0TP1wb 1aqLPvYCkHnSFBPyids91Kqz3CMr5FnpBVSbM9OBfCAqw+HQMynlkZEVCuvJQcGIUSrOXKJcQ+Gs 4Q7RM5oIDAyyhKR1AlgKwQdwdQT+6lKBFb+hH5qgwOoXVnwK02sbiACsnej7Pzgt2Egjpod1bAQp 20oCukBMFK4RCIr0Ge9d5R26NwJ5PCgjELBhj0FG7qdwtN+pbV5b1GfAtiCltoVHwUSrVlk72Zl/ cqCmalaTUU9Be9tkSgUiTndsssgL8s2MpVX1HYfBUaZDVL4ZiWCeMzbB1dyf1agxCBw+THyi2Yoa nUQOEpWf8mqG5VIrRMDgreoIZstR34wZQxQczhMCaW3is96jBdQgFM5hqr5dM4jjNaCmEy+ASO8p NnGvi4Z14JTB1KHuz1iuHuslFjlbdGAxRu5xMy2VhSVHsOXEfH53q9P/bK24WTTCrwuVa1U1bYHw TOHWENHlhTmTtkfZ6O48pl2q7iGWN8unGHTLCdqIWQkRLssab/HA1rFtilshqbjCuwi7MV/MusC5 xl8QIpQygjxIAut8demqZK3qN5BiI7GAriqbxchKtxOj3PgHtR1qaSqT0tRQ26QgyU2ALvMY4N0W 1sbI0idu+gNHVRotLCfiINBYRNrCN4EpEVbcsOAjeE3N18SQ0iJeweBDBQaM3EnDag== f1zDIwxw+VFAVHQpN7D1R0uVkBEy7FSyRYFfqcSsuKWhxVSYOkY/UtWbWmFdcBqS0DI/Qa1CSNXU ITpExKYavYEROw6v1Kzgy6NOyhs1UbUVf2LvN4wpqqYMc0Yuafi3thItFmUbVrEwKYwtnShYRE0P ak74BFmv+hbexT4dAA54TpkhVdfySaUq8BQKdp5pjQHjayvYNCMzykpQ3ZDpNCZW2v6GVWzEDSUE 3Qudqu6/Rr0HHCcaEZ8UbiPjaxms8m2XNaJLGv/3GcWLgElMZPmsLiRCFi7BZcQj2LeJ2A/qu33V RAEDDgShgU6kFoQzKI+gXKvkiATjwStSh4OKygKwDVLSKHbHcFfBSCxVFwrdZfCutXRQGcQ8zt3/ pOBUlApaGGGGErexM4MAM6CYkghMTDpza6GoC7oCsy9DYUyPm/JD0Ix9wEajGJWNqSVD7SLSQmH+ FYXkgUVuARvHZdqj1eifbmCDBYjD4jSPXjBGS+xEAksiEcfViJNHYF8Dt5wtUiySgKAUIh0fldcj zA/jlD7orvVodfckKRn7eRDeRWyRFKBKvpyRS5wGIb4lm4lEdb7B4OjUTVsBZ6JGCoPSGKn3GraT dmNm3TTYO3MmWLlBAvxAIQR9sVVvF6vDNoHjbeYNmRVZrmpINkK90e92rQ4mqutHgjg4zMO37cjY sS9dQupeq0kcN/biEYYAHHeHcHipMFyN3dZAYthKj6iqc1rZifICT3+vO8wBSW5shgIQ8JqtQOng GuQ9YnNtrAp7HQAhxlIY819DPzlhVZ1sHFNRC/MfdO4cImCi4nA/wLoomaJpr9jgzPgd3WX7GgiG DzDoFDPzyjJ3AS4lqiIySywwv4LNuFcE2XF5IKJkAYPC3sg1bQJRHaPGYIW9QFITlWYw0NKGVRBZ s85d1Zg8UtMwyzh/AfksFBBlWmNQbGFEWVxG7tG22O2EeAUcT96RMDnIiAcmW71pGf5WAI8jI3JJ Gr9IpQ3GB0ZYgzAcTxvIuhOVKU2eUJBqYHiAaSPldI1DMlZPgqkauxRBz8rGnV/PUnycahBs1kJO S53cCqgZh6i8g4XUCGpV7Rpi8Lrj32F3GIeTIUmpZaPI+5oARYSwvbjibAeGFcnHvlUTYMdzZYgJ Z30wHIjAL30rULDvEr609U14vU3M25bKIhmPPIoWRfCMAh0kfCMUPSAM7K1iArjW0XgdQkasBhuP cJqGroiPmmemX+tb7T12laIaQhYxMsVcbFZW0o1KOB7CNlZibhkVPtZ2kspIABkxqSHNLjFb0LQc T2FI7Am3nZPiWV0ET1nNL5RC0NoKwV4Qz5A95zIl+iA41SN6hrVZ8OdSK4HACTwsRqeVN7rH31FJ aLzZaZyMKoA1XmjUMUHlcGqIbkmnn0CKbVUyDQdge1zV+E4rTbZA2I7xkhYnddxURw5iwZOlY8gb cGyGOAgo6wpaIg6L6ZCMLpwlqtY1NMo0Wh4WDlqyGvMbTFjKM8IyrBzgwTrOtqR11RisDBBRWc/a A6qoFVC43wvPRFW/TjdGaACf64NdIwg9YY8bwyLYtMYqq9ol+3B4BauqoCvIPM42tDA0DtirLp4H KAx0kpItEERLMmt/YtFqPR0UcreFwAXukXMajC5OixdX2UYL/zApi/5j+yvzM7WNmqFNhD40SI0t dqh0RlADBgy8YmorzGOFtmO6mckRGnunlTzIcwRVeUnP6chVjwkgqzNGiSyFDc22BaA95DGARKBI URXKOwwys1SURZ9BaxAgo9MYXWz6mmXlzCTSs3YAV1oBKH98ew+KsgtYnelNl7UMBTtQNJrtUAqA WlZMC9kHKrnNtU/0fHCaExJ6pcUe1fQGrdvMoZPDrCXL2TNS6ZKamq5gGrwhSIyhoDS0rJDv0rZD cJUTYAGqVRFatQSyWlSKO8jtuqpRdQy53fvMzU8W0AUb2pE5R/wuYWNQUQ8bCTFAa/HtijolhWfq JNzB3U/cGFW4iAZJDybNgkJECCpCBFjo7Jp4V6tLryclySImxdoVO1K+UTIBzHWzi8cdES44uYml VBR3zbw2jIZDC1h8207UWkFvWViBvngNGDa3jKHZiB2MhNswsgA12IPaagthp2W82LkDt9SrJlyB ViloXSi0KnxL9C4rRaOuUXuLCpFI+2KjKkb4v841SsEmPVaaKINZPWxI2jFw2jHmgiIPp9gQ+gvy CoAJRMbN5xbnoIGCVcE15iI6TdrhuiiGY5GAblfHoVhEtplbZyPSfQDVGnqBlKJoDZUPjqoXfhty JyhcQHYJ4gM3CqUNVTGFKNwEF4A1A7CLNRDeJ2PaiViQpdAOmRLbX3CkRkhaNUjQEbEHGpQAsIPY l0hNgD9ImcUzIlDiTDSQi0dQlh7anjlSGHaAA9yED3xceXAOs/U+avLcN1iyBo+B7gtcCc3Je+wc g9NYNDDkq8b7fEuByDNV/XEcuqSQPBgttIRoq5OKWgtAFlA8uSS0bQidiaSLwypHuMFA5cHpnkTI NtYGZ20g0SPX+HqRfhM0uENYItD1o3+Lg7KQMaZnRZMnFNZN07Pihhksa2LFDU80EmHnqUyWwQ5W lhPvM33GduHCZ6gfthJQchpB4QSq44r4DRA5ysNIoRpzRXOHfAqOPgBzYaQWriDiLFSu6j4C8Gce ZYE0IMLMKGgDFqSFDq0kCQwRKeUIiwAHe27MJsuw6Nc3fbNGlxRVBAChCMqFrPvU/LAGL7TNY143 WQeesyCdSBrJ5Q2YUN8OcGM/WTrkUSYZ6GxhtygYCQpyrRcRO5MuB+6RBqvznC+MmEApGo2mhqRC vQIKvWocaoBNYCsn/O1/MOILDW6otHLJiPfCHqHqAb5p9C1nz6x7pyxKy8NFq9ujWCVDVxBVUNhI ijoIeG1wZ+EDsy7C00OnolrkRkpwENxmOFl4B4vMDWwyAQXTnaHZXz7B0kYoD68bOjlJ4BjrdLdm gjZCCMJrsYEn8AtZi7xJ0GIlZGm1agCRn0hWoJ4hhTFORCnikMKwADVOaa4Bxgq4vMpdvWCnaBU0 4g7Uk9CnZoyHpe9OC2M22F5T3zz9D3ud2oyzbmJNF0XL5dt6H1umfzTkH7S8hDVMGvFHlb4WErnI fCK3NIOQomZHmRRDiTWyl5qEjEWdUFEvSZOQ3AmDXJ7VzCXTdij6EUvXpTpD57haxhjpQKcWIWT2 VjfPEchrfpfRUZR/+6z5e2aKmJwPscuPY5Lg3BYGNXDYpdNzC1uhG0oF4NZhh1DW2CorMXB6nfFM BRqN+4Hi2/5Yandu9ms7YWEh2f0aW+9YN4liLx6Yhe3asJksRo+6y1sLB+DOa+YBiXgsNuvBou4q 93gaOYPYErosTOq2iC1yicS+aw5Iexda8WBLyjD/EpmRSS23hIQ6sh2sT2f+M+hGstTW6jgr/IMR Q6ZIgQGQ8zc8FREkrbVBzMYFPXeG6RRgR6eFDFnDYKkokmZCNmsJBc3XWkvaspZGa4xIYGFX7qof UDmK0EXWMIhmelkjk9reGuaQwYxZg5Ka1KI885aiSa2o5VtgJB81hUWPpDsH6JvmwsTR4vl3Ner5 ejwpEPe0/LixWo5KDbOh+fFW0uq905UocO7H2bqlwkFCcT/P9tQtFowZ6kyQGyB0iPwxo1+U/+iv rTGdF1DanaLaNhI4W1FLHL/xHoIfCF6I2k4rHaT88BqIBARglo2Wu8Y+RqxG0ESOHRJqbu+u3nAm mNvgQ17r+oSd6Zvj3TDxCYmFrO8ihtX6atvaYXoLhSmmau3AMf76BzUktiMzJMfNGXp6lIdBoSag /11R/JRYIuQjzwXgXu7CoXIzH7IdRleYpQKrfAZxpJR0oLVtPeFc8JyJYy/+RxPa2PWvR/byXJtt JUH/dOV7zBVaPZnVWIpIbYf0Ot1doRk9WlKSXEvfISzPfVA4Y8xoVS6uPQ/pYcIRGT0ezRo141hT K1fkkZ4gRF7n2r1GNxp53TYAQneeIbc3aCrWR91tlfQhAPXEVyfNbAJQZbZik3KWx0ZPFEG2NLBp p9chBoWYI+s2Cqvb9Gg9Hr2lW96qD+0Rp8Ey6hjeAfSO+GC2mi/vtlWxNklP4CIH5S7J2R1aAKGM +kxA1AJFSqElT7NuyOVpTLSOeTgcbn8BgSdOcC9dy1BjL2xwwzpO7b9tsbzQSZNWTlr1EY4QwLOr 7Z623Y3HHGPhq+4Q8bXVzVXVFrgnqZstS52q7lyhJ6gJXz0K2mgYVe/KNM9VM85KotJAPRiWCNlk LWpCahIJSQtnXreAYN/dIksDAvZGICiJwmrmqJGz5L6ZrIQjXP9PZuMpLYp1GUzWbDxBLQJs1Xaj 18NG2vQzQQ+YgfNJrNHyBbjzQXeyxqQiRNCAzboaPGVdATQocGGpuh70ekAAkP2m1Qn4jlXLSYXG ceOtHsqNlWiJS3pqKmdaImW706YqbCvPX8naE54fwjLmHNsjrAjHPZlOLwUc7gqiK6Xjg24zcYSU khAa7kdO+8T0/UNLwzVwLLsS/i9tVbh9i4cFtFqVVjZR2rmZjeR1U3+2bbYYCYIPEa1OzXjL3z2y NB85/S6K+dTDQN3wb8tge+4DQPwgM7CCo5lq0c6CSwyA7yxj1dhkPYsd61B1CF1Zp8cCxiry82K7 J0oxI2iIM72kl6c/OJszKzq8nirJAs6FDRZSIbiIBwx0+Y+8c2EbCVTUcGFQGVHUUx5DMY1pR7gl cafwGCpvkPjhLhjsrT7tdazbZe1C0eOYFn5wjCjsYTRDnxQ2H53B+Q/w18Vnf7OVPz8v8K9wzH/y TLLTDiLLP3Yg4Oj0YxyGip2X6kHyCPNtpZh23pZlCgTJ5ty8MNYQhrZ9GqcRJYYcuZOvtp11iwjU IOCCcn0UxSK4YrVukUUiCGY0FWAjT+9GJjipTdZNHa7BiXaIMTQ2a6yDZt0WuQUBNVHcI4XzZawq OZyU5DKripgxRFog81huIsnM8laLM19Q8FK0qIXnCvFQM9b1B/q/xbaz+eE04shASEAZui+CRY/P HtXSLDI8PDofETixNrMWG5oCD7oTacLpIn9wZOBIfQmW0JNYHWwuStNNUrasSDLNFu7cxglfPncV XbLYOJccmfJZlFzjRDO4MDjUw3GrKLOEPKWkaMHAn5wPOIlT+sQjrvlvSxdCd0dG88dj+U+f+Xf6 un3vCMAfEb3gtH4WPoYlbCHF+yGFlTHcuIGaBB/0PHRso6tauIy6k1hZoceNtos9nntsNZoH4OWt Oj0op0NwNCrXB6O5oRbArl5/4sSiOiIhnYUfCdGMPU/rRRJNDyBGLrTw5w4YgudRbmJNkL5HCJBn oYSkJ9zWosf6Fbj7SBPgQEskslidVLWIAikoZB64N5cYHRjRawDLIRWF0Rl1MFn+cHzi2iG5AmRp Oh3q+cV0Mj5teMa3TAUOpf2OnNUjMKGyHATNUNy8L0CNswh2ibTPOsHAuqEfyqrtjg== lqVFZhg7mnE4OX7FAPEOZMtQg4d4LCKh6HvWWpb/f3KGAR0dzp8M5j8uaact3KmCVn9M0J716olP Kwv6w1k3dzb5s1mzs7j2+kNai3u7X/d7h/tbe5sykv4cvtjZBfXe+t6v+/1fd3Z/3+nv7B70/8/4 TyTh+M4l+e9xD95Pwf5zfDBEWBnXcXj0+OSrx/Km/YO93ddT7PiLl0cPJu9+GgnngZ74caRZgX5Q lyiHnxXRaxcrvSc9/J2t/IYfoUH4xWjsh/o8fzxo2IBedS1YPqZNWBRzn2hCVPNYGxZxr7E2rCtj rbSrP2injUWbGY5Fn2ujaRenjGf06f/2P3/QdZR11VWcmHi4/nHr6d765y9be72P++vftvrrOzuQ oa2v8o2w09b+we7eVn//0+7voMgj3e0TEzcf3Or9P39xpbo=