This page documents the public API of the launcher. This launcher library provides high flexibility for launching Minecraft in many environments.
Documented version: 4.0.0
.
- File structure
- Standard version
- Fabric/Quilt version
- Forge version
- Authentication
- Versioning and stability
Sources of the launcher's API are stored in the src/portablemc
directory. Files are described in the following tree list:
__init__.py
, root of the library, contains launcher name, version, authors, copyright and URL;download.py
, optimized parallel download classes and tasks;http.py
, collection of simple functions to make simple HTTP API requests with better response/error classes;util.py
, global misc utilities without particular classification;auth.py
, base classes for authentication;standard.py
, base classes required to launch standard versions provided by Mojang;fabric.py
: fabric/quilt mod loaders support;forge.py
: forge mod loader support.
The standard version is the most basic type of version, it allows you to start any Mojang version, install it and run it with simple functions. The game's installation finds missing resources and download them in parallel, allowing fast installation and low-latency startup for already installed versions.
Following piece of code is the simplest method to install and run the game with default parameters (latest release):
from portablemc.standard import Version
Version().install().run()
To understand the different steps involved, let's rewrite the previous code with explanatory comments:
# Create a version installer for latest release
version = Version()
# Ensure that version is installed, returning the
# game's environment needed to run it.
env = version.install()
# Start the game and wait for it to close.
env.run()
This example uses default parameters, but you can specify a lot of parameters to run the game, starting with the version to run:
version = Version("1.20.1") # A particular release
version = Version("23w18a") # A particular snapshot
version = Version("release") # Latest release
version = Version("snapshot") # Latest snapshot
Another thing that can be specified is the context of the game, the context describe
which directories to use for various resources, such as jvm, libraries, assets or
versions. The two main kind of directories are "main directory" and "work directory",
the main directory stores resources as described previously, and work directory stores
user's personal files such as worlds, resource packs and options. By default, both
are set to the standard .minecraft
.
The context can be specified at version's instantiation:
from portablemc.standard import Context, Version
from pathlib import Path
# Here's an example where main directory is '/opt/minecraft'
# and work dir is '~/.minecraft'.
context = Context(Path("/opt/minecraft"), Path.home() / ".minecraft")
# Use this context for our version.
version = Version(context=context)
You can watch events produced by the installation by providing a watcher to the install
method, a watcher must be a subclass of the Watcher
class:
class MyWatcher(Watcher):
def handle(self, event) -> None:
print("install event", event)
version.install(watcher=MyWatcher())
Events are differentiated by their class, these classes has the suffix Event
and are
available in the same standard
module, for example VersionLoadingEvent
.
Documentation of each event can help you understand how they are triggered.
The version class allows for various fixes to be applied on various game's parts, in
order to fix well-known issues with old versions. Fixes are string constant defined in
the Version
class prefixed with FIX_
, the following fixes are supported:
FIX_LEGACY_PROXY
, add HTTP proxies to old versions' arguments in order to adapt old request to newer Mojang's APIs, such as skins. Possible thanks to betacraft.FIX_LEGACY_MERGE_SORT
, add a JVM flag for fixing crashes on old beta/alpha versions.FIX_LEGACY_RESOLUTION
, old versions doesn't support default window resolution by default in their metadata, in such case this fix adds resolution arguments if relevant.FIX_LEGACY_QUICK_PLAY
, old versions doesn't support quick play arguments in their metadata, this fix adds the proper arguments to automatically connect. This only works with multiplayer server quick play, for now.FIX_1_16_AUTH_LIB
, versions 1.16.4 and 1.16.5 of Minecraft use Mojang's authlib version2.1.28
which cause multiplayer to be disabled in offline mode, this can be fixed by replacing it with the next version2.2.30
.FIX_LWJGL
, unlike previous fixes, this one takes a particular LWJGL version, it will force the game to use the given LWJGL version. This can be used to support ARM devices which are not supported by default by Mojang. Supported versions are3.2.3
,3.3.0
,3.3.1
and3.3.2
, specifying other version will raise an exception.
All fixes except LWJGL are enabled by default (good defaults!), you can enable or disable fixes like this:
# Disable fix of authlib 2.1.28
version.fixes[Version.FIX_AUTH_LIB_2_1_28] = False
# Enable force use of LWJGL 3.3.2
version.fixes[Version.FIX_LWJGL] = "3.3.2"
You can gather the applied fixes in the environment returned by the install
method:
env = version.install()
print(env.fixes[Version.FIX_AUTH_LIB_2_1_28]) # True
print(env.fixes[Version.FIX_LWJGL]) # 3.3.2
Various options are available on version's instances before starting the installation:
demo
, enable demo mode (may not be supported on all versions).auth_session
, the authentication session to use to be authenticated in-game:set_auth_offline(username, uuid)
, this function is a shortcut to setup an offline authentication mode for standard the game.
resolution
, optional initial resolution for the game's window.disable_multiplayer
, force disable multiplayer.disable_chat
, force disable chat.quick_play
, optional quick play mode for starting the game:set_quick_play_singleplayer(level_name)
, shortcut function to setup a singleplayer quick play to a given level name;set_quick_play_multiplayer(host, port = 25565)
, shortcut function to setup a server multiplayer quick play given a host and optional port;set_quick_play_realms(realm)
, shortcut function to setup a realm multiplayer quick play to a given realm identifier.
jvm_path
, optional JVM executable path to force this to be used for running the game.
The environment returned by Version.install()
method can be modified before running the
game, it's an instance of Environment
and contains the following attributes:
jvm_args
, the list of arguments passed to the JVM, the first argument is the JVM executable path. You can use it to add custom JVM arguments for memory, like-Xmx
.game_args
, the list of arguments passed to Minecraft.main_class
, the main class started by the JVM, usually something likenet.minecraft.client.main.Main
.args_replacements
, a mapping of variables names to values, these will be used to replace variables of the form${my_var}
in bothjvm_args
andgame_args
.native_libs
, a list of native libraries or shared objects that are copied (or symlinked if relevant) in the game's temporary bin directory.fixes
, this one as no effect for the game's runtime, it's just a summary of fixes applied during the game's installation, that affected the environment.
The Environment.run
method we've previously seen to run the game accepts an argument
named runner
. This argument provides a Runner
instance, which provides an abstract
method that should run the given environment.
If not provided, this argument defaults to an instance of StandardRunner
, this class
provides default logic for starting the game's java process and how to wait for its
termination (it also provides universal Ctrl-C support to kill the game).
Another subclass is provided, named StreamRunner
, it extends StandardRunner
by
embedding a game's output parser, which support both raw logs and Log4j's XML logs.
You're free to subclass it and implement the process_stream_event
method to listen
for incoming log lines, here's an example on how to do it:
from portablemc.standard import StreamRunner, XmlStreamEvent
class MyRunner(StreamRunner):
def process_stream_event(self, event: Any) -> None:
if isinstance(event, XmlStreamEvent):
print(f"xml log: {repr(event)}")
else:
print(f"raw log: {event}")
env = ...
env.run(MyRunner())
The fabric mod loader is supported by default, using the FabricVersion
class. Both
Fabric and Quilt are supported by this class because those mod loaders have the same
install process and API endpoints:
from portablemc.fabric import FabricVersion
fabric_version = FabricVersion.with_fabric()
quilt_version = FabricVersion.with_quilt()
These versions can be installed and run like standard ones, and much like standard versions you can specify which version to run (the same applies for both fabric and quilt):
# Fabric for Minecraft 1.20.1, with latest fabric loader
version = FabricVersion.with_fabric("1.20.1")
# Fabric for Minecraft 1.20.1, with fabric loader v0.14.21
version = FabricVersion.with_fabric("1.20.1", "0.14.21")
# Context can be specified like standard version
version = FabricVersion.with_fabric(context=my_context)
The real name of a fabric or quilt version (as stored inside the versions directory)
will be by default fabric|quilt-<vanilla_version>-<loader_version>
. You can change
the prefix using the prefix
argument of the constructors:
# Using format: my-fabric-<vanilla_version>-<loader_version>
version = FabricVersion.with_fabric(prefix="my-fabric")
The forge mod loader is supported by default, using the ForgeVersion
:
from portablemc.forge import ForgeVersion
# Forge for latest version
version = ForgeVersion()
# Forge for Minecraft 1.20.1, recommended (both equivalent)
version = ForgeVersion("1.20.1")
version = ForgeVersion("1.20.1-recommended")
# Forge for Minecraft 1.20.1, latest
version = ForgeVersion("1.20.1-latest")
# Forge for Minecraft 1.20.1, explicit version
version = ForgeVersion("1.20.1-47.1.0")
Same as fabric, this version type is a subclass of standard version and therefore can be installed and run in the same way. Note however that because of the huge complexity of supporting a wide variety of forge installer versions (all forge versions with an installer are supported), this version's installation is more complex and might be longer due to "processors" (executables parts of the forge's installer that build the version).
The real name of a forge version (as stored inside the versions directory)
will be by default forge-<forge_version>
You can change the prefix using the prefix
argument of the constructors:
# Using format: my-forge-<forge_version>
version = ForgeVersion(prefix="my-forge")
The authentication API is all provided in the portablemc.auth
module, it supports both
Mojang (named Yggdrasil) and Microsoft (actually Microsoft/XBox Live/Minecraft)
authentication. It also provide an offline authentication session, this may seem a bit
contradictory but it's used by default to generate username and UUIDs when needed.
These authentication services are implemented by sub classing the AuthSession
class,
in YggdrasilAuthSession
, MicrosoftAuthSession
and OfflineAuthSession
.
These various services may raise errors of type AuthError
(and its subclass) in case
of wrong credentials or unexpected states.
The Yggdrasil authentication is simple (but phased out by Mojang!), to connect to such
account you can use YggdrasilAuthSession.authenticate
class method:
from portablemc.auth import YggdrasilAuthSession
# Used for uniquely naming the client, in the CLI it's a random UUID
client_id = "foobar"
email = "[email protected]"
password = ...
version.auth_session = YggdrasilAuthSession.authenticate(client_id, email, password)
The Microsoft authentication is complicated because it requires an interactive login
through Microsoft's login web page, it also requires you to have registered an Azure App
(I know that people are likely to do that in the end, so here is the Azuze App ID of the
CLI: 708e91b5-99f8-4a1d-80ec-e746cbb24771
, note that it's a bad idea for automation
because it might use the wrong redirect URIs not suited for your case).
So the first step it to register an Azure App, as far as I know it's free of charge. Go on https://portal.azure.com/, then click on "Azure Active Directory" and then (in left pane) "App Registrations". Then you should click on "New registration", you can choose the app name, select supported account type, and to finish the redirect URI. This URI is where the login page will redirect, with query parameters containing tokens.
Once you have the app id, use MicrosoftAuthSession.get_authentication_url
to get the
URL of the login page and follow this procedure:
from portablemc.auth import MicrosoftAuthSession
import urllib.parse
client_id = "foobar"
app_id = ...
code_redirect_uri = "http://localhost:7969/code" # URI of your choice
email = "[email protected]"
nonce = ... # random string
# Go to the displayed page...
print(MicrosoftAuthSession.get_authentication_url(app_id, code_redirect_uri, email, nonce))
# After successful login, you are redirected to your 'code_redirect_uri' with arguments
# given as 'application/x-www-form-urlencoded' format in the request's body:
# - Successful login: code=foo&id_token=bar
# - Failed login: error=foo&error_description=bar
# To parse such response, you can use the following line:
qs = urllib.parse.parse_qs(request_body)
# If successful
print(qs["id_token"][0], qs["code"][0])
# If failed
print(qs["error"][0], qs["error_description"][0])
# If successful, you can send the user to the logout URL. This URL will not invalide your
# token but will reset the login page so it can be used again for another account:
exit_redirect_uri = "http://localhost:7969/exit" # URI of your choice
print(MicrosoftAuthSession.get_logout_url(app_id, exit_redirect_uri))
# To continue, you should keep both 'id_token' and 'code'.
# At this point, you may want to check response data, using 'get_authentication_url':
if not MicrosoftAuthSession.check_token_id(id_token, email, nonce):
print("incoherent data")
# And the final step is actually logging in Mojang's account:
version.auth_session = MicrosoftAuthSession.authenticate(client_id, app_id, code, code_redirect_uri)
This launcher uses semantic versioning, the version is defined by
three numbers: major, minor and patch with the format <major>.<minor>.<patch>
. Release
candidates are suffixed with rc<num>
. Semantic versioning defines three cases when
incrementing the version's numbers:
- Major number is incremented when API change, breaking older one;
- Minor number is incremented when API change without breaking older one;
- Patch number is incremented when API doesn't change.
The API is defined by all symbols publicly available to users of the PortableMC library, it is composed of all symbols (classes, variables, functions), modules and arguments that are not prefixed by one or more underscore "_", this is a common convention in Python. Note that only keyword arguments can be prefixed with an underscore, which means that the arguments is made for internal use or is experimental.
Because of the dynamic design of Python, it's unstable (may break at any time) to
externally inject symbols to the portablemc's modules and classes, because the API might
define such symbol in the future. For example, a subclass of an API's class may break at
any time if defining methods or attributes, because those might be defined on superclasses
in any future version. It is also unstable to override a method, because arguments may be
added in any future versions and break your override (you can use *args
and **kwargs
to make it stable).
Many symbols are private, this allows greater flexibility for developing the launcher, but if you think that some members should be made public, you can open an issue.
Here are examples of stable and unstable usage of the library:
# Import API symbol: stable
from portablemc.standard import Version, Watcher
# Import a private symbol: unstable
from portablemc.download import _DownloadEntry
version = Version()
# Access API attribute: stable
version.demo = True
# Access private attribute: unstable
version._assets_index_version = "..."
# Inject a new attribute: unstable
version.foo = 42
# Subclass of an API's class is stable, following important details...
class MyVersion(Version):
# It's *stable* to override using variadic arguments, because
# this adapts to future function signatures.
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# It's *unstable* to define your own argument, because the
# same attribute may be defined in the future on Version class
self.bar = "..."
# You can reduce the risk by using uncommon prefixes.
self._my_version_bar = "..."
# Or by using name mangling built in Python (at your own stability risk!).
self.__bar = "..."
# It's *unstable* to override with parent signature, because
# this doesn't adapt for future function signatures.
def set_auth_offline(self, username: Optional[str], uuid: Optional[str]) -> None:
super().set_auth_offline(username, uuid)
# Same as above, but *stable* because future signatures are handled.
def set_auth_offline(self, username: Optional[str], uuid: Optional[str], *args, **kwargs) -> None:
super().set_auth_offline(username, uuid, *args, **kwargs)
# It's *unstable* to define your own methods because the same
# name might get defined in the future class' signature.
def baz(self):
pass
# Example of stable subclass of the standard Watcher class
class MyWatcher(Watcher):
def handle(self, event: Any, *args, **kwargs) -> None:
print(f"here's an event: {event}")