Files
Launcher/core_lib/game/argument.py
T

335 lines
10 KiB
Python

import json
import logging
import re
import shlex
from core_lib.common.common import get_platform_info
from core_lib.game.library import get_natives_platform_info
logger = logging.getLogger("Launcher.CoreLib")
def is_argument_allowed(argument: dict | str, allow_features: dict | None = None) -> bool:
"""
Check if argument is allowed or not.
:param argument:
:param allow_features: Should be like {key: value}
:return:
allowed: bool
"""
if isinstance(argument, str):
return True
if allow_features is None:
allow_features = {}
rules = argument.get('rules', {})
_, platform_rule_name, platform_arch_type = get_natives_platform_info()
_, _, platform_version = get_platform_info()
allowed = False
if not rules:
return True
for rule in rules:
if not isinstance(rule, dict):
logger.warning("Ignoring invalid argument rule: {}".format(rule))
continue
rule_action = rule.get("action", None)
os_rule = rule.get("os") or {}
rule_os = os_rule.get("name", None)
feature_rule = rule.get("features") or {}
rule_arch = os_rule.get("arch")
rule_version = os_rule.get("version")
# Check platform
if rule_os is not None and platform_rule_name != rule_os:
continue
# Check architecture
if rule_arch is not None and rule_arch != platform_arch_type:
continue
# Check platform version
if rule_version is not None:
try:
if re.search(
rule_version,
str(platform_version),
) is None:
continue
except re.error as exc:
logger.warning(
"Unsupported argument OS version regex %r: %s",
rule_version,
exc,
)
continue
# Check feature rule
if any(allow_features.get(key, False) != expected for key, expected in feature_rule.items()):
continue
if rule_action == "allow":
allowed = True
elif rule_action == "disallow":
allowed = False
else:
logger.warning(f"Unknown argument rule action {rule_action}\n")
return allowed
def replace_placeholders(argument, placeholders: dict) -> str:
if argument:
for key, value in placeholders.items():
argument = argument.replace("${" + key + "}", "" if value is None else str(value))
return argument
class ArgumentMappings:
def __init__(self, player_name="Player", version_name="unknown",
game_directory=None, assets_root=None, legacy_assets_root=None, assets_index_name=None,
auth_uuid="00000001-0002-0003-0004-000000000005",
auth_access_token="", clientid=None, auth_xuid=None,
user_type=None, user_properties=None, version_type=None, natives_directory=None,
launcher_name=None, launcher_version=None,
classpath=None, classpath_separator=None, library_directory=None):
self.player_name = player_name
self.version_name = version_name
self.game_directory = game_directory
self.assets_root = assets_root
self.game_assets = legacy_assets_root
self.assets_index_name = assets_index_name
self.auth_uuid = auth_uuid
self.auth_access_token = auth_access_token
self.clientid = clientid
self.auth_xuid = auth_xuid
self.user_type = user_type
self.user_properties = user_properties
if isinstance(user_properties, dict):
self.user_properties = json.dumps(
user_properties,
separators=(",", ":"),
)
else:
if user_properties is not None:
logger.warning(f"User properties should be a dictionary.")
self.user_properties = "{}"
self.version_type = version_type
self.natives_directory = natives_directory
self.launcher_name = launcher_name
self.launcher_version = launcher_version
self.classpath = classpath
self.classpath_separator = classpath_separator
self.library_directory = library_directory
def generate_placeholders(self):
return {
"player_name": self.player_name,
"version_name": self.version_name,
"game_directory": self.game_directory,
"assets_root": self.assets_root,
"game_assets": self.game_assets,
"assets_index_name": self.assets_index_name,
"auth_uuid": self.auth_uuid,
"auth_session": self.auth_access_token,
"auth_access_token": self.auth_access_token,
"auth_player_name": self.player_name,
"clientid": self.clientid,
"auth_xuid": self.auth_xuid,
"user_type": self.user_type,
"user_properties": self.user_properties,
"version_type": self.version_type,
"natives_directory": self.natives_directory,
"launcher_name": self.launcher_name,
"launcher_version": self.launcher_version,
"classpath": self.classpath,
"classpath_separator": self.classpath_separator,
"library_directory": self.library_directory
}
def convert_argument_to_multiple_string(argument: str | dict) -> list[str]:
if isinstance(argument, str):
return [argument]
if isinstance(argument, dict):
value = argument.get("value", None)
if isinstance(value, list):
return [str(item) for item in value] # ensure all item are string
elif isinstance(value, str):
return [value]
raise TypeError("Argument must be of type str or dict")
def find_game_and_jvm_args(arguments: dict | str) -> tuple[list, list]:
game_args = []
jvm_args = []
if isinstance(arguments, dict):
game = arguments.get("game", [])
jvm = arguments.get("jvm", [])
if isinstance(game, list):
game_args.extend(game)
elif isinstance(game, str):
game_args.append(game)
if isinstance(jvm, list):
jvm_args.extend(jvm)
elif isinstance(jvm, str):
jvm_args.append(jvm)
if isinstance(arguments, str):
# Convert legacy argument into a list type
game_args = shlex.split(arguments)
return game_args, jvm_args
REMAPPING_ARGUMENTS = {
# For some (I don't know) reason, in 26.2, Mojang include some
"-Djava.library.path=${natives_directory}/java": "-Djava.library.path=${natives_directory}"
}
def hard_remapping_special_arguments(argument: str) -> str:
for arg in REMAPPING_ARGUMENTS:
new_value = REMAPPING_ARGUMENTS[arg]
if arg == argument:
return new_value
return argument
def parse_and_generate_arguments(arguments: list | str, mappings: ArgumentMappings, features: dict | None = None) -> \
list[str]:
if features is None:
features = {}
parsed_arguments = []
if isinstance(arguments, str):
# Convert legacy argument into a list type
arguments = shlex.split(arguments)
placeholders = mappings.generate_placeholders()
for argument in arguments:
# Check rules
if not is_argument_allowed(argument, allow_features=features):
continue
# Replace some special arguments
new_argument = hard_remapping_special_arguments(argument)
# Each argument (section) may contain multiple items, convert it to list before replace placeholder
converted = convert_argument_to_multiple_string(new_argument)
for item in converted:
parsed = replace_placeholders(item, placeholders)
parsed_arguments.append(parsed)
return parsed_arguments
def append_missing_necessary_jvm_arguments(arguments: list, arg_maps: ArgumentMappings) -> list[str]:
result = []
has_classpath = any(
argument in ("-cp", "-classpath", "--class-path")
for argument in arguments
)
has_native_path = any(
argument.startswith("-Djava.library.path=")
for argument in arguments
)
if not has_native_path:
result.append(f"-Djava.library.path={arg_maps.natives_directory}")
if not has_classpath:
result.extend(["-cp", arg_maps.classpath])
result.extend(arguments)
return result
def generate_launch_command(arg_maps: ArgumentMappings, main_class: str, java_path: str, full_args: dict | str
, features: dict | None = None) -> list[str]:
cmd = [java_path]
game_args, jvm_args = find_game_and_jvm_args(full_args)
parsed_jvm_args = parse_and_generate_arguments(
jvm_args,
arg_maps,
features=features
)
parsed_game_args = parse_and_generate_arguments(
game_args,
arg_maps,
features=features
)
# Legacy Minecraft don't have jvm arguments exist in the version library. We need to generate by ourselves
parsed_jvm_args = append_missing_necessary_jvm_arguments(parsed_jvm_args, arg_maps)
cmd.extend(parsed_jvm_args)
cmd.append(main_class)
cmd.extend(parsed_game_args)
return cmd
def is_a_argument(arg: str, startswith="--"):
pattern = re.compile(rf"{startswith}(.+)")
return pattern.match(arg) is not None
def generate_argument_details(arguments: list[str | dict], startswith="--") -> list[dict]:
items = []
for index, arg in enumerate(arguments):
if isinstance(arg, str) and is_a_argument(arg, startswith):
items.append({
"value": arg,
"type": "argument",
"dataType": "string",
})
if index > 0:
previous = items[index - 1]
if previous["type"] == "argument":
previous["isFlag"] = True
elif isinstance(arg, str):
items.append({
"value": arg,
"type": "value",
})
elif isinstance(arg, dict):
value = arg.get("value", None)
if not value:
raise ValueError("Dictionary argument must contain a value")
items.append({
"value": value,
"type": "argument",
"dataType": "dictionary",
"dictValue": arg,
})
return items
def is_same_argument(item: dict, another: dict) -> bool:
pass