Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client).
* Copyright (c) Meteor Development.
*/

package meteordevelopment.meteorclient.events.game;

/** Posted the moment the client learns why it's about to disconnect, before {@link GameLeftEvent} fires. */
public class DisconnectReasonEvent {
public final String reason;

public DisconnectReasonEvent(String reason) {
this.reason = reason == null ? "" : reason;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client).
* Copyright (c) Meteor Development.
*/

package meteordevelopment.meteorclient.gui.screens.baritoneflow;

import meteordevelopment.meteorclient.gui.GuiTheme;
import meteordevelopment.meteorclient.gui.WindowScreen;
import meteordevelopment.meteorclient.gui.widgets.containers.WSection;
import meteordevelopment.meteorclient.gui.widgets.pressable.WButton;
import meteordevelopment.meteorclient.systems.baritoneflow.Flow;
import meteordevelopment.meteorclient.systems.baritoneflow.FlowNode;
import meteordevelopment.meteorclient.systems.baritoneflow.FlowNodeType;

/**
* Picker shown when right-clicking empty canvas in the {@link FlowEditorScreen}. Adds a node of the
* chosen type at the click position.
*/
public class AddFlowNodeScreen extends WindowScreen {
private final Flow flow;
private final int x, y;

public AddFlowNodeScreen(GuiTheme theme, Flow flow, int x, int y) {
super(theme, "Add Node");

this.flow = flow;
this.x = x;
this.y = y;
}

@Override
public void initWidgets() {
WSection triggers = add(theme.section("Triggers")).expandX().widget();
WSection baritone = add(theme.section("Baritone Tasks")).expandX().widget();
WSection modules = add(theme.section("Meteor Modules")).expandX().widget();
WSection client = add(theme.section("Client Actions")).expandX().widget();

for (FlowNodeType type : FlowNodeType.values()) {
if (type == FlowNodeType.Start) continue; // only one Start node per flow

WSection section = switch (type.category()) {
case Trigger -> triggers;
case Module -> modules;
case Client -> client;
case Baritone -> baritone;
};

WButton button = section.add(theme.button(type.label())).expandX().widget();
button.action = () -> {
FlowNode node = flow.addNode(x, y);
node.type.set(type);
onClose();
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client).
* Copyright (c) Meteor Development.
*/

package meteordevelopment.meteorclient.gui.screens.baritoneflow;

import meteordevelopment.meteorclient.gui.GuiTheme;
import meteordevelopment.meteorclient.gui.WindowScreen;
import meteordevelopment.meteorclient.gui.widgets.containers.WHorizontalList;
import meteordevelopment.meteorclient.gui.widgets.pressable.WButton;
import meteordevelopment.meteorclient.systems.baritoneflow.DisconnectLog;

import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

/** Lists every disconnect reason recorded by {@link DisconnectLog}, newest first, with a way to clear the history. */
public class DisconnectLogScreen extends WindowScreen {
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());

public DisconnectLogScreen(GuiTheme theme) {
super(theme, "Disconnect Log");
}

@Override
public void initWidgets() {
DisconnectLog log = DisconnectLog.get();

WHorizontalList top = add(theme.horizontalList()).expandX().widget();
top.add(theme.label(log.getAll().size() + " recorded disconnect(s).")).expandCellX().widget();

WButton clear = top.add(theme.button("Clear")).widget();
clear.action = () -> {
log.clear();
reload();
};

add(theme.horizontalSeparator()).expandX();

if (log.getAll().isEmpty()) {
add(theme.label("No disconnects recorded yet."));
return;
}

for (DisconnectLog.Entry entry : log.getAll()) {
WHorizontalList row = add(theme.horizontalList()).expandX().widget();

row.add(theme.label(TIME_FORMAT.format(Instant.ofEpochMilli(entry.time())))).widget();

String reason = entry.reason().isBlank() ? "(unknown)" : entry.reason().replace("\n", " ");
row.add(theme.label(reason)).expandCellX().widget();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
/*
* This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client).
* Copyright (c) Meteor Development.
*/

package meteordevelopment.meteorclient.gui.screens.baritoneflow;

import meteordevelopment.meteorclient.gui.GuiTheme;
import meteordevelopment.meteorclient.gui.WidgetScreen;
import meteordevelopment.meteorclient.renderer.Renderer2D;
import meteordevelopment.meteorclient.renderer.text.VanillaTextRenderer;
import meteordevelopment.meteorclient.systems.baritoneflow.BaritoneFlows;
import meteordevelopment.meteorclient.systems.baritoneflow.Flow;
import meteordevelopment.meteorclient.systems.baritoneflow.FlowNode;
import meteordevelopment.meteorclient.systems.baritoneflow.FlowNodeType;
import meteordevelopment.meteorclient.systems.modules.Modules;
import meteordevelopment.meteorclient.systems.modules.baritone.FlowBuilder;
import meteordevelopment.meteorclient.utils.render.color.Color;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.client.input.MouseButtonEvent;
import org.lwjgl.glfw.GLFW;

import static meteordevelopment.meteorclient.MeteorClient.mc;

/**
* n8n-style visual editor for a {@link Flow}. Nodes are draggable boxes; drag from a node's right
* (output) port onto another node to wire them together. Right-click a node to edit it, right-click
* empty space to add one. Rendered directly with {@link Renderer2D} like the HUD editor.
*/
public class FlowEditorScreen extends WidgetScreen {
private static final int NODE_W = 172;
private static final int NODE_H = 48;
private static final int PORT = 8;

private static final Color BG = new Color(38, 40, 52, 220);
private static final Color BG_HOVER = new Color(54, 57, 72, 230);
private static final Color BG_CURRENT = new Color(70, 62, 30, 235);
private static final Color OL = new Color(120, 124, 145, 255);
private static final Color OL_START = new Color(90, 190, 110, 255);
private static final Color OL_TRIGGER = new Color(150, 110, 235, 255);
private static final Color OL_CURRENT = new Color(235, 190, 70, 255);
private static final Color PORT_COLOR = new Color(150, 155, 190, 255);
private static final Color LINE = new Color(150, 155, 180, 220);
private static final Color TITLE = new Color(235, 236, 245, 255);
private static final Color SUBTITLE = new Color(170, 174, 190, 255);
private static final Color HELP = new Color(200, 202, 214, 190);

private final Flow flow;

private int lastMouseX, lastMouseY;

private FlowNode draggingNode;
private int dragOffsetX, dragOffsetY;
private FlowNode connectingFrom;

public FlowEditorScreen(GuiTheme theme, Flow flow) {
super(theme, "Flow: " + flow.name);
this.flow = flow;
}

@Override
public void initWidgets() {
// Everything is custom-rendered; no widgets on the canvas itself.
}

// Input

@Override
public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {
int mx = scale(click.x());
int my = scale(click.y());

if (click.button() == GLFW.GLFW_MOUSE_BUTTON_LEFT) {
FlowNode port = getOutputPortAt(mx, my);
if (port != null) {
connectingFrom = port;
return true;
}

FlowNode node = getNodeAt(mx, my);
if (node != null) {
draggingNode = node;
dragOffsetX = mx - node.x;
dragOffsetY = my - node.y;
}
} else if (click.button() == GLFW.GLFW_MOUSE_BUTTON_RIGHT) {
FlowNode node = getNodeAt(mx, my);
if (node != null) mc.setScreen(new FlowNodeScreen(theme, flow, node));
else mc.setScreen(new AddFlowNodeScreen(theme, flow, mx, my));
return true;
}

return false;
}

@Override
public void mouseMoved(double mouseX, double mouseY) {
lastMouseX = scale(mouseX);
lastMouseY = scale(mouseY);

if (draggingNode != null) {
draggingNode.x = lastMouseX - dragOffsetX;
draggingNode.y = lastMouseY - dragOffsetY;
}
}

@Override
public boolean mouseReleased(MouseButtonEvent click) {
int mx = scale(click.x());
int my = scale(click.y());

if (click.button() == GLFW.GLFW_MOUSE_BUTTON_LEFT) {
if (connectingFrom != null) {
FlowNode target = getNodeAt(mx, my);
if (target != null && target != connectingFrom) {
// Toggle: wire them up, or remove an existing wire.
if (connectingFrom.children.contains(target.id)) flow.disconnect(connectingFrom, target);
else flow.connect(connectingFrom, target);
save();
}
connectingFrom = null;
}

if (draggingNode != null) {
draggingNode = null;
save();
}
}

return false;
}

@Override
public boolean keyPressed(KeyEvent input) {
switch (input.key()) {
case GLFW.GLFW_KEY_DELETE -> {
FlowNode node = getNodeAt(lastMouseX, lastMouseY);
if (node != null) {
flow.removeNode(node);
save();
return true;
}
}
case GLFW.GLFW_KEY_R -> {
FlowBuilder module = Modules.get().get(FlowBuilder.class);
if (module != null) module.runFlow(flow);
return true;
}
case GLFW.GLFW_KEY_X -> {
FlowBuilder module = Modules.get().get(FlowBuilder.class);
if (module != null) module.stopAll();
return true;
}
case GLFW.GLFW_KEY_A -> {
flow.armed = !flow.armed;
save();
return true;
}
}

return super.keyPressed(input);
}

// Rendering

@Override
protected void onRenderBefore(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) {
FlowNode current = currentNode();
FlowNode hovered = getNodeAt(mouseX, mouseY);

Renderer2D r = Renderer2D.COLOR;
r.begin();

// Connections
for (FlowNode node : flow.nodes) {
double ox = node.x + NODE_W, oy = node.y + NODE_H / 2.0;
for (int childId : node.children) {
FlowNode child = flow.getById(childId);
if (child == null) continue;
r.line(ox, oy, child.x, child.y + NODE_H / 2.0, LINE);
}
}

// Wire currently being dragged
if (connectingFrom != null) {
r.line(connectingFrom.x + NODE_W, connectingFrom.y + NODE_H / 2.0, lastMouseX, lastMouseY, LINE);
}

// Nodes
for (FlowNode node : flow.nodes) {
boolean isStart = node.type.get() == FlowNodeType.Start;
boolean isTrigger = node.type.get().category() == FlowNodeType.Category.Trigger;
Color bg = node == current ? BG_CURRENT : (node == hovered ? BG_HOVER : BG);
Color ol = node == current ? OL_CURRENT : (isStart ? OL_START : (isTrigger ? OL_TRIGGER : OL));

box(r, node.x, node.y, NODE_W, NODE_H, bg, ol);

// Ports
r.quad(node.x - PORT / 2.0, node.y + NODE_H / 2.0 - PORT / 2.0, PORT, PORT, PORT_COLOR);
r.quad(node.x + NODE_W - PORT / 2.0, node.y + NODE_H / 2.0 - PORT / 2.0, PORT, PORT, PORT_COLOR);
}

r.render();

// Text
VanillaTextRenderer text = VanillaTextRenderer.INSTANCE;
text.begin(1, false, false);

text.render("Flow: " + flow.name + (flow.armed ? " [ARMED]" : ""), 8, 8, TITLE, true);
text.render("Left-drag node / wire ports - Right-click: edit or add - Delete: remove - R: run - X: stop - A: arm/disarm", 8, 26, HELP, true);

for (FlowNode node : flow.nodes) {
text.render(node.title(), node.x + 8, node.y + 7, TITLE, false);
String summary = clip(node.summary(), 22);
if (!summary.isEmpty()) text.render(summary, node.x + 8, node.y + 26, SUBTITLE, false);
}

text.end();
}

private void box(Renderer2D r, double x, double y, double w, double h, Color bg, Color ol) {
r.quad(x + 1, y + 1, w - 2, h - 2, bg);
r.quad(x, y, w, 1, ol);
r.quad(x, y + h - 1, w, 1, ol);
r.quad(x, y + 1, 1, h - 2, ol);
r.quad(x + w - 1, y + 1, 1, h - 2, ol);
}

// Helpers

private int scale(double coord) {
return (int) (coord * mc.getWindow().getGuiScale());
}

private FlowNode getNodeAt(int mx, int my) {
for (int i = flow.nodes.size() - 1; i >= 0; i--) {
FlowNode n = flow.nodes.get(i);
if (mx >= n.x && mx <= n.x + NODE_W && my >= n.y && my <= n.y + NODE_H) return n;
}
return null;
}

private FlowNode getOutputPortAt(int mx, int my) {
for (int i = flow.nodes.size() - 1; i >= 0; i--) {
FlowNode n = flow.nodes.get(i);
double px = n.x + NODE_W, py = n.y + NODE_H / 2.0;
if (Math.abs(mx - px) <= PORT && Math.abs(my - py) <= PORT) return n;
}
return null;
}

private FlowNode currentNode() {
FlowBuilder module = Modules.get().get(FlowBuilder.class);
if (module == null || module.getRunningFlow() != flow) return null;
return module.getCurrentNode();
}

private void save() {
BaritoneFlows.get().save();
}

private static String clip(String text, int max) {
if (text == null) return "";
return text.length() <= max ? text : text.substring(0, max - 1) + "…";
}
}
Loading