Skip to content

Commit b11a2c2

Browse files
committed
Merge branch '1.21.4' into 1.21.11
2 parents acb381d + fc135ab commit b11a2c2

File tree

13 files changed

+447
-40
lines changed

13 files changed

+447
-40
lines changed

.github/workflows/plugin-test.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: 1.21.4 Plugin Test
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
tags:
7+
- '[0-9]+.[0-9]+.[0-9]+\+1.21.11.pre'
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Check out repository
14+
uses: actions/checkout@v6
15+
with:
16+
persist-credentials: false
17+
18+
- name: Setup JDK
19+
uses: actions/setup-java@v5
20+
with:
21+
java-version: '25'
22+
distribution: 'corretto'
23+
24+
- name: Elevate wrapper permissions
25+
run: chmod +x ./gradlew
26+
27+
- name: Setup Gradle
28+
uses: gradle/actions/setup-gradle@v5
29+
with:
30+
dependency-graph: generate-and-submit
31+
cache-read-only: ${{ github.event_name == 'pull_request' }}
32+
33+
- name: Build ZenithProxy
34+
run: ./gradlew build
35+
36+
- name: Setup Plugin Load Test
37+
run: |
38+
mkdir -p run/plugins
39+
wget https://github.com/rfresh2/ZenithProxyWebAPI/releases/download/1.0.5/ZenithProxyWebAPI-1.0.5.jar -O run/plugins/ZenithProxyWebAPI.jar
40+
wget https://github.com/rfresh2/ZenithProxyChatControl/releases/download/1.0.5/ZenithProxyChatControl-1.0.5.jar -O run/plugins/ZenithProxyChatControl.jar
41+
wget https://github.com/rfresh2/ZenithProxySparkPlugin/releases/download/1.0.3/ZenithProxySparkPlugin-1.0.3.jar -O run/plugins/ZenithProxySparkPlugin.jar
42+
43+
- name: Run Plugin Load Test
44+
run: ./gradlew pluginLoadTest
45+
46+
- name: Upload Java Artifact
47+
uses: actions/upload-artifact@v6
48+
with:
49+
name: ZenithProxy-java
50+
path: build/libs/ZenithProxy.jar
51+
if-no-files-found: error

build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ tasks {
152152
environment("ZENITH_DEV", "true")
153153
outputs.file(outputFile)
154154
}
155+
val pluginLoadTestTask = register("pluginLoadTest", PluginLoadTestTask::class.java) {
156+
group = "verification"
157+
description = "Tests that plugins are able to load"
158+
javaLauncher = javaLauncherProvider
159+
workingDir = layout.projectDirectory.dir("run").asFile
160+
classpath = sourceSets.main.get().runtimeClasspath
161+
mainClass.set("com.zenith.Proxy")
162+
}
155163
val updateWikiTask = register<UpdateWikiTask>("updateWiki") {
156164
inputs.files(generateCommandDocsTask.get().outputs.files)
157165
wikiDirectory = layout.projectDirectory.dir("docs/wiki").asFile
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import org.gradle.api.DefaultTask
2+
import org.gradle.api.GradleException
3+
import org.gradle.api.file.ConfigurableFileCollection
4+
import org.gradle.api.file.DirectoryProperty
5+
import org.gradle.api.file.ProjectLayout
6+
import org.gradle.api.file.RegularFileProperty
7+
import org.gradle.api.provider.ListProperty
8+
import org.gradle.api.provider.Property
9+
import org.gradle.api.tasks.Input
10+
import org.gradle.api.tasks.InputDirectory
11+
import org.gradle.api.tasks.InputFile
12+
import org.gradle.api.tasks.InputFiles
13+
import org.gradle.api.tasks.JavaExec
14+
import org.gradle.api.tasks.Nested
15+
import org.gradle.api.tasks.TaskAction
16+
import org.gradle.jvm.toolchain.JavaLauncher
17+
import java.io.BufferedOutputStream
18+
import javax.inject.Inject
19+
import java.io.BufferedReader
20+
import java.io.ByteArrayOutputStream
21+
import java.io.File
22+
import java.io.InputStream
23+
import java.io.InputStreamReader
24+
import java.io.PipedInputStream
25+
import java.io.PipedOutputStream
26+
import java.time.Duration
27+
import java.util.concurrent.TimeUnit
28+
import java.util.concurrent.atomic.AtomicBoolean
29+
30+
abstract class PluginLoadTestTask : DefaultTask() {
31+
32+
@get:Nested
33+
abstract val javaLauncher: Property<JavaLauncher>
34+
35+
@get:InputFiles
36+
abstract val classpath: ConfigurableFileCollection
37+
38+
@get:Input
39+
abstract val mainClass: Property<String>
40+
41+
@get:Input
42+
abstract val jvmArgs: ListProperty<String>
43+
44+
@get:InputDirectory
45+
abstract val workingDir: RegularFileProperty
46+
47+
@get:Inject
48+
abstract val layout: ProjectLayout
49+
50+
init {
51+
group = "verification"
52+
outputs.upToDateWhen { false }
53+
}
54+
55+
@TaskAction
56+
fun exec() {
57+
// Build the command to launch the application similarly to JavaExec
58+
val javaPath = javaLauncher.get().executablePath.asFile.absolutePath
59+
val cp = checkNotNull(classpath) { "Classpath must be configured" }.asPath
60+
val main = checkNotNull(mainClass.orNull) { "mainClass must be configured" }
61+
62+
val jvm = (jvmArgs.get() ?: emptyList()).map { it.toString() }
63+
64+
val command = mutableListOf<String>().apply {
65+
add(javaPath)
66+
addAll(jvm)
67+
add("-cp")
68+
add(cp)
69+
add(main)
70+
}
71+
72+
val pb = ProcessBuilder(command)
73+
.directory(workingDir.asFile.get() ?: layout.projectDirectory.asFile)
74+
.redirectErrorStream(false) // Capture stdout only, as requested
75+
76+
logger.lifecycle("[pluginLoadTest] Starting application: ${command.joinToString(" ")}")
77+
78+
val process = try {
79+
pb.start()
80+
} catch (e: Exception) {
81+
throw GradleException("Failed to start process for PluginLoadTest", e)
82+
}
83+
84+
val startedMarker = "ZenithProxy started!"
85+
val failureMarker = "Plugin Load Failure"
86+
val successMarker = "Plugin Loaded"
87+
88+
val capturedLines = mutableListOf<String>()
89+
val started = AtomicBoolean(false)
90+
91+
val reader = BufferedReader(InputStreamReader(process.inputStream))
92+
val readerThread = Thread {
93+
try {
94+
var line: String?
95+
while (reader.readLine().also { line = it } != null) {
96+
val l = line!!
97+
capturedLines.add(l)
98+
logger.lifecycle(l)
99+
if (!started.get() && l.contains(startedMarker)) {
100+
started.set(true)
101+
}
102+
}
103+
} catch (_: Exception) {
104+
// ignore reader exceptions when process is destroyed
105+
} finally {
106+
try { reader.close() } catch (_: Exception) {}
107+
}
108+
}
109+
readerThread.isDaemon = true
110+
readerThread.start()
111+
112+
// Wait for the started marker with a timeout
113+
val timeout = Duration.ofMinutes(1)
114+
val waitUntil = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout.toMillis())
115+
while (!started.get() && System.nanoTime() < waitUntil) {
116+
try { Thread.sleep(100) } catch (_: InterruptedException) { break }
117+
}
118+
119+
val sawStarted = started.get()
120+
121+
// Stop the process regardless
122+
try {
123+
process.destroy()
124+
if (process.isAlive) process.destroyForcibly()
125+
} catch (_: Exception) { }
126+
127+
try { readerThread.join(5000) } catch (_: InterruptedException) { }
128+
129+
if (!sawStarted) {
130+
throw GradleException("Timed out waiting for '$startedMarker' in application output")
131+
}
132+
133+
// Analyze output prior to the start marker
134+
val startIndex = capturedLines.indexOfFirst { it.contains(startedMarker) }
135+
val linesBeforeStart = if (startIndex >= 0) capturedLines.subList(0, startIndex) else capturedLines
136+
137+
val failures = linesBeforeStart.count { it.contains(failureMarker) }
138+
val successes = linesBeforeStart.count { it.contains(successMarker) }
139+
140+
logger.lifecycle("[pluginLoadTest] Detected $successes successful plugin load(s) and $failures failure(s) before start")
141+
142+
if (failures > 0) {
143+
throw GradleException("PluginLoadTest failed: detected $failures plugin load failure(s). See output above for details.")
144+
}
145+
146+
logger.lifecycle("[pluginLoadTest] No plugin load failures detected. Task successful.")
147+
148+
}
149+
150+
}

docs/wiki/FAQ.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,13 @@ and [ZenithProxyMod](https://github.com/rfresh2/ZenithProxyMod/)
125125

126126
Load pearls over the API [with this command](https://github.com/rfresh2/ZenithProxyMod#web-api-commands)
127127

128-
## Can I ZenithProxy run while my PC is off?
128+
## Can I run ZenithProxy while my PC is off?
129129

130130
No.
131131

132-
You can use a spare computer kept on all the time, or you can run ZenithProxy on a [VPS](./DigitalOcean-Setup-Guide.md).
132+
Some people use a spare computer.
133+
134+
Some choose to rent a computer in a datacenter (VPS), I recommend [DigitalOcean](./DigitalOcean-Setup-Guide.md).
133135

134136
## How can I make my Xaero map work while using ZenithProxy?
135137

src/main/java/com/zenith/command/impl/PluginsCommand.java

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
package com.zenith.command.impl;
22

33
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
4-
import com.zenith.command.api.Command;
5-
import com.zenith.command.api.CommandCategory;
6-
import com.zenith.command.api.CommandContext;
7-
import com.zenith.command.api.CommandUsage;
4+
import com.zenith.command.api.*;
85
import com.zenith.discord.DiscordBot;
96
import com.zenith.feature.api.Api;
107
import com.zenith.plugin.PluginManager;
118
import com.zenith.plugin.api.PluginInfo;
129
import com.zenith.util.ImageInfo;
1310
import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodec;
11+
import org.jspecify.annotations.Nullable;
1412

13+
import java.io.File;
1514
import java.net.MalformedURLException;
1615
import java.net.URI;
1716
import java.net.URL;
1817
import java.net.http.HttpRequest;
1918
import java.net.http.HttpResponse;
19+
import java.nio.file.FileSystems;
2020
import java.nio.file.Files;
2121
import java.nio.file.StandardOpenOption;
22+
import java.util.Collections;
2223
import java.util.Comparator;
24+
import java.util.List;
2325
import java.util.Objects;
2426
import java.util.stream.Collectors;
2527

@@ -56,7 +58,10 @@ public CommandUsage commandUsage() {
5658

5759
@Override
5860
public LiteralArgumentBuilder<CommandContext> register() {
59-
return command("plugins").requires(Command::validateAccountOwner)
61+
return command("plugins")
62+
.requires(c -> Command.validateAccountOwner(c)
63+
// todo: consider blocking discord source by default, overridable by config
64+
&& Command.validateCommandSource(c, List.of(CommandSources.TERMINAL, CommandSources.DISCORD)))
6065
.then(argument("toggle", toggle()).executes(c -> {
6166
CONFIG.plugins.enabled = getToggle(c, "toggle");
6267
c.getSource().getEmbed()
@@ -115,15 +120,38 @@ public LiteralArgumentBuilder<CommandContext> register() {
115120
return ERROR;
116121
}
117122
var api = new PluginDownloadApi();
118-
if (!api.download(url)) {
123+
var downloadResult = api.download(url);
124+
if (!downloadResult.success()) {
119125
c.getSource().getEmbed()
120126
.title("Download Failed")
121-
.description("More info may be in ZenithProxy logs");
127+
.description(downloadResult.error());
128+
if (downloadResult.file() != null) downloadResult.file().delete();
122129
return ERROR;
123130
}
131+
var readResult = readPluginInfo(downloadResult.file());
132+
if (!readResult.success()) {
133+
c.getSource().getEmbed()
134+
.title("Invalid Plugin Jar")
135+
.description(readResult.error());
136+
downloadResult.file().delete();
137+
return ERROR;
138+
}
139+
var pluginId = readResult.pluginInfo().id();
140+
var existingPlugin = PLUGIN_MANAGER.getPluginInstance(pluginId);
141+
String desc = "Restart ZenithProxy to reload plugins: `restart`";
142+
if (existingPlugin != null) {
143+
existingPlugin.getJarPath().toFile().deleteOnExit();
144+
desc += "\n\nExisting plugin with ID: `%s` found. It will be replaced/updated on next restart.".formatted(pluginId);
145+
}
124146
c.getSource().getEmbed()
125-
.title("Jar Downloaded")
126-
.description(appendWarningToDescription("Restart ZenithProxy to reload plugins: `restart`"))
147+
.title("Plugin Downloaded")
148+
.description(appendWarningToDescription(desc))
149+
.addField("ID", pluginId)
150+
.addField("Description", readResult.pluginInfo().description())
151+
.addField("Version", readResult.pluginInfo().version())
152+
.addField("URL", readResult.pluginInfo().url())
153+
.addField("Author(s)", String.join(", ", readResult.pluginInfo().authors()))
154+
.addField("Jar", downloadResult.file().toPath().getFileName())
127155
.primaryColor();
128156
return OK;
129157
})))
@@ -158,33 +186,59 @@ private String appendWarningToDescription(String description) {
158186
return description;
159187
}
160188

189+
private PluginInfoReadResult readPluginInfo(File jarFile) {
190+
var zipUri = URI.create("jar:file:" + jarFile.toURI().getPath());
191+
try (var fs = FileSystems.newFileSystem(zipUri, Collections.emptyMap())) {
192+
var root = fs.getPath("/");
193+
var pluginJson = root.resolve("zenithproxy.plugin.json");
194+
if (!Files.exists(pluginJson)) {
195+
return new PluginInfoReadResult(false, "No zenithproxy.plugin.json found in jar", null);
196+
}
197+
// should never be larger than a few kb
198+
if (Files.size(pluginJson) > 100 * 1024) {
199+
return new PluginInfoReadResult(false, "zenithproxy.plugin.json is too large", null);
200+
}
201+
var jsonString = Files.readString(pluginJson);
202+
var pluginInfo = OBJECT_MAPPER.readValue(jsonString, PluginInfo.class);
203+
return new PluginInfoReadResult(true, null, pluginInfo);
204+
} catch (Exception e) {
205+
return new PluginInfoReadResult(false, e.getMessage(), null);
206+
}
207+
}
208+
209+
record PluginInfoReadResult(boolean success, @Nullable String error, @Nullable PluginInfo pluginInfo) {}
210+
161211
private static class PluginDownloadApi extends Api {
162212

163213
public PluginDownloadApi() {
164214
super("");
165215
}
166216

167-
public boolean download(URL url) {
217+
public PluginDownloadResult download(URL url) {
168218
HttpRequest request = buildBaseRequest(url.toString())
169219
.GET()
170220
.build();
221+
File resFile = null;
171222
try (var client = buildHttpClient()) {
172223
var response = client
173224
.send(request, HttpResponse.BodyHandlers.ofFileDownload(PluginManager.PLUGINS_PATH, StandardOpenOption.CREATE, StandardOpenOption.WRITE));
174225
if (response.statusCode() >= 400) {
175226
PLUGIN_LOG.error("Failed to download plugin from: {} - {}", url, response.statusCode());
176-
return false;
227+
return new PluginDownloadResult(false, "Failed to download plugin, HTTP error code: %s".formatted(url, response.statusCode()), null);
177228
}
178229
// verify the jar was written to file
179230
if (!Files.exists(response.body())) {
180231
PLUGIN_LOG.error("Failed to download plugin from: {} - File not written", url);
181-
return false;
232+
return new PluginDownloadResult(false, "Failed to download plugin, file not written", null);
182233
}
183-
return true;
234+
resFile = response.body().toFile();
235+
return new PluginDownloadResult(true, null, resFile);
184236
} catch (Exception e) {
185237
PLUGIN_LOG.error("Failed to download plugin from: {} - {}", url, e.getMessage());
238+
return new PluginDownloadResult(false, "Failed to download plugin, %s".formatted(e.getMessage()), resFile);
186239
}
187-
return false;
188240
}
189241
}
242+
243+
record PluginDownloadResult(boolean success, @Nullable String error, @Nullable File file) { }
190244
}

src/main/java/com/zenith/feature/pathfinder/Baritone.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ public PathingRequestFuture getTo(final Block block) {
128128
return getGetToBlockProcess().getToBlock(block);
129129
}
130130

131+
@Override
132+
public PathingRequestFuture getTo(final Block block, boolean rightClickContainerOnArrival) {
133+
return getGetToBlockProcess().getToBlock(block, rightClickContainerOnArrival);
134+
}
135+
131136
@Override
132137
public PathingRequestFuture mine(Block... blocks) {
133138
return getMineProcess().mine(blocks);

0 commit comments

Comments
 (0)