Skip to content

Commit

Permalink
Merge pull request #55 from mindstorm38/addon/forge
Browse files Browse the repository at this point in the history
Addon/forge
  • Loading branch information
mindstorm38 authored Dec 22, 2021
2 parents e48d999 + be9adc8 commit 603483b
Show file tree
Hide file tree
Showing 37 changed files with 926 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/.idea
.idea
__pycache__
/packages
/dist
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ following issue for a temporary fix [#52](https://github.com/mindstorm38/portabl
- [Addon sub-command](#addon-sub-command)
- [Addons](#addons)
- [Fabric support](#fabric-support)
- [Forge support ⇗](/addons/forge/README.md)
- [Better console](#better-console)
- [Archives support](#archives-support)
- [Modrinth mod management](#modrinth-mod-management-wip)
Expand Down
25 changes: 25 additions & 0 deletions addons/forge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Forge add-on
The forge add-on allows you to install and run Minecraft with forge mod loader in a single command
line!

### Usage
This add-on extends the syntax accepted by the [start](/README.md#start-the-game) sub-command, by
prepending the version with `forge:`. Almost all releases are supported by forge, the latest
releases are often supported, if not please refer to forge website. You can also append either
`-recommended` or `-latest` to the version to take the corresponding version according to the
forge public information, this is reflecting the "Download Latest" and "Download Recommended" on
the forge website. You can also use version aliases like `release` or equivalent empty version
(just `forge:`). You can also give the exact forge version like `1.18.1-39.0.7`, in such cases,
no HTTP request is made if the version is already installed.

### Examples
```sh
portablemc start forge: # Install recommended forge version for latest release
portablemc start forge:release # Same as above
portablemc start forge:1.18.1 # Install recommended forge for 1.18.1
portablemc start forge:1.18.1-39.0.7 # Install the exact forge version 1.18.1-39.0.7
```

### Credits
- [Forge Website](https://files.minecraftforge.net/net/minecraftforge/forge/)
- Consider supporting [LexManos](https://www.patreon.com/LexManos/)
231 changes: 231 additions & 0 deletions addons/forge/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
from argparse import ArgumentParser, Namespace
from typing import Dict, List
from os import path
import subprocess
import sys
import os

from portablemc import Version, \
DownloadList, DownloadEntry, DownloadError, \
BaseError, \
http_request, json_simple_request, cli as pmc

from portablemc.cli import CliContext


def load(_pmc):

# Private mixins

@pmc.mixin()
def register_start_arguments(old, parser: ArgumentParser):
_ = pmc.get_message
parser.add_argument("--forge-prefix", help=_("args.start.forge_prefix"), default="forge", metavar="PREFIX")
old(parser)

@pmc.mixin()
def cmd_start(old, ns: Namespace, ctx: CliContext):
try:
return old(ns, ctx)
except ForgeInvalidMainDirectory:
pmc.print_task("FAILED", "start.forge.error.invalid_main_dir", done=True)
sys.exit(pmc.EXIT_FAILURE)
except ForgeInstallerFailed as err:
pmc.print_task("FAILED", f"start.forge.error.installer_{err.return_code}", done=True)
sys.exit(pmc.EXIT_FAILURE)
except ForgeVersionNotFound as err:
pmc.print_task("FAILED", f"start.forge.error.{err.code}", {"version": err.version}, done=True)
sys.exit(pmc.EXIT_VERSION_NOT_FOUND)

@pmc.mixin()
def new_version(old, ctx: CliContext, version_id: str) -> Version:

if version_id.startswith("forge:"):

main_dir = path.dirname(ctx.versions_dir)
if main_dir != path.dirname(ctx.libraries_dir):
raise ForgeInvalidMainDirectory()

game_version = version_id[6:]
if not len(game_version):
game_version = "release"

manifest = pmc.load_version_manifest(ctx)
game_version, game_version_alias = manifest.filter_latest(game_version)

forge_version = None

# If the version is an alias, we know that the version needs to be resolved from the forge
# promotion metadata. It's also the case if the version ends with '-recommended' or '-latest',
# or if the version doesn't contains a "-".
if game_version_alias or game_version.endswith(("-recommended", "-latest")) or "-" not in game_version:
promo_versions = request_promo_versions()
for suffix in ("", "-recommended", "-latest"):
tmp_forge_version = promo_versions.get(f"{game_version}{suffix}")
if tmp_forge_version is not None:
if game_version.endswith("-recommended"):
game_version = game_version[:-12]
elif game_version.endswith("-latest"):
game_version = game_version[:-7]
forge_version = f"{game_version}-{tmp_forge_version}"
break

if forge_version is None:
# Test if the user has given the full forge version
forge_version = game_version

version_id = f"{ctx.ns.forge_prefix}-{forge_version}"
version_dir = ctx.get_version_dir(version_id)
version = Version(ctx, version_id)

# Extract minecraft version from the full forge version
mc_version_id = forge_version[:max(0, forge_version.find("-"))]

# List of possible artifacts names-version, some versions (e.g. 1.7) have the minecraft
# version in suffix of the version in addition to the suffix.
possible_artifact_versions = [forge_version, f"{forge_version}-{mc_version_id}"]

# Check if Forge should be installed, based on version meta file and potentially missing forge lib.
version_meta_file = path.join(version_dir, f"{version_id}.json")
should_install = not path.isfile(version_meta_file)
if not should_install:
should_install = True
local_artifact_path = path.join(ctx.libraries_dir, "net", "minecraftforge", "forge")
for possible_version in possible_artifact_versions:
for possible_classifier in (possible_version, f"{possible_version}-client"):
artifact_jar = path.join(local_artifact_path, possible_version, f"forge-{possible_classifier}.jar")
if path.isfile(artifact_jar):
should_install = False
break

if should_install:

# 1.7 used to have an additional suffix with minecraft version.
installer_file = path.join(version_dir, "installer.jar")

pmc.print_task("", "start.forge.installer.resolving", {"version": forge_version})

found_installer = False
dl_list = DownloadList()
for possible_version in possible_artifact_versions:
try:
installer_url = f"https://maven.minecraftforge.net/net/minecraftforge/forge/{possible_version}/forge-{possible_version}-installer.jar"
dl_list.reset()
dl_list.append(DownloadEntry(installer_url, installer_file))
dl_list.download_files()
pmc.print_task("OK", "start.forge.installer.found", {"version": forge_version}, done=True)
found_installer = True
break
except DownloadError:
pass

if not found_installer:
raise ForgeVersionNotFound(ForgeVersionNotFound.INSTALLER_NOT_FOUND, forge_version)

# We ensure that the parent Minecraft version JAR and metadata are
# downloaded because it's needed by installers.
if len(mc_version_id):
try:
pmc.print_task("", "start.forge.vanilla.resolving", {"version": mc_version_id})
mc_version = Version(ctx, mc_version_id)
mc_version.prepare_meta()
mc_version.prepare_jar()
# mc_version.download() # Use pretty download??
pmc.pretty_download(mc_version.dl)
pmc.print_task("OK", "start.forge.vanilla.found", {"version": mc_version_id}, done=True)
except DownloadError:
raise ForgeVersionNotFound(ForgeVersionNotFound.MINECRAFT_VERSION_NOT_FOUND, mc_version_id)

pmc.print_task("", "start.forge.wrapper.running")
wrapper_jar_file = path.join(path.dirname(__file__), "wrapper", "target", "wrapper.jar")
wrapper_completed = subprocess.run([
"java",
"-cp", path.pathsep.join([wrapper_jar_file, installer_file]),
"portablemc.wrapper.Main",
main_dir,
version_id
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
os.remove(installer_file)
pmc.print_task("OK", "start.forge.wrapper.done", done=True)

if wrapper_completed.returncode != 0:
raise ForgeInstallerFailed(wrapper_completed.returncode)

pmc.print_task("INFO", "start.forge.consider_support", done=True)

return version

return old(ctx, version_id)

# Messages

pmc.messages.update({
"args.start.forge_prefix": "Change the prefix of the version ID when starting with Forge.",
"start.forge.installer.resolving": "Resolving forge {version}...",
"start.forge.installer.found": "Found installer for forge {version}.",
"start.forge.vanilla.resolving": "Preparing parent Minecraft version {version}...",
"start.forge.vanilla.found": "Found parent Minecraft version {version}.",
"start.forge.wrapper.running": "Running installer (can take few minutes)...",
"start.forge.wrapper.done": "Forge installation done.",
"start.forge.consider_support": "Consider supporting the forge project through https://www.patreon.com/LexManos/.",
"start.forge.error.invalid_main_dir": "The main directory cannot be determined, because version directory "
"and libraries directory must have the same parent directory.",
"start.forge.error.installer_3": "This forge installer is currently not supported.",
"start.forge.error.installer_4": "This forge installer is missing something to run (internal).",
"start.forge.error.installer_5": "This forge installer failed to install forge (internal).",
f"start.forge.error.{ForgeVersionNotFound.INSTALLER_NOT_FOUND}": "No installer found for forge {version}.",
f"start.forge.error.{ForgeVersionNotFound.MINECRAFT_VERSION_NOT_FOUND}": "Parent Minecraft version not found "
"{version}.",
})


# Forge API

def request_promo_versions() -> Dict[str, str]:
raw = json_simple_request("https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json")
return raw["promos"]


def request_maven_versions() -> List[str]:

status, raw = http_request("https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml", "GET", headers={
"Accept": "application/xml"
})

text = raw.decode()

versions = []
last_idx = 0

while True:
start_idx = text.find("<version>", last_idx)
if start_idx == -1:
break
end_idx = text.find("</version>", start_idx + 9)
if end_idx == -1:
break
versions.append(text[(start_idx + 9):end_idx])
last_idx = end_idx + 10

return versions


# Errors

class ForgeInvalidMainDirectory(Exception):
pass


class ForgeInstallerFailed(Exception):
def __init__(self, return_code: int):
self.return_code = return_code


class ForgeVersionNotFound(BaseError):

INSTALLER_NOT_FOUND = "installer_not_found"
MINECRAFT_VERSION_NOT_FOUND = "minecraft_version_not_found"

def __init__(self, code: str, version: str):
super().__init__(code)
self.version = version
6 changes: 6 additions & 0 deletions addons/forge/addon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "Forge",
"version": "1.0.0",
"authors": ["Théo Rozier"],
"description": "Start Minecraft using the Forge mod loader using '<exec> start forge:[<mc-version>]'."
}
4 changes: 4 additions & 0 deletions addons/forge/wrapper/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/target/classes
/target/generated-sources
/target/maven-archiver
/target/maven-status
38 changes: 38 additions & 0 deletions addons/forge/wrapper/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>portablemc</groupId>
<artifactId>wrapper</artifactId>
<version>1.0.0</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<finalName>wrapper</finalName>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>portablemc.wrapper.Main</mainClass>
</manifest>
</archive>
<includes>
<include>portablemc/**</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package argo.jdom;

abstract class AbstractJsonObject extends JsonRootNode {

}
21 changes: 21 additions & 0 deletions addons/forge/wrapper/src/main/java/argo/jdom/JsonField.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package argo.jdom;

public class JsonField {

private final JsonStringNode name;
private final JsonNode value;

public JsonField(final JsonStringNode name, final JsonNode value) {
this.name = name;
this.value = value;
}

public JsonStringNode getName() {
return this.name;
}

public JsonNode getValue() {
return this.value;
}

}
11 changes: 11 additions & 0 deletions addons/forge/wrapper/src/main/java/argo/jdom/JsonNode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package argo.jdom;

import java.util.List;

public abstract class JsonNode {

public abstract String getText();

public abstract List<JsonField> getFieldList();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package argo.jdom;

import java.util.Map;

public class JsonNodeFactories {

public static JsonStringNode string(final String value) {
return null;
}

public static JsonRootNode object(final Map<JsonStringNode, ? extends JsonNode> fields) {
return null;
}

}
17 changes: 17 additions & 0 deletions addons/forge/wrapper/src/main/java/argo/jdom/JsonObject.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package argo.jdom;

import java.util.List;

final class JsonObject extends AbstractJsonObject {

@Override
public String getText() {
return null;
}

@Override
public List<JsonField> getFieldList() {
return null;
}

}
Loading

0 comments on commit 603483b

Please sign in to comment.