Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 4.3 #204

Merged
merged 15 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ This launcher is compatible with the standard Minecraft directories.
- [Setup environment](#setup-environment)
- [Contributors](#contributors)
- [Sponsors](#sponsors)
- [API Documentation (v4.2) ⇗](https://github.com/mindstorm38/portablemc/blob/main/doc/API.md)
- [API Documentation (v4.3) ⇗](https://github.com/mindstorm38/portablemc/blob/main/doc/API.md)

## Installation

Expand Down Expand Up @@ -107,13 +107,19 @@ needed by the version before launching it. If you provide no version, the latest
is started, but you can specify a version to launch, or a version alias: `release`
or `snapshot` for the latest version of their type.

In addition to Mojang's vanilla versions, the launcher natively supports common mod
loaders such as **Fabric**, **Forge**, **NeoForge** and **Quilt**. To start such versions,
you can prefix the version with either `fabric:`, `forge:`, `neoforge:` or `quilt:` (or
`vanilla:` to explicitly choose a vanilla version).
Depending on the mod loader, the version you put after the colon is different:
- For Fabric and Quilt, you can directly specify the vanilla version, optionally followed
by `:<loader_version>`.
In addition to Mojang's versions, the launcher natively supports common mod
loaders:
[Fabric](https://fabricmc.net/),
[Forge](https://minecraftforge.net/),
[NeoForge](https://neoforged.net/),
[LegacyFabric](https://legacyfabric.net/) and
[Quilt](https://quiltmc.org/).
To start such versions, you can prefix the version with either `fabric:`, `forge:`,
`neoforge:`, `legacyfabric:` or `quilt:` (or `standard:` to explicitly choose a vanilla
version). Depending on the mod loader, the version you put after the colon is different:
- For Fabric, LegacyFabric and Quilt, you can directly specify the vanilla version,
optionally followed by `:<loader_version>`. Note that legacy fabric start 1.13.2
by default and does not support more recent version as it's not the goal.
- For Forge and NeoForge, you can put either a vanilla game version, optionally followed
by `-<loader_version>`. Forge also supports `-latest` and `-recommended`, but NeoForge
will always take the latest loader.
Expand Down Expand Up @@ -260,6 +266,7 @@ you can instead search for many kinds of versions using the `-k` (`--kind`) argu
- `forge`, show all recommended and latest Forge loader versions *(only 1.5.2 and
onward can be started)*.
- `fabric`, show all available Fabric loader versions.
- `legacyfabric`, show all available LegacyFabric loader versions.
- `quilt`, show all available Quilt loader versions.

The search string is optional, if not specified no filter is applied on the table shown.
Expand Down
2 changes: 1 addition & 1 deletion doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ This page documents the public API of the launcher. This launcher
library provides high flexibility for launching Minecraft in many
environments.

Documented version: `4.2`.
Documented version: `4.3`.

## Table of contents
- [File structure](#file-structure)
Expand Down
2 changes: 1 addition & 1 deletion portablemc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""

LAUNCHER_NAME = "portablemc"
LAUNCHER_VERSION = "4.2.1"
LAUNCHER_VERSION = "4.3.0"
LAUNCHER_AUTHORS = ["Théo Rozier <[email protected]>", "Contributors"]
LAUNCHER_COPYRIGHT = "PortableMC Copyright (C) 2021-2024 Théo Rozier"
LAUNCHER_URL = "https://github.com/mindstorm38/portablemc"
16 changes: 9 additions & 7 deletions portablemc/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,8 @@ def authenticate_base(cls, request_token_payload: dict) -> dict:
# Microsoft OAuth
try:
res = cls.ms_request("https://login.live.com/oauth20_token.srf", request_token_payload, payload_url_encoded=True)
except HttpError:
raise OutdatedTokenError()
except HttpError as error:
raise OutdatedTokenError(error.res.text())

ms_refresh_token = res.get("refresh_token")

Expand Down Expand Up @@ -332,12 +332,12 @@ def authenticate_base(cls, request_token_payload: dict) -> dict:
res = cls.mc_request_profile(mc_access_token)
except HttpError as error:
if error.res.status == 404:
raise DoesNotOwnMinecraftError()
raise DoesNotOwnMinecraftError(error.res.text())
elif error.res.status == 401:
raise OutdatedTokenError()
raise OutdatedTokenError(error.res.text())
else:
res = error.res.json()
raise AuthError(res.get("errorMessage", res.get("error", "Unknown error")))
raise AuthError(res.get("errorMessage", res.get("error", "unknown error")))

return {
"refresh_token": ms_refresh_token,
Expand Down Expand Up @@ -465,7 +465,9 @@ class AuthError(Exception):
pass

class DoesNotOwnMinecraftError(AuthError):
pass
def __init__(self, *args) -> None:
super().__init__("does not own minecraft", *args)

class OutdatedTokenError(AuthError):
pass
def __init__(self, *args) -> None:
super().__init__("outdated token", *args)
114 changes: 74 additions & 40 deletions portablemc/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pathlib import Path
import socket
import sys
import io

from .parse import register_arguments, RootNs, SearchNs, StartNs, LoginNs, LogoutNs, AuthBaseNs, ShowCompletionNs

Expand Down Expand Up @@ -67,6 +68,13 @@ def main(args: Optional[List[str]] = None):
`get_command_handlers` function.
"""

# Force stdout/stderr to use UTF-8 encoding, this reconfigure method
# is available for Python 3.7 and onward.
if isinstance(sys.stdout, io.TextIOWrapper):
sys.stdout.reconfigure(encoding='utf-8')
if isinstance(sys.stderr, io.TextIOWrapper):
sys.stderr.reconfigure(encoding='utf-8')

parser = register_arguments()
ns: RootNs = cast(RootNs, parser.parse_args(args or sys.argv[1:]))

Expand Down Expand Up @@ -273,17 +281,23 @@ def cmd_search_handler(ns: SearchNs, kind: str, table: OutputTable):
if search is None or search in alias:
table.add(alias, version)

elif kind in ("fabric", "quilt"):
elif kind in ("fabric", "quilt", "legacyfabric"):

from ..fabric import FABRIC_API, QUILT_API
from ..fabric import FABRIC_API, QUILT_API, LEGACYFABRIC_API

table.add(_("search.loader_version"))
table.add(_("search.loader_version"), _("search.flags"))
table.separator()

api = FABRIC_API if kind == "fabric" else QUILT_API
for version in api.request_fabric_loader_versions():
if search is None or search in version:
table.add(version)
if kind == "fabric":
api = FABRIC_API
elif kind == "quilt":
api = QUILT_API
else:
api = LEGACYFABRIC_API

for loader in api._request_loaders():
if search is None or search in loader.version:
table.add(loader.version, _("search.flags.stable") if loader.stable else "")

else:
raise ValueError()
Expand All @@ -292,11 +306,11 @@ def cmd_search_handler(ns: SearchNs, kind: str, table: OutputTable):
def cmd_start(ns: StartNs):

version_parts = ns.version.split(":")

# If no split, the kind of version is "standard": parts have at least 2 elements.
if len(version_parts) == 1:
version_parts = ["standard", version_parts[0]]

# No handler means that the format is invalid.
version = cmd_start_handler(ns, version_parts[0], version_parts[1:])
if version is None:
Expand Down Expand Up @@ -362,33 +376,28 @@ def filter_libraries(libs: Dict[LibrarySpecifier, Any]) -> None:

env = version.install(watcher=StartWatcher(ns))

if ns.verbose >= 1 and len(env.fixes):
ns.out.task("INFO", "start.fixes")
ns.out.finish()
if ns.verbose >= 1:
for fix, fix_value in env.fixes.items():
ns.out.task(None, f"start.fix.{fix}", value=fix_value)
ns.out.task("INFO", f"start.fix.{fix}", value=fix_value)
ns.out.finish()

# If not dry run, run it!
if not ns.dry:

# Included binaries
if ns.include_bin is not None:
for bin_path in ns.include_bin:
if not bin_path.is_file():
ns.out.task("FAILED", "start.additional_binary_not_found", path=bin_path)
ns.out.finish()
sys.exit(EXIT_FAILURE)
env.native_libs.append(bin_path)
# Included binaries
if ns.include_bin is not None:
for bin_path in ns.include_bin:
if not bin_path.is_file():
ns.out.task("FAILED", "start.additional_binary_not_found", path=bin_path)
ns.out.finish()
sys.exit(EXIT_FAILURE)
env.native_libs.append(bin_path)

# Extend JVM arguments with given arguments, or defaults
if ns.jvm_args is None:
env.jvm_args.extend(DEFAULT_JVM_ARGS)
elif len(ns.jvm_args):
env.jvm_args.extend(ns.jvm_args.split())

env.run(CliRunner(ns))

# Extend JVM arguments with given arguments, or defaults
if ns.jvm_args is None:
env.jvm_args.extend(DEFAULT_JVM_ARGS)
elif len(ns.jvm_args):
env.jvm_args.extend(ns.jvm_args.split())

# This CliRunner will abort running if in dry mode.
env.run(CliRunner(ns))
sys.exit(EXIT_OK)

except VersionNotFoundError as error:
Expand Down Expand Up @@ -439,19 +448,39 @@ def cmd_start_handler(ns: StartNs, kind: str, parts: List[str]) -> Optional[Vers

version = parts[0] or "release"
ns.socket_error_tips.append("version_manifest")

if ns.verbose >= 1:
ns.out.task("INFO", "start.global_version", kind=kind, version=version, remaining=" ".join(parts[1:]))
ns.out.finish()

if kind == "standard":
if len(parts) != 1:
return None
return Version(version, context=ns.context)

elif kind in ("fabric", "quilt"):
elif kind in ("fabric", "quilt", "legacyfabric"):

if len(parts) > 2:
return None
constructor = FabricVersion.with_fabric if kind == "fabric" else FabricVersion.with_quilt
prefix = ns.fabric_prefix if kind == "fabric" else ns.quilt_prefix

# Legacy fabric has a special case because it will never be supported for
# versions past 1.13.2, it is not made for latest release version.
if kind == "legacyfabric" and version == "release":
version = "1.13.2"

if kind == "fabric":
constructor = FabricVersion.with_fabric
prefix = ns.fabric_prefix
elif kind == "quilt":
constructor = FabricVersion.with_quilt
prefix = ns.quilt_prefix
else:
constructor = FabricVersion._with_legacyfabric
prefix = ns.legacyfabric_prefix

if len(parts) != 2:
ns.socket_error_tips.append(f"{kind}_loader_version")

return constructor(version, parts[1] if len(parts) == 2 else None, context=ns.context, prefix=prefix)

elif kind in ("forge", "neoforge"):
Expand Down Expand Up @@ -770,7 +799,7 @@ def finish_task(key: str, **kwargs) -> None:
ns.out.finish()

def features(e: FeaturesEvent) -> None:
if ns.verbose >= 1 and len(e.features):
if ns.verbose >= 1:
ns.out.task("INFO", "start.features", features=", ".join(e.features))
ns.out.finish()

Expand Down Expand Up @@ -866,16 +895,21 @@ def download_complete(self, e: DownloadCompleteEvent) -> None:

class CliRunner(StreamRunner):

def __init__(self, ns: RootNs) -> None:
def __init__(self, ns: StartNs) -> None:
super().__init__()
self.ns = ns

def process_create(self, args: List[str], work_dir: Path) -> Popen:
def process_create(self, args: List[str], work_dir: Path) -> Optional[Popen]:

self.ns.out.print("\n")
if self.ns.verbose >= 1:
if not self.ns.dry or self.ns.verbose >= 2:
self.ns.out.print("\n")

if self.ns.verbose >= 2:
self.ns.out.print(" ".join(args) + "\n")

if self.ns.dry:
return None

return super().process_create(args, work_dir)

def process_stream_event(self, event: Any) -> None:
Expand Down
14 changes: 10 additions & 4 deletions portablemc/cli/lang.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ def get(key: str, **kwargs) -> str:
"args._":
" A fast, reliable and cross-platform command-line Minecraft launcher and API\n"
" for developers. Including fast and easy installation of common mod loaders such\n"
" as Fabric, Forge, NeoForge and Quilt. This launcher is compatible with the\n"
" standard Minecraft directories.\n\n",
" as Fabric, LegacyFabric, Forge, NeoForge and Quilt. This launcher is compatible\n"
" with the standard Minecraft directories.\n\n",
"args.main_dir": "Set the main directory where libraries, assets and versions.",
"args.work_dir": "Set the working directory where the game run and place for examples "
"saves, screenshots (and resources for legacy versions), it also store "
Expand Down Expand Up @@ -72,6 +72,7 @@ def get(key: str, **kwargs) -> str:
"args.search.kind.comp.local": "Search for locally installed versions.",
"args.search.kind.comp.forge": "Search for Forge versions.",
"args.search.kind.comp.fabric": "Search for Fabric versions.",
"args.search.kind.comp.legacyfabric": "Search for LegacyFabric versions.",
"args.search.kind.comp.quilt": "Search for Quilt versions.",
"args.search.input": "Search input.",
"args.search.input.comp.release": "Resolve version of the latest release.",
Expand All @@ -81,12 +82,14 @@ def get(key: str, **kwargs) -> str:
"args.start.version": "Version identifier (default to release): {formats}.",
"args.start.version.standard": "release|snapshot|<vanilla-version>",
"args.start.version.fabric": "fabric:[<vanilla-version>[:<loader-version>]]",
"args.start.version.legacyfabric": "legacyfabric:[<vanilla-version>[:<loader-version>]]",
"args.start.version.quilt": "quilt:[<vanilla-version>[:<loader-version>]]",
"args.start.version.forge": "forge:[<forge-version>] (forge-version >= 1.5.2)",
"args.start.version.neoforge": "neoforge:[<neoforge-version>] (neoforge-version >= 1.20.1)",
"args.start.version.comp.release": "Start the latest release (default).",
"args.start.version.comp.snapshot": "Start the latest snapshot.",
"args.start.version.comp.fabric": "Start Fabric mod loader with latest release.",
"args.start.version.comp.legacyfabric": "Start LegacyFabric mod loader with latest release.",
"args.start.version.comp.quilt": "Start Quilt mod loader with latest release.",
"args.start.version.comp.forge": "Start Forge mod loader with latest release.",
"args.start.version.comp.neoforge": "Start NeoForge mod loader with latest release.",
Expand All @@ -102,6 +105,7 @@ def get(key: str, **kwargs) -> str:
"args.start.no_fix": "Flag that globally disable fixes (proxy for old versions), "
"enabled by default.",
"args.start.fabric_prefix": "Change the prefix of the version ID when starting with Fabric (<prefix>-<vanilla-version>-<loader-version>).",
"args.start.legacyfabric_prefix": "Change the prefix of the version ID when starting with LegacyFabric (<prefix>-<vanilla-version>-<loader-version>).",
"args.start.quilt_prefix": "Change the prefix of the version ID when starting with Quilt (<prefix>-<vanilla-version>-<loader-version>).",
"args.start.forge_prefix": "Change the prefix of the version ID when starting with Forge (<prefix>-<forge-version>).",
"args.start.neoforge_prefix": "Change the prefix of the version ID when starting with NeoForge (<prefix>-<neoforge-version>).",
Expand Down Expand Up @@ -168,6 +172,7 @@ def get(key: str, **kwargs) -> str:
"error.socket": "This operation requires an operational network, but a socket error happened:",
"error.socket.tip.version_manifest": "Version manifest may not be locally cached, try to run this command once with an operational network.",
"error.socket.tip.fabric_loader_version": "Fabric loader version must be specified if network is not operational.",
"error.socket.tip.legacyfabric_loader_version": "Fabric loader version must be specified if network is not operational.",
"error.socket.tip.quilt_loader_version": "Quilt loader version must be specified if network is not operational.",
"error.cert": "Certificate verification failed, you can try installing 'certifi' package:",
# Command search
Expand All @@ -177,6 +182,7 @@ def get(key: str, **kwargs) -> str:
"search.last_modified": "Last modified",
"search.flags": "Flags",
"search.flags.local": "local",
"search.flags.stable": "stable",
"search.loader_version": "Loader version",
# Command login
"login.tip.remember_start_login": "Remember to start the game with '-l {email}' if you want to be authenticated in-game.",
Expand All @@ -186,6 +192,7 @@ def get(key: str, **kwargs) -> str:
"logout.success": "Logged out {email}",
"logout.unknown_session": "No session for {email}",
# Command start
"start.global_version": "Global version: {kind} {version} {remaining}",
"start.version.invalid_id": "Invalid version id, expected: {expected}",
"start.version.invalid_id_unknown_kind": "Invalid version id, unknown kind: {kind}.",
"start.version.loading": "Loading version {version}... ",
Expand All @@ -194,7 +201,7 @@ def get(key: str, **kwargs) -> str:
"start.version.loaded.fetched": "Loaded version {version} (fetched)",
"start.version.not_found": "Version {version} not found",
"start.version.too_much_parents": "Too much parents while resolving versions.",
"start.features": "Features: {features}",
"start.features": "Features: [{features}]",
"start.jar.found": "Checked version jar",
"start.jar.not_found": "Version jar not found",
"start.assets.resolving": "Checking assets version {index_version}... ",
Expand All @@ -217,7 +224,6 @@ def get(key: str, **kwargs) -> str:
"use --jvm argument to manually set the path to your JVM executable.",
f"start.jvm.not_found_error.{JvmNotFoundError.BUILTIN_INVALID_VERSION}": f"The builtin JVM ({jvm_bin_filename}) is not compatible "
"with selected game version.",
"start.fixes": "Applied the following fixes:",
f"start.fix.{Version.FIX_LEGACY_PROXY}": "Using legacy proxy for online resources: {value}",
f"start.fix.{Version.FIX_LEGACY_MERGE_SORT}": "Using legacy merge sort: {value}",
f"start.fix.{Version.FIX_LEGACY_RESOLUTION}": "Included resolution into game arguments: {value}",
Expand Down
Loading
Loading