Add advancements, class generator, and recipe handling.

- Removed deprecated `Remarkable` annotation/processor.
- Added advancements for Nether and Skyblock progression.
- Introduced `ClassBuilder` for dynamic class generation.
- Implemented `SmeltingCraftingHandler` for custom recipes.
- Updated dependencies for Paper API integration.
This commit is contained in:
Nick Guy 2025-08-09 21:22:06 +01:00
parent c869fdefb1
commit 4032a0c6ff
123 changed files with 6731 additions and 680 deletions

View file

@ -0,0 +1,31 @@
plugins {
id 'java'
id 'application'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'com.google.code.gson:gson:2.11.0'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
application {
mainClass = 'com.ncguy.achievements.Crafter'
}
// Swing is part of the JDK; no extra dependencies required.
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
options.release.set(21)
}
run.jvmArgs += ["--add-opens", "java.base/java.io=ALL-UNNAMED", "--add-opens", "java.base/sun.nio.cs=ALL-UNNAMED", "--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED"]

View file

@ -0,0 +1,26 @@
package com.ncguy.achievements;
import com.ncguy.achievements.ui.TreeSplitPanePanel;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import java.awt.Dimension;
public class Crafter {
public static void main(String[] args) {
SwingUtilities.invokeLater(Crafter::createAndShowUI);
}
private static void createAndShowUI() {
JFrame frame = new JFrame("UsefulSkyblock | Advancement Crafter");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
TreeSplitPanePanel panel = new TreeSplitPanePanel();
frame.setContentPane(panel);
frame.setPreferredSize(new Dimension(800, 500));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}

View file

@ -0,0 +1,37 @@
package com.ncguy.achievements.data;
import com.ncguy.achievements.ui.TreeSplitPanePanel;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class TreeNode<T extends TreeNode<T>> {
public List<T> children;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public Optional<T> parent;
public transient String parentId;
public TreeNode() {
children = new ArrayList<>();
parent = Optional.empty();
parentId = null;
}
public void setParent(T parent) {
this.parent = Optional.ofNullable(parent);
this.parentId = null;
}
public void addChild(T child) {
this.children.add(child);
//noinspection unchecked
child.setParent((T) this);
}
public void removeChild(T child) {
child.setParent(null);
this.children.remove(child);
}
}

View file

@ -0,0 +1,286 @@
package com.ncguy.achievements.data.gen;
import com.ncguy.achievements.data.TreeNode;
import java.util.List;
import java.util.Map;
/**
* Represents a Minecraft datapack advancement JSON structure.
* This model is designed to closely mirror the schema in
* src/main/resources/schemas/minecraft_advancement_schema.json
* while remaining library-agnostic (no JSON annotations).
*/
public class Advancement extends TreeNode<Advancement> {
private String id;
/**
* Data related to the advancement's display (icon, title, etc.).
*/
private Display display;
/**
* The criteria tracked by this advancement. Each key is a criterion name, mapping to
* an object describing the criterion. The exact shape is flexible (e.g., may contain
* fields like "trigger" and "conditions").
*/
private Map<String, Object> criteria;
/**
* Defines how criteria are combined to grant the advancement. Each inner list is a group
* of criterion names where at least one must be satisfied; the outer list requires all groups.
*/
private List<List<String>> requirements;
/**
* Rewards granted when the advancement is completed.
*/
private Rewards rewards;
/**
* Whether telemetry data should be collected on completion.
*/
private Boolean sendsTelemetryEvent;
public Advancement() {
}
// Getters and setters
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Display getDisplay() {
return display;
}
public void setDisplay(Display display) {
this.display = display;
}
public Map<String, Object> getCriteria() {
return criteria;
}
public void setCriteria(Map<String, Object> criteria) {
this.criteria = criteria;
}
public List<List<String>> getRequirements() {
return requirements;
}
public void setRequirements(List<List<String>> requirements) {
this.requirements = requirements;
}
public Rewards getRewards() {
return rewards;
}
public void setRewards(Rewards rewards) {
this.rewards = rewards;
}
public Boolean getSendsTelemetryEvent() {
return sendsTelemetryEvent;
}
public void setSendsTelemetryEvent(Boolean sendsTelemetryEvent) {
this.sendsTelemetryEvent = sendsTelemetryEvent;
}
/**
* Display section of an advancement.
*/
public static class Display {
public enum DisplayFrame {
CHALLENGE,
GOAL,
TASK;
@Override
public String toString() {
return name().toLowerCase();
}
}
private Icon icon;
/**
* Title supports Minecraft JSON text component types: string, object, or array.
* Represented as Object to stay flexible.
*/
private Object title;
/**
* Description supports Minecraft JSON text component types: string, object, or array.
* Represented as Object to stay flexible.
*/
private Object description;
/**
* Frame type for the icon: "challenge", "goal", or "task".
*/
private DisplayFrame frame;
/**
* The directory for the background (used only for root advancements).
*/
private String background;
private Boolean showToast;
private Boolean announceToChat;
private Boolean hidden;
public Display() {}
public Icon getIcon() {
return icon;
}
public void setIcon(Icon icon) {
this.icon = icon;
}
public Object getTitle() {
return title;
}
public void setTitle(Object title) {
this.title = title;
}
public Object getDescription() {
return description;
}
public void setDescription(Object description) {
this.description = description;
}
public DisplayFrame getFrame() {
return frame;
}
public void setFrame(DisplayFrame frame) {
this.frame = frame;
}
public String getBackground() {
return background;
}
public void setBackground(String background) {
this.background = background;
}
public Boolean getShowToast() {
return showToast;
}
public void setShowToast(Boolean showToast) {
this.showToast = showToast;
}
public Boolean getAnnounceToChat() {
return announceToChat;
}
public void setAnnounceToChat(Boolean announceToChat) {
this.announceToChat = announceToChat;
}
public Boolean getHidden() {
return hidden;
}
public void setHidden(Boolean hidden) {
this.hidden = hidden;
}
}
/**
* Icon definition for the display section.
*/
public static class Icon {
/** Item ID, e.g., "minecraft:stone" */
private String id;
/** Amount of the item. Defaults to 1 if omitted in JSON. */
private Integer count;
/** Additional item components/information. */
private Map<String, Object> components;
public Icon() {}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Map<String, Object> getComponents() {
return components;
}
public void setComponents(Map<String, Object> components) {
this.components = components;
}
}
/**
* Rewards section of an advancement.
*/
public static class Rewards {
private Integer experience;
private List<String> recipes;
private List<String> loot;
private String function;
public Rewards() {}
public Integer getExperience() {
return experience;
}
public void setExperience(Integer experience) {
this.experience = experience;
}
public List<String> getRecipes() {
return recipes;
}
public void setRecipes(List<String> recipes) {
this.recipes = recipes;
}
public List<String> getLoot() {
return loot;
}
public void setLoot(List<String> loot) {
this.loot = loot;
}
public String getFunction() {
return function;
}
public void setFunction(String function) {
this.function = function;
}
}
}

View file

@ -0,0 +1,60 @@
package com.ncguy.achievements.data.gen;
import com.google.gson.Gson;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
public class AdvancementTreeSetLoader {
private List<Advancement> orphanedAdvancements;
public AdvancementTreeSetLoader() {
orphanedAdvancements = new ArrayList<>();
}
public Advancement load(Path directory) {
File[] files = directory.toFile().listFiles();
Gson gson = AdvancementTypeAdapter.createGsonWithAdapter();
for (File file : files) {
Path path = file.toPath();
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
Advancement result = gson.fromJson(reader, Advancement.class);
orphanedAdvancements.add(result);
} catch (IOException e) {
System.err.println(e);
}
}
orphanedAdvancements.sort((a, b) -> {
if (a.parentId == null)
return -1;
if (b.parentId == null)
return 1;
return a.parentId.compareTo(b.parentId);
});
findParentsForOrphans();
return orphanedAdvancements.getFirst();
}
private void findParentsForOrphans() {
while (orphanedAdvancements.size() > 1) {
Advancement last = orphanedAdvancements.getLast();
if (last.parentId != null)
break;
Optional<Advancement> first = orphanedAdvancements.stream().filter(x -> Objects.equals(x.getId(), last.parentId)).findFirst();
first.ifPresent(parent -> {
parent.addChild(last);
});
orphanedAdvancements.remove(last);
}
}
}

View file

@ -0,0 +1,124 @@
package com.ncguy.achievements.data.gen;
import com.google.gson.*;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.ncguy.achievements.utils.ReflectionUtils;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.*;
import static com.ncguy.achievements.utils.ReflectionUtils.*;
/**
* GSON TypeAdapter for Advancement that preserves unknown/custom top-level fields.
*
* Notes:
* - Known JSON keys follow the Minecraft advancement spec (snake_case).
* - The Advancement model uses camelCase; this adapter handles the mapping.
*/
public class AdvancementTypeAdapter extends TypeAdapter<Advancement> {
private static final String K_DISPLAY = "display";
private static final String K_CRITERIA = "criteria";
private static final String K_REQUIREMENTS = "requirements";
private static final String K_REWARDS = "rewards";
private static final String K_SENDS_TELEMETRY = "sends_telemetry_event";
private static final String K_PARENT = "parent";
private final Gson gson;
public AdvancementTypeAdapter(Gson gson) {
this.gson = gson;
}
@Override
public void write(JsonWriter out, Advancement value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
JsonObject obj = new JsonObject();
if (value.getDisplay() != null) obj.add(K_DISPLAY, gson.toJsonTree(value.getDisplay()));
if (value.getCriteria() != null) obj.add(K_CRITERIA, gson.toJsonTree(value.getCriteria()));
if (value.getRequirements() != null) obj.add(K_REQUIREMENTS, gson.toJsonTree(value.getRequirements()));
if (value.getRewards() != null) obj.add(K_REWARDS, gson.toJsonTree(value.getRewards()));
if (value.getSendsTelemetryEvent() != null) obj.addProperty(K_SENDS_TELEMETRY, value.getSendsTelemetryEvent());
value.parent.ifPresent(advancement -> obj.addProperty(K_PARENT, advancement.getId()));
Streams.write(obj, out);
}
@Override
public Advancement read(JsonReader in) throws IOException {
JsonElement root = Streams.parse(in);
if (root == null || root.isJsonNull()) return null;
JsonObject obj = root.getAsJsonObject();
BufferedReader br = get(in, "in");
InputStreamReader isr = get(br, "in");
var sd = get(isr, "sd");
var cire = get(sd, "in");
var fci = get(cire, "ch");
String path = get(fci, "path");
Advancement adv = new Advancement();
// TODO extract from path
String namespace = "usefulskyblock";
File f = new File(path);
String group = f.getParentFile().getName();
String name = f.getName().substring(0, f.getName().length() - 5);
adv.setId(namespace + ":" + group + "/" + name);
if (obj.has(K_DISPLAY)) adv.setDisplay(gson.fromJson(obj.get(K_DISPLAY), Advancement.Display.class));
if (obj.has(K_CRITERIA)) {
Type t = new TypeToken<Map<String, Object>>() {}.getType();
adv.setCriteria(gson.fromJson(obj.get(K_CRITERIA), t));
}
if (obj.has(K_REQUIREMENTS)) {
Type t = new TypeToken<List<List<String>>>() {}.getType();
adv.setRequirements(gson.fromJson(obj.get(K_REQUIREMENTS), t));
}
if (obj.has(K_REWARDS)) adv.setRewards(gson.fromJson(obj.get(K_REWARDS), Advancement.Rewards.class));
if (obj.has(K_SENDS_TELEMETRY)) adv.setSendsTelemetryEvent(getAsBooleanSafe(obj.get(K_SENDS_TELEMETRY)));
if (obj.has(K_PARENT)) adv.parentId = obj.get(K_PARENT).getAsString();
return adv;
}
private static boolean isKnownKey(String key) {
return K_DISPLAY.equals(key)
|| K_CRITERIA.equals(key)
|| K_REQUIREMENTS.equals(key)
|| K_REWARDS.equals(key)
|| K_SENDS_TELEMETRY.equals(key)
|| K_PARENT.equals(key);
}
private static Boolean getAsBooleanSafe(JsonElement el) {
if (el == null || el.isJsonNull()) return null;
if (el.isJsonPrimitive() && el.getAsJsonPrimitive().isBoolean()) return el.getAsBoolean();
return null;
}
/**
* Convenience helper to build a Gson instance with this adapter registered.
*/
public static Gson createGsonWithAdapter() {
GsonBuilder b = new GsonBuilder();
Gson gson = b.create();
// The adapter requires a Gson reference; build another builder to register once we have it
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Advancement.class, new AdvancementTypeAdapter(gson));
return builder.create();
}
}

View file

@ -0,0 +1,330 @@
package com.ncguy.achievements.ui;
import com.ncguy.achievements.data.gen.Advancement;
import com.ncguy.achievements.data.gen.AdvancementTreeSetLoader;
import com.ncguy.achievements.ui.editor.AdvancementEditorPanel;
import com.ncguy.achievements.ui.editor.extensions.AutoCompleteProvider;
import javax.swing.BorderFactory;
import javax.swing.DropMode;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.JTree;
import javax.swing.JScrollPane;
import javax.swing.TransferHandler;
import javax.swing.JPopupMenu;
import javax.swing.JMenuItem;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.text.JTextComponent;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreePath;
import java.awt.BorderLayout;
import javax.swing.JComponent;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.io.Serializable;
import java.nio.file.Path;
import java.util.Optional;
/**
* A reusable UI control providing:
* - Adjustable split pane
* - Left: JTree with drag-and-drop reordering of nodes within the same tree
* - Right: empty container for dynamic content
*/
public class TreeSplitPanePanel extends JPanel {
private final JSplitPane splitPane;
private final JTree tree;
private final JPanel rightContainer;
private static class AdvancementMutableTreeNode extends DefaultMutableTreeNode {
public AdvancementMutableTreeNode(Advancement userObject) {
super(userObject);
}
public Advancement getAdvancement() {
return (Advancement) getUserObject();
}
@Override
public String toString() {
return getAdvancement().getId();
}
}
public TreeSplitPanePanel() {
super(new BorderLayout());
// Build sample tree model (can be replaced by callers as needed)
// TODO remove hardcoded filepath
Advancement rootAdvancement = new AdvancementTreeSetLoader().load(Path.of("S:\\Development\\Minecraft-PaperMC\\UsefulSkyblock\\src\\main\\resources\\datapacks\\usefulskyblock\\data\\usefulskyblock\\advancement\\skyblock"));
AdvancementMutableTreeNode root = new AdvancementMutableTreeNode(rootAdvancement);
buildChildren(root);
DefaultTreeModel model = new DefaultTreeModel(root);
this.tree = new JTree(model);
this.tree.setRootVisible(true);
this.tree.setShowsRootHandles(true);
this.tree.setDragEnabled(true);
this.tree.setDropMode(DropMode.ON_OR_INSERT);
this.tree.setTransferHandler(new TreeNodeMoveTransferHandler());
// Context menu for adding/removing nodes
JPopupMenu popupMenu = new JPopupMenu();
JMenuItem addChildItem = new JMenuItem("Add Child");
JMenuItem deleteItem = new JMenuItem("Delete");
popupMenu.add(addChildItem);
popupMenu.add(deleteItem);
// Add Child action
addChildItem.addActionListener(e -> {
TreePath selPath = this.tree.getSelectionPath();
if (selPath == null) return;
Object last = selPath.getLastPathComponent();
if (!(last instanceof AdvancementMutableTreeNode parentNode)) return;
// Create a new child Advancement with a placeholder id
Advancement newAdv = new Advancement();
String baseId = "new_advancement";
String id = baseId;
// Try to make id unique among siblings
DefaultTreeModel m = (DefaultTreeModel) this.tree.getModel();
int suffix = 1;
boolean unique;
do {
unique = true;
for (int i = 0; i < parentNode.getChildCount(); i++) {
Object c = ((DefaultMutableTreeNode) parentNode.getChildAt(i)).getUserObject();
if (c instanceof Advancement a && id.equals(a.getId())) {
unique = false; break;
}
}
if (!unique) id = baseId + "_" + (suffix++);
} while (!unique);
newAdv.setId(id);
// Keep data model and tree model in sync
parentNode.getAdvancement().addChild(newAdv);
AdvancementMutableTreeNode newChild = new AdvancementMutableTreeNode(newAdv);
m.insertNodeInto(newChild, parentNode, parentNode.getChildCount());
// Reveal and select the new node
TreePath newPath = selPath.pathByAddingChild(newChild);
this.tree.expandPath(selPath);
this.tree.setSelectionPath(newPath);
this.tree.scrollPathToVisible(newPath);
});
// Delete action (intentionally left empty per requirement)
deleteItem.addActionListener(e -> {
// Intentionally empty callback to be implemented later
});
// Show popup on appropriate mouse event
this.tree.addMouseListener(new MouseAdapter() {
private void maybeShowPopup(MouseEvent e) {
if (e.isPopupTrigger()) {
int row = tree.getRowForLocation(e.getX(), e.getY());
if (row != -1) {
tree.setSelectionRow(row);
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
}
@Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); }
@Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); }
});
JScrollPane treeScroll = new JScrollPane(tree);
rightContainer = new JPanel(new BorderLayout());
rightContainer.setBorder(BorderFactory.createEmptyBorder());
splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, treeScroll, rightContainer);
splitPane.setResizeWeight(0.3); // allocate 30% to the tree by default
splitPane.setContinuousLayout(true);
add(splitPane, BorderLayout.CENTER);
// Selection listener to render editor on right
this.tree.addTreeSelectionListener(e -> {
TreePath path = e.getNewLeadSelectionPath();
if (path == null) return;
Object last = path.getLastPathComponent();
if (last instanceof DefaultMutableTreeNode node) {
Object uo = node.getUserObject();
if (uo instanceof Advancement adv) {
showEditorFor(adv);
}
}
});
// Show editor for root initially
Object rootObj = ((DefaultMutableTreeNode) this.tree.getModel().getRoot()).getUserObject();
if (rootObj instanceof Advancement adv) {
showEditorFor(adv);
}
}
private void showEditorFor(Advancement adv) {
rightContainer.removeAll();
rightContainer.add(new AdvancementEditorPanel(adv, p -> {
p.provider = new AutoCompleteProvider() {
@Override
public boolean install(String fieldPath, JTextComponent component) {
return false;
}
};
}), BorderLayout.CENTER);
rightContainer.revalidate();
rightContainer.repaint();
}
private void buildChildren(AdvancementMutableTreeNode root) {
Advancement adv = root.getAdvancement();
adv.children.forEach(c -> {
AdvancementMutableTreeNode newChild = new AdvancementMutableTreeNode(c);
buildChildren(newChild);
root.add(newChild);
});
}
public JTree getTree() {
return tree;
}
public JPanel getRightContainer() {
return rightContainer;
}
public JSplitPane getSplitPane() {
return splitPane;
}
/**
* Empty listener invoked after a node is dropped via drag-and-drop.
* Subclasses may override to react to drops.
*/
protected void onNodeDropped(AdvancementMutableTreeNode node, TreePath destinationPath) {
// Intentionally empty per requirement
Object lastPathComponent = destinationPath.getLastPathComponent();
if(!(lastPathComponent instanceof AdvancementMutableTreeNode parent))
return;
node.getAdvancement().parent.ifPresent(p -> p.removeChild(node.getAdvancement()));
node.setParent(parent);
node.getAdvancement().parent = Optional.ofNullable(parent.getAdvancement());
}
// TransferHandler enabling moving nodes within the same JTree
private class TreeNodeMoveTransferHandler extends TransferHandler {
private static final DataFlavor NODES_FLAVOR = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType + ";class=javax.swing.tree.DefaultMutableTreeNode", "TreeNode");
private TreePath[] dragPaths;
@Override
public int getSourceActions(JComponent c) {
return MOVE;
}
@Override
protected Transferable createTransferable(JComponent c) {
if (!(c instanceof JTree)) return null;
JTree tree = (JTree) c;
dragPaths = tree.getSelectionPaths();
if (dragPaths == null || dragPaths.length == 0) return null;
// Only support single-node drag for simplicity and clarity
DefaultMutableTreeNode node = (DefaultMutableTreeNode) dragPaths[0].getLastPathComponent();
return new NodesTransferable(node);
}
@Override
public boolean canImport(TransferSupport support) {
if (!support.isDataFlavorSupported(NODES_FLAVOR)) return false;
if (!(support.getComponent() instanceof JTree)) return false;
support.setShowDropLocation(true);
JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
TreePath path = dl.getPath();
if (path == null) return false;
// Prevent dropping a node into itself or its descendants
try {
DefaultMutableTreeNode dropTarget = (DefaultMutableTreeNode) path.getLastPathComponent();
DefaultMutableTreeNode dragged = (DefaultMutableTreeNode) support.getTransferable().getTransferData(NODES_FLAVOR);
if (dragged == dropTarget) return false;
if (dragged.isNodeDescendant(dropTarget)) return false;
} catch (Exception ignored) {
}
return true;
}
@Override
public boolean importData(TransferSupport support) {
if (!canImport(support)) return false;
try {
JTree tree = (JTree) support.getComponent();
DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
DefaultMutableTreeNode dragged = (DefaultMutableTreeNode) support.getTransferable().getTransferData(NODES_FLAVOR);
// Remove from old location
DefaultMutableTreeNode oldParent = (DefaultMutableTreeNode) dragged.getParent();
int oldIndex = oldParent != null ? oldParent.getIndex(dragged) : -1;
if (oldParent != null) {
model.removeNodeFromParent(dragged);
}
JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
TreePath destPath = dl.getPath();
DefaultMutableTreeNode target = (DefaultMutableTreeNode) destPath.getLastPathComponent();
if (dl.getChildIndex() == -1) {
// ON: append as child of target
model.insertNodeInto(dragged, target, target.getChildCount());
} else {
// INSERT: insert next to the target's parent at index
DefaultMutableTreeNode parent = (DefaultMutableTreeNode) target.getParent();
int index = dl.getChildIndex();
if (parent == null) {
parent = target; // insert into root-level if target has no parent
}
model.insertNodeInto(dragged, parent, Math.min(index, parent.getChildCount()));
}
// Expand the path to show the newly moved node
tree.expandPath(new TreePath(((DefaultMutableTreeNode) model.getRoot()).getPath()));
// Invoke drop listener with details
if (dragged instanceof AdvancementMutableTreeNode amt) {
TreeSplitPanePanel.this.onNodeDropped(amt, destPath);
}
return true;
} catch (Exception e) {
return false;
}
}
private static class NodesTransferable implements Transferable, Serializable {
private final DefaultMutableTreeNode node;
NodesTransferable(DefaultMutableTreeNode node) {
this.node = node;
}
@Override
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[] { NODES_FLAVOR };
}
@Override
public boolean isDataFlavorSupported(DataFlavor flavor) {
return NODES_FLAVOR.equals(flavor);
}
@Override
public Object getTransferData(DataFlavor flavor) {
return node;
}
}
}
}

View file

@ -0,0 +1,303 @@
package com.ncguy.achievements.ui.editor;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import com.ncguy.achievements.data.gen.Advancement;
import com.ncguy.achievements.ui.editor.extensions.AutoCompleteProvider;
import com.ncguy.achievements.ui.editor.extensions.TypeAdvisor;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.text.JTextComponent;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.lang.reflect.Type;
import java.util.*;
import java.util.function.Consumer;
/**
* An editor panel for a single Advancement instance. It focuses on common top-level fields and offers
* JSON text areas for complex structures. Extension hooks are provided via ExtensionRegistry but not implemented here.
*/
public class AdvancementEditorPanel extends JPanel {
private final Advancement advancement;
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
// Display fields
private final JTextField iconId = new JTextField();
private final JSpinner iconCount = new JSpinner(new SpinnerNumberModel(1, 1, Integer.MAX_VALUE, 1));
private final JTextField background = new JTextField();
private final JComboBox<Advancement.Display.DisplayFrame> frame = new JComboBox<>(Advancement.Display.DisplayFrame.values());
private final JCheckBox showToast = new JCheckBox("Show toast");
private final JCheckBox announceToChat = new JCheckBox("Announce to chat");
private final JCheckBox hidden = new JCheckBox("Hidden");
private final JTextArea titleJson = new JTextArea(3, 30);
private final JTextArea descriptionJson = new JTextArea(4, 30);
// Other fields
private final JCheckBox telemetry = new JCheckBox("Sends telemetry event");
// Complex JSON fields
private final JTextArea criteriaJson = new JTextArea(8, 30);
private final JTextArea requirementsJson = new JTextArea(5, 30);
// Rewards
private final JSpinner exp = new JSpinner(new SpinnerNumberModel(0, 0, Integer.MAX_VALUE, 10));
private final JTextArea recipesJson = new JTextArea(3, 30);
private final JTextArea lootJson = new JTextArea(3, 30);
private final JTextField functionField = new JTextField();
// Custom fields table (key, json)
private final DefaultTableModel customModel = new DefaultTableModel(new Object[]{"Key", "JSON Value"}, 0);
private final JTable customTable = new JTable(customModel);
public TypeAdvisor advisor;
public AutoCompleteProvider provider;
public AdvancementEditorPanel(Advancement advancement) {
this(advancement, null);
}
public AdvancementEditorPanel(Advancement advancement, Consumer<AdvancementEditorPanel> earlySetup) {
super(new BorderLayout());
this.advancement = advancement;
if(earlySetup != null) earlySetup.accept(this);
JTabbedPane tabs = new JTabbedPane();
tabs.addTab("Display", wrap(buildDisplayPanel()));
tabs.addTab("Logic", wrap(buildLogicPanel()));
tabs.addTab("Rewards", wrap(buildRewardsPanel()));
tabs.addTab("Custom", wrap(buildCustomPanel()));
JPanel header = new JPanel(new BorderLayout());
JLabel idLabel = new JLabel("ID: " + Optional.ofNullable(advancement.getId()).orElse("<unknown>"));
JButton applyBtn = new JButton("Apply");
applyBtn.addActionListener(e -> applyToModel());
header.add(idLabel, BorderLayout.WEST);
header.add(applyBtn, BorderLayout.EAST);
header.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
add(header, BorderLayout.NORTH);
add(tabs, BorderLayout.CENTER);
loadFromModel();
}
private JComponent wrap(JComponent inner) {
return new JScrollPane(inner);
}
private JPanel buildDisplayPanel() {
JPanel p = new JPanel(new GridBagLayout());
GridBagConstraints gbc = baseGbc();
// display.icon.id
addLabeled(p, gbc, "Icon ID", enhance("display.icon.id", iconId));
// display.icon.count
addLabeled(p, gbc, "Icon Count", enhance("display.icon.count", iconCount));
// display.frame
addLabeled(p, gbc, "Frame", enhance("display.frame", frame));
// display.background
addLabeled(p, gbc, "Background", enhance("display.background", background));
// display.booleans
addLabeled(p, gbc, "Show Toast", enhance("display.show_toast", showToast));
addLabeled(p, gbc, "Announce To Chat", enhance("display.announce_to_chat", announceToChat));
addLabeled(p, gbc, "Hidden", enhance("display.hidden", hidden));
// title / description JSON
titleJson.setBorder(BorderFactory.createTitledBorder("Title (JSON text component)"));
descriptionJson.setBorder(BorderFactory.createTitledBorder("Description (JSON text component)"));
gbc.gridwidth = 2; gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1.0; gbc.weighty = 0.2;
p.add(enhance("display.title", new JScrollPane(titleJson)), gbc); gbc.gridy++;
p.add(enhance("display.description", new JScrollPane(descriptionJson)), gbc); gbc.gridy++;
return p;
}
private JPanel buildLogicPanel() {
JPanel p = new JPanel(new GridBagLayout());
GridBagConstraints gbc = baseGbc();
addLabeled(p, gbc, "Telemetry", enhance("sends_telemetry_event", telemetry));
criteriaJson.setBorder(BorderFactory.createTitledBorder("Criteria (JSON object)"));
requirementsJson.setBorder(BorderFactory.createTitledBorder("Requirements (JSON array of arrays)"));
gbc.gridwidth = 2; gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1.0; gbc.weighty = 0.5;
p.add(new JScrollPane(criteriaJson), gbc); gbc.gridy++;
p.add(new JScrollPane(requirementsJson), gbc); gbc.gridy++;
return p;
}
private JPanel buildRewardsPanel() {
JPanel p = new JPanel(new GridBagLayout());
GridBagConstraints gbc = baseGbc();
addLabeled(p, gbc, "Experience", enhance("rewards.experience", exp));
addLabeled(p, gbc, "Function", enhance("rewards.function", functionField));
recipesJson.setBorder(BorderFactory.createTitledBorder("Recipes (JSON array of strings)"));
lootJson.setBorder(BorderFactory.createTitledBorder("Loot (JSON array of strings)"));
gbc.gridwidth = 2; gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1.0; gbc.weighty = 0.3;
p.add(new JScrollPane(recipesJson), gbc); gbc.gridy++;
p.add(new JScrollPane(lootJson), gbc); gbc.gridy++;
return p;
}
private JPanel buildCustomPanel() {
JPanel panel = new JPanel(new BorderLayout());
JPanel top = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton addRow = new JButton("Add");
JButton removeRow = new JButton("Remove");
addRow.addActionListener(e -> customModel.addRow(new Object[]{"key", "null"}));
removeRow.addActionListener(e -> {
int i = customTable.getSelectedRow();
if (i >= 0) customModel.removeRow(i);
});
top.add(addRow); top.add(removeRow);
panel.add(top, BorderLayout.NORTH);
panel.add(new JScrollPane(customTable), BorderLayout.CENTER);
return panel;
}
private void addLabeled(JPanel panel, GridBagConstraints gbc, String label, JComponent editor) {
gbc.gridwidth = 1; gbc.weightx = 0; gbc.fill = GridBagConstraints.NONE;
panel.add(new JLabel(label), gbc); gbc.gridx++;
gbc.weightx = 1; gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(editor, gbc); gbc.gridx = 0; gbc.gridy++;
}
private GridBagConstraints baseGbc() {
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0; gbc.gridy = 0; gbc.insets = new Insets(4, 6, 4, 6);
gbc.anchor = GridBagConstraints.WEST;
return gbc;
}
private <T extends JComponent> T enhance(String fieldPath, T component) {
// Let TypeAdvisor replace or decorate the editor
if(advisor != null) {
Optional<JComponent> alt = advisor.provideEditor(fieldPath, component);
if (alt.isPresent() && alt.get() != component) {
//noinspection unchecked
component = (T) alt.get();
}
}
// Allow auto-complete providers to install behavior on text components
if (provider != null && component instanceof JTextComponent jc) {
provider.install(fieldPath, jc);
}
return component;
}
private void loadFromModel() {
Advancement.Display display = advancement.getDisplay();
if (display != null) {
Advancement.Icon icon = display.getIcon();
if (icon != null) {
if (icon.getId() != null) iconId.setText(icon.getId());
if (icon.getCount() != null) iconCount.setValue(icon.getCount());
}
if (display.getBackground() != null) background.setText(display.getBackground());
if (display.getFrame() != null) frame.setSelectedItem(display.getFrame());
if (display.getShowToast() != null) showToast.setSelected(display.getShowToast());
if (display.getAnnounceToChat() != null) announceToChat.setSelected(display.getAnnounceToChat());
if (display.getHidden() != null) hidden.setSelected(display.getHidden());
if (display.getTitle() != null) titleJson.setText(toJson(display.getTitle()));
if (display.getDescription() != null) descriptionJson.setText(toJson(display.getDescription()));
}
if (advancement.getSendsTelemetryEvent() != null) telemetry.setSelected(advancement.getSendsTelemetryEvent());
if (advancement.getCriteria() != null) criteriaJson.setText(toJson(advancement.getCriteria()));
if (advancement.getRequirements() != null) requirementsJson.setText(toJson(advancement.getRequirements()));
Advancement.Rewards rewards = advancement.getRewards();
if (rewards != null) {
if (rewards.getExperience() != null) exp.setValue(rewards.getExperience());
if (rewards.getFunction() != null) functionField.setText(rewards.getFunction());
if (rewards.getRecipes() != null) recipesJson.setText(toJson(rewards.getRecipes()));
if (rewards.getLoot() != null) lootJson.setText(toJson(rewards.getLoot()));
}
}
private void applyToModel() {
// Display
Advancement.Display display = Optional.ofNullable(advancement.getDisplay()).orElseGet(Advancement.Display::new);
Advancement.Icon icon = Optional.ofNullable(display.getIcon()).orElseGet(Advancement.Icon::new);
icon.setId(emptyToNull(iconId.getText()));
icon.setCount((Integer) iconCount.getValue());
display.setIcon(icon);
display.setBackground(emptyToNull(background.getText()));
display.setFrame((Advancement.Display.DisplayFrame) frame.getSelectedItem());
display.setShowToast(showToast.isSelected());
display.setAnnounceToChat(announceToChat.isSelected());
display.setHidden(hidden.isSelected());
// Title/Description as JSON components
display.setTitle(parseJsonValueNullable(titleJson.getText()));
display.setDescription(parseJsonValueNullable(descriptionJson.getText()));
advancement.setDisplay(display);
// Telemetry
advancement.setSendsTelemetryEvent(telemetry.isSelected());
// Criteria and Requirements
Type critType = new TypeToken<Map<String, Object>>() {}.getType();
advancement.setCriteria(parseJsonNullable(criteriaJson.getText(), critType));
Type reqType = new TypeToken<java.util.List<java.util.List<String>>>() {}.getType();
advancement.setRequirements(parseJsonNullable(requirementsJson.getText(), reqType));
// Rewards
Advancement.Rewards rewards = Optional.ofNullable(advancement.getRewards()).orElseGet(Advancement.Rewards::new);
rewards.setExperience((Integer) exp.getValue());
rewards.setFunction(emptyToNull(functionField.getText()));
Type arrStr = new TypeToken<java.util.List<String>>() {}.getType();
rewards.setRecipes(parseJsonNullable(recipesJson.getText(), arrStr));
rewards.setLoot(parseJsonNullable(lootJson.getText(), arrStr));
advancement.setRewards(rewards);
JOptionPane.showMessageDialog(this, "Applied changes to model.", "Advancement Editor", JOptionPane.INFORMATION_MESSAGE);
}
private String toJson(Object value) {
try {
JsonElement el = gson.toJsonTree(value);
return gson.toJson(el);
} catch (Exception e) {
return String.valueOf(value);
}
}
private <T> T parseJsonNullable(String text, Type type) {
String t = text == null ? "" : text.trim();
if (t.isEmpty()) return null;
try {
return gson.fromJson(t, type);
} catch (Exception e) {
// keep prior value if parse fails silently
return null;
}
}
private Object parseJsonValueNullable(String text) {
String t = text == null ? "" : text.trim();
if (t.isEmpty()) return null;
try {
return gson.fromJson(t, Object.class);
} catch (Exception e) {
return t; // fallback as plain string
}
}
private String emptyToNull(String s) {
if (s == null) return null;
String t = s.trim();
return t.isEmpty() ? null : t;
}
}

View file

@ -0,0 +1,26 @@
package com.ncguy.achievements.ui.editor.extensions;
import javax.swing.text.JTextComponent;
import java.util.List;
import java.util.Optional;
/**
* Extension point for providing auto-complete suggestions for text-based fields.
* This is a hook only; no implementation is provided here.
*/
public interface AutoCompleteProvider {
/**
* @param fieldPath dot-separated path to the field (e.g., "display.icon.id").
* @param component the text component to enhance with auto-complete.
* @return true if auto-complete behavior was installed; otherwise false.
*/
boolean install(String fieldPath, JTextComponent component);
/**
* Optional static suggestions for a field if install is not feasible (callers can render these externally).
*/
default Optional<List<String>> suggestions(String fieldPath, String currentInput) {
return Optional.empty();
}
}

View file

@ -0,0 +1,39 @@
package com.ncguy.achievements.ui.editor.extensions;
import javax.swing.JComponent;
import javax.swing.text.JTextComponent;
import java.util.Optional;
/**
* Extension point for providing type-aware editor customizations.
* Implementations can return specialized editors or converters for specific fields.
* This is a hook only; no default implementations are provided here.
*/
public interface TypeAdvisor {
/**
* @param fieldPath dot-separated path to the field within Advancement, e.g., "display.icon.id" or "rewards.experience".
* @param defaultEditor the default Swing editor component (typically a JTextField, JCheckBox, JSpinner, etc.).
* @return an alternative editor component to use instead of the default (if present). Implementations may wrap or
* decorate the default editor. Empty to use the provided default editor.
*/
Optional<JComponent> provideEditor(String fieldPath, JComponent defaultEditor);
/**
* Optionally supply a converter that can translate from the editor's text/value to a typed value and back.
* If not provided, the editor's raw value will be used.
*/
default Optional<ValueAdapter<?>> provideValueAdapter(String fieldPath) {
return Optional.empty();
}
/**
* Simple bidirectional conversion contract for editor values.
*/
interface ValueAdapter<T> {
/** Convert from raw editor value (often String) to typed value */
T parse(Object raw) throws Exception;
/** Convert typed value to editor-presentable form */
Object format(T value);
}
}

View file

@ -0,0 +1,29 @@
package com.ncguy.achievements.utils;
import java.lang.reflect.Field;
public class ReflectionUtils {
public static Field getField(Object obj, String fieldName) throws NoSuchFieldException {
Class<?> cls = obj.getClass();
try {
return cls.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
if(cls.getSuperclass() != Object.class)
return getField(cls.getSuperclass(), fieldName);
throw e;
}
}
public static <T> T get(Object obj, String fieldName) {
try {
Field field = getField(obj, fieldName);
field.setAccessible(true);
return (T) field.get(obj);
} catch (IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}
}

View file

@ -1,11 +0,0 @@
package com.ncguy.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Remarkable {
}

View file

@ -1,25 +0,0 @@
package com.ncguy.processors;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@SupportedAnnotationTypes("com.ncguy.annotations.Remarkable")
@SupportedSourceVersion(SourceVersion.RELEASE_21)
public class RemarkableProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}

View file

@ -1,7 +1,11 @@
import com.ncguy.usefulskyblock.generators.AdvancementGenerator
plugins {
id 'java'
id("xyz.jpenilla.run-paper") version "2.3.1"
id 'idea'
id 'org.jetbrains.kotlin.jvm'
id("xyz.jpenilla.run-paper") version "2.3.1"
id("net.devrieze.gradlecodegen") version "0.6.0"
}
group = 'com.ncguy'
@ -31,6 +35,8 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
testImplementation("com.google.code.gson:gson:2.13.1")
}
tasks {
@ -58,6 +64,18 @@ tasks.withType(JavaCompile).configureEach {
}
}
task generateAdvancements {
doLast {
AdvancementGenerator.generate(file("src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement"),
file("src/main/gen/com/ncguy/usefulskyblock/Advancements.java"))
}
}
task generateAll {
dependsOn += generateAdvancements
}
generate.dependsOn += generateAll
processResources {
def props = [version: version]
@ -70,3 +88,7 @@ processResources {
kotlin {
jvmToolchain(21)
}
File genSrc = file('src/main/gen')
sourceSets.main.java.srcDirs += genSrc
idea.module.generatedSourceDirs += genSrc

19
buildSrc/build.gradle Normal file
View file

@ -0,0 +1,19 @@
plugins {
id 'groovy-gradle-plugin'
}
repositories {
gradlePluginPortal()
maven {
name = "papermc-repo"
url = "https://repo.papermc.io/repository/maven-public/"
}
maven {
name = "sonatype"
url = "https://oss.sonatype.org/content/groups/public/"
}
}
dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.5-R0.1-SNAPSHOT")
}

View file

@ -0,0 +1,112 @@
package com.ncguy.usefulskyblock.generators;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class AdvancementGenerator {
public static void generate(File sourceDirectory, File output) throws IOException {
System.out.println("Generating advancements from " + sourceDirectory.toString());
System.out.println("Outputting to " + output.toString());
ClassBuilder builder = new ClassBuilder();
builder.packageName("com.ncguy.usefulskyblock").blankLine();
builder.importClass("org.bukkit.advancement.Advancement");
builder.importClass(java.util.Arrays.class);
builder.blankLine();
builder.newClass("Advancements");
File[] files = Objects.requireNonNull(sourceDirectory.listFiles());
for (File file : files) {
if (file.isDirectory()) {
generateGroup(file, builder);
}
}
builder.blankLine();
builder.openMethod("init", "void", Modifier.PUBLIC | Modifier.STATIC);
for (File file : files) {
if(file.isDirectory())
builder.functionCall(file.getName() + ".init");
}
builder.endMethod();
builder.openMethod("groups", "Advancement[][]", Modifier.PUBLIC | Modifier.STATIC);
builder.genericLine("Advancement[][] result = new Advancement[][] {");
List<String> groupNames = new ArrayList<>();
for (File file : files) {
if (!file.isDirectory())
continue;
builder.genericLine(" " + file.getName() + ".values,");
}
builder.genericLine("};");
builder.methodReturn("result");
builder.endMethod();
builder.openMethod("values", "Advancement[]", Modifier.PUBLIC | Modifier.STATIC);
builder.genericLine("return Arrays.stream(groups()).flatMap(Arrays::stream).toArray(Advancement[]::new);");
builder.endMethod();
builder.openMethod("getGroupName", "String", Modifier.PUBLIC | Modifier.STATIC, "int groupIdx");
ClassBuilder.SwitchBuilder groupSwitch = builder.switchCase("groupIdx");
for (int i = 0; i < files.length; i++)
groupSwitch.caseReturn(String.valueOf(i), "\"" + files[i].getName() + "\"");
groupSwitch.build();
builder.methodReturn("\"UNKNOWN_GROUP: \" + groupIdx");
builder.endMethod().blankLine();
builder.endClass();
output.getParentFile().mkdirs();
Files.writeString(output.toPath(), builder.build());
}
public static void generateGroup(File src, ClassBuilder builder) {
builder.newClass(src.getName(), Modifier.PUBLIC | Modifier.STATIC);
// Declaration
File[] files = Objects.requireNonNull(src.listFiles());
for (File file : files) {
String name = file.getName();
if (!name.endsWith(".json"))
continue;
String substring = name.substring(0, name.length() - 5);
builder.field("Advancement", substring.toUpperCase(), Modifier.PUBLIC | Modifier.STATIC);
}
builder.field("Advancement[]", "values", Modifier.PUBLIC | Modifier.STATIC);
// Definition
builder.blankLine();
builder.openMethod("init", "void", Modifier.PUBLIC | Modifier.STATIC);
List<String> advancementNames = new ArrayList<>();
for (File file : files) {
String name = file.getName();
if (!name.endsWith(".json"))
continue;
String substring = name.substring(0, name.length() - 5);
String key = src.getName() + "/" + substring;
advancementNames.add(substring.toUpperCase());
builder.memberAssign(substring.toUpperCase(), "new AdvancementRef(Reference.key(\"" + key + "\")).getActualAdvancement()");
}
builder.blankLine();
builder.memberAssign("values", "new Advancement[] {" + String.join(", ", advancementNames) + "}");
builder.endMethod().blankLine();
builder.endClass();
}
}

View file

@ -0,0 +1,243 @@
package com.ncguy.usefulskyblock.generators;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Optional;
import java.util.Stack;
public class ClassBuilder {
private StringBuilder sb = new StringBuilder();
private int scopeDepth = 0;
private boolean isOpenField = false;
private Stack<String> currentPath;
public ClassBuilder() {
currentPath = new Stack<>();
}
private void indent() {
sb.append("\t".repeat(Math.max(0, scopeDepth)));
}
public ClassBuilder blankLine() {
sb.append("\n");
return this;
}
public ClassBuilder packageName(String packageName) {
indent();
sb.append("package ").append(packageName).append(";\n");
return this;
}
public ClassBuilder importClass(Class<?> cls) {
return importClass(cls.getName());
}
public ClassBuilder importClass(String cls) {
indent();
sb.append("import ").append(cls).append(";\n");
return this;
}
public ClassBuilder newClass(String name) {
return newClass(name, Modifier.PUBLIC);
}
public ClassBuilder newClass(String name, int modifiers) {
indent();
sb.append(Modifier.toString(modifiers)).append(" class ").append(name).append(" {\n");
scopeDepth++;
currentPath.push(name);
return this;
}
public ClassBuilder field(String type, String name) {
return field(type, name, Modifier.PUBLIC);
}
public ClassBuilder genericLine(String line) {
indent();
sb.append(line).append("\n");
return this;
}
public ClassBuilder field(String type, String name, int modifiers) {
openField(type, name, modifiers);
isOpenField = false;
sb.append(";\n");
return this;
}
public ClassBuilder openField(String type, String name) {
return openField(type, name, Modifier.PUBLIC);
}
public ClassBuilder openField(String type, String name, int modifiers) {
indent();
sb.append(Modifier.toString(modifiers)).append(" ").append(type).append(" ").append(name);
isOpenField = true;
return this;
}
public ClassBuilder withValue(String value) {
if(!isOpenField)
throw new IllegalStateException("Cannot set value on a field that is not open");
sb.append(" = ").append(value);
isOpenField = false;
return terminateStatement();
}
public ClassBuilder terminateStatement() {
sb.append(";\n");
return this;
}
public ClassBuilder endClass() {
endScope();
currentPath.pop();
return this;
}
public ClassBuilder endScope() {
scopeDepth--;
indent();
sb.append("}\n");
return this;
}
public String build() {
if(isOpenField) {
terminateStatement();
System.out.println("Dangling field");
}
if(scopeDepth > 0)
System.out.println("Dangling scope of depth " + scopeDepth);
while(scopeDepth > 0)
endScope();
return sb.toString();
}
public ClassBuilder openMethod(String name, String returnType, int modifiers, String... params) {
indent();
sb.append(Modifier.toString(modifiers)).append(" ").append(returnType).append(" ").append(name).append("(");
Arrays.stream(params).reduce((a, b) -> a + ", " + b).ifPresent(sb::append);
sb.append(") {\n");
scopeDepth++;
currentPath.push(name);
return this;
}
public ClassBuilder memberAssign(String member, String value) {
Optional<String> cls = currentPath.stream().limit(currentPath.size() - 1).reduce((a, b) -> a + "." + b);
indent();
sb.append(cls.orElse("UNKNOWN_CLASS_GENERATE_COMPILE_ERROR")).append(".").append(member).append(" = ").append(value).append(";\n");
return this;
}
public ClassBuilder methodReturn() {
indent();
sb.append("return;\n");
return this;
}
public ClassBuilder methodReturn(String variable) {
indent();
sb.append("return ").append(variable).append(";\n");
return this;
}
public ClassBuilder endMethod() {
scopeDepth--;
indent();
sb.append("}\n");
currentPath.pop();
return blankLine();
}
public ClassBuilder functionCall(String name) {
indent();
sb.append(name).append("();\n");
return this;
}
public SwitchBuilder switchCase(String variable) {
return new SwitchBuilder(this, scopeDepth).start(variable);
}
public static class SwitchBuilder {
private ClassBuilder builder;
private StringBuilder sb;
private int depth;
private boolean isOpen;
public SwitchBuilder(ClassBuilder builder, int initialDepth) {
sb = new StringBuilder();
depth = initialDepth;
this.builder = builder;
}
protected void pushScope() {
depth++;
}
protected void popScope() {
depth--;
}
private void indent() {
sb.append("\t".repeat(Math.max(0, depth)));
}
public SwitchBuilder start(String variable) {
indent();
isOpen = true;
sb.append("switch(").append(variable).append(") {\n");
pushScope();
return this;
}
public SwitchBuilder caseReturn(String value, String ret) {
return this.caseStatement(value).returnCase(ret);
}
public SwitchBuilder caseStatement(String value) {
indent();
sb.append("case ").append(value).append(": ");
pushScope();
return this;
}
public SwitchBuilder returnCase(String res) {
sb.append("return ").append(res).append(";\n");
popScope();
return this;
}
public SwitchBuilder breakCase() {
sb.append("break;\n");
popScope();
return this;
}
public SwitchBuilder end() {
isOpen = false;
popScope();
indent();
sb.append("}\n");
return this;
}
public ClassBuilder build() {
if(isOpen)
this.end();
builder.genericLine(sb.toString());
return builder;
}
}
}

118
outline.txt Normal file
View file

@ -0,0 +1,118 @@
Tasks (actionable steps) Day 1 setup and safety
- [ ] Preserve starting flora: harvest grass/flowers to secure at least one seed; leave a few grass blocks for spread.
- [ ] Reposition dirt: place dirt around the starter tree to catch saplings.
- [ ] Basic tools and bench: craft a crafting table and wooden pickaxe, then move to stone tools.
- [ ] Cobblestone generator: build a safe, non-flammable design and begin mining.
- [ ] Shelter and spawn-proofing: build a small shelter and light the area to reduce hostile spawns and phantoms.
- [ ] Inventory audit: check the starter chest (crops, obsidian, lava, ice, turtle eggs if present) and plan usage.
Early farming and resources
- [ ] Tree loop: replant saplings promptly; expand the platform to improve drop capture.
- [ ] Crop start: plant starting crops (wheat/melon/pumpkin/sugar cane) near water.
- [ ] Melon prep: craft melon seeds if you have a melon slice; plant and manage stems.
- [ ] Early food: collect apples from oak; supplement with fish once you have string.
Mob farming and animal access
- [ ] Hostile mob farm (low-tech): build a waterless/pathfinding drop-shaft farm to obtain bones, string, rotten flesh, and occasional iron/redstone from mobs.
- [ ] Fishing unlock: use string from spiders to craft a fishing rod; fish in your generators water channel or a small pool.
- [ ] Passive mob platform: bridge well away from your island, place grass, and create pens to capture spawned animals.
Crop progression and bone meal
- [ ] Bone meal loop: convert skeleton bones to bone meal to accelerate crops and tree growth.
- [ ] Crop diversity: add potato and carrot farming from mob drops; use carrots to lure/breed pigs.
- [ ] Sugar cane line: expand near water for paper/trade potential.
Water acquisition and duplication
- [ ] Ice to water: break ice to create flowing water.
- [ ] Infinite source: create a 2×2 infinite water pool using two sources (or two ice blocks).
- [ ] Rain capture: place a cauldron under open sky to gather water slowly; bucket it into an infinite source once you have two buckets worth.
- [ ] Underwater bonemeal trick: use bone meal in water to create seagrass and generate source blocks (where mechanics allow).
Villager acquisition and utility
- [ ] Secure a zombie villager: isolate one from your hostile farm or a night spawn.
- [ ] Weakness application (no brewing path): bait a witch to throw Weakness at the zombie villager by staying close and with low health.
- [ ] Weakness application (brewing path): obtain a brewing stand (blackstone/cobblestone), then brew a splash potion of Weakness.
- [ ] Golden apple prep: acquire gold (Nether piglins, smelt mob-dropped armor, or drowned—version dependent) and craft a golden apple.
- [ ] Cure and protect: cure the zombie villager and secure it; then build a basic villager breeder.
- [ ] Trading starter: set up initial workstations (e.g., farmer/librarian/cleric) for emeralds, gear, and books.
Iron farming
- [ ] Minimal iron farm: assemble 3 villagers, 3 beds, and a non-despawning zombie (holding an item); build a safe kill chamber.
- [ ] Scale-out: duplicate the farm or upgrade design to increase iron per hour as needs grow.
Nether progression and farms
- [ ] Portal access: light a Nether portal (using provided obsidian or lava/water methods as appropriate).
- [ ] Biome-targeted farms:
- Nether Wastes: gold from zombified piglins (simple roofed spawning pad).
- Crimson Forest: hoglin farm for pork.
- Warped Forest: enderman farm for pearls.
- Basalt Deltas: magma cube farm for magma cream.
- Soul Sand Valley: ghast farm for tears and additional gunpowder.
- [ ] Piglin bartering post: trade gold for utility items (blackstone, crying obsidian, gravel, quartz, soul sand).
- [ ] Fortress utilization: locate a Nether fortress in the void; collect blaze rods, bones, coal, soul sand, and wither skulls.
Wither and late utility
- [ ] Wither prep and fight: obtain three skulls and soul sand; defeat the Wither safely; craft a beacon.
Goals (milestones) Foundational milestones
- [ ] Establish a reliable cobblestone generator and upgrade to full stone tools.
- [ ] Secure renewable food sources (crops and/or fishing).
- [ ] Build a functioning hostile mob farm yielding bones and string consistently.
- [ ] Create an infinite water source using any valid method available to your map/version.
- [ ] Acquire your first animals via a passive mob platform.
Villager and economy milestones
- [ ] Cure your first zombie villager and protect them.
- [ ] Build a villager breeder and produce multiple villagers.
- [ ] Open a trading loop that generates emeralds steadily.
- [ ] Obtain enchanted gear (tools/armor/books) through villager trades.
Metal and automation milestones
- [ ] Construct your first iron farm and achieve steady iron income.
- [ ] Scale your iron production to meet tool/automation demands.
Nether milestones
- [ ] Activate and safely use a Nether portal.
- [ ] Build a basic gold farm and unlock piglin bartering.
- [ ] Acquire key bartering outputs (blackstone, gravel, quartz, crying obsidian, soul sand).
- [ ] Locate and utilize a Nether fortress for blaze rods and skulls.
Combat and power milestones
- [ ] Craft a brewing stand and brew splash Weakness potions.
- [ ] Defeat the Wither and place your first beacon.
Challenges (optional constraints or variants) Resource and progression constraints
- [ ] Waterless start: reach infinite water using only rain-in-cauldron collection.
- [ ] No-redstone early: construct a fully passive/pathfinding-based hostile mob farm.
- [ ] No-bone-meal: grow crops and trees without bone meal until Nether access.
- [ ] No-fishing: establish sustainable food without fishing.
- [ ] No-villager-trading until iron: reach an operational iron farm before any trades.
Villager-specific constraints
- [ ] Witch-only cure: cure your first villager via a witchs Weakness potion (no brewing).
- [ ] No-Nether cure: obtain a golden apple and cure a villager without entering the Nether (relying on armor smelts/drowned for gold; version dependent).
Mob farm and safety constraints
- [ ] Spiderless farm: design a hostile farm that prevents spider spawns while keeping rates high.
- [ ] Phantom-proof base: eliminate phantom risk via shelter design and lighting without relying on frequent sleep.
- [ ] Zero fall deaths: complete early bridging/expansion without falling into the void.
Nether farming variants
- [ ] Biome mastery: build one working farm in each Nether biome (piglin, hoglin, enderman, magma cube, ghast).
- [ ] Minimal gold farm: produce bartering-grade gold using a simple low-material pad (no turtle eggs or complex baiting).
Iron farm constraints
- [ ] Lava-free iron farm: use fall damage or other non-lava methods for the kill chamber.
- [ ] Minimal villagers: keep iron farm to the smallest villager count while maintaining uptime.
Version-aware constraints
- [ ] Copper-era start: if drowned drop copper (not gold), reach your first golden apple via alternative gold paths.
- [ ] Ice-free start: obtain infinite water without ice blocks present.
Collection and completionist variants
- [ ] Barter unlock list: obtain each notable bartering item at least once.
- [ ] Fortress-only advance: obtain blaze rods and wither skulls before building non-Nether automated farms.
- [ ] Farm trifecta: operate simultaneous farms for bones, string, and gunpowder using low-tech designs.
Use these as a menu: pick Tasks to guide immediate actions, track progress with Goals, and spice up playthroughs using Challenges tailored to your map/version.

View file

@ -8,4 +8,5 @@ plugins {
}
rootProject.name = 'UsefulSkyblock'
include 'annotation-processor'
include(':achievement-crafter')

View file

@ -0,0 +1,76 @@
package com.ncguy.usefulskyblock;
import io.papermc.paper.advancement.AdvancementDisplay;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementRequirements;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.Collection;
import java.util.Objects;
public class AdvancementRef implements Advancement {
private final NamespacedKey key;
private Advancement advancement;
public AdvancementRef(NamespacedKey key) {
this.key = key;
}
public Advancement getActualAdvancement() {
if (advancement == null)
advancement = Objects.requireNonNull(Bukkit.getAdvancement(key), "Advancement " + key + " not found");
return advancement;
}
@Override
public @NotNull Collection<String> getCriteria() {
return getActualAdvancement().getCriteria();
}
@Override
public @NotNull AdvancementRequirements getRequirements() {
return getActualAdvancement().getRequirements();
}
@Override
public @Nullable AdvancementDisplay getDisplay() {
return getActualAdvancement().getDisplay();
}
@Override
public @NotNull Component displayName() {
return getActualAdvancement().displayName();
}
@Override
public @Nullable Advancement getParent() {
return getActualAdvancement().getParent();
}
@Override
public @NotNull @Unmodifiable Collection<Advancement> getChildren() {
return getActualAdvancement().getChildren();
}
@Override
public @NotNull Advancement getRoot() {
return getActualAdvancement().getRoot();
}
@Override
public @NotNull NamespacedKey getKey() {
return getActualAdvancement().getKey();
}
@Override
public @NotNull Key key() {
return getActualAdvancement().key();
}
}

View file

@ -0,0 +1,244 @@
package com.ncguy.usefulskyblock;
import com.ncguy.usefulskyblock.data.BiomedStructure;
import com.ncguy.usefulskyblock.utils.MathsUtils;
import org.bukkit.*;
import org.bukkit.block.Biome;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.structure.Mirror;
import org.bukkit.block.structure.StructureRotation;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.structure.Structure;
import org.bukkit.util.Vector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.function.Supplier;
public class IslandNetworkGenerator {
private static final Logger log = LoggerFactory.getLogger(IslandNetworkGenerator.class);
public static class TierDefinition {
public int tier;
public int slotCount;
public float spacing;
public StructureRef[] structures;
public StructureRotation[] validRotations;
public Mirror[] validMirrors;
public Supplier<Float> yNoise;
public TierDefinition(int tier, int slotCount, float spacing, StructureRef[] structures, boolean canRotate, boolean canMirror, Supplier<Float> yNoise) {
this.tier = tier;
this.slotCount = slotCount;
this.spacing = spacing;
this.structures = structures;
this.yNoise = yNoise;
if (canRotate)
validRotations = StructureRotation.values();
else
validRotations = new StructureRotation[]{StructureRotation.NONE};
if (canMirror)
validMirrors = Mirror.values();
else
validMirrors = new Mirror[]{Mirror.NONE};
}
public TierDefinition(int tier, int slotCount, float spacing, StructureRef[] structures, StructureRotation[] validRotations, Mirror[] validMirrors, Supplier<Float> yNoise) {
this.tier = tier;
this.slotCount = slotCount;
this.spacing = spacing;
this.structures = structures;
this.validRotations = validRotations;
this.validMirrors = validMirrors;
this.yNoise = yNoise;
}
public TierDefinition(int tier, int slotCount, float spacing, StructureRef[] structures, boolean canRotate, boolean canMirror) {
this(tier, slotCount, spacing, structures, canRotate, canMirror, () -> 0f);
}
public TierDefinition(int tier, int slotCount, float spacing, StructureRef[] structures, Supplier<Float> yNoise) {
this(tier, slotCount, spacing, structures, true, true, yNoise);
}
public TierDefinition(int tier, int slotCount, float spacing, StructureRef[] structures) {
this(tier, slotCount, spacing, structures, true, true);
}
}
public static class Definition {
public TierDefinition[] tiers;
}
private void generateChunks(World world, Queue<Location> chunkCoords, Runnable onComplete) {
world.getChunkAtAsync(chunkCoords.remove(), true).thenAccept(chunk -> {
if(chunkCoords.isEmpty()) {
onComplete.run();
return;
}
generateChunks(world, chunkCoords, onComplete);
});
}
private Location placeStructureAtLocation(Structure structure, Location loc, StructureRotation[] validRotations, Mirror[] validMirrors) {
StructureRotation rotation = randomElement(validRotations);
Mirror mirror = randomElement(validMirrors);
int xRange, yRange, zRange;
World world = loc.getWorld();
if((structure instanceof StructureRef ref) && ref.getActualStructure() == null) {
log.info("Trying to place structure {}", ref.name);
Location finalLoc = loc;
final StructureRef refCopy = ref;
int minX = loc.getBlockX() - 256;
int minZ = loc.getBlockZ() - 256;
int maxX = loc.getBlockX() + 256;
int maxZ = loc.getBlockZ() + 256;
Queue<Location> chunkCoords = new LinkedList<>();
for(int x = minX; x <= maxX; x += 16) {
for(int z = minZ; z <= maxZ; z += 16) {
chunkCoords.add(new Location(world, x, 0, z));
}
}
generateChunks(world, chunkCoords, () -> {
String cmd = String.format("execute in %s run place structure %s %d %d %d",
world.getKey().asString(), refCopy.name, finalLoc.getBlockX(), finalLoc.getBlockY(), finalLoc.getBlockZ());
log.info("Executing command {}", cmd);
int foundationRadius = 16;
List<Location> foundationLocations = new ArrayList<>();
for(int x = -foundationRadius; x <= foundationRadius; x++) {
for(int z = -foundationRadius; z <= foundationRadius; z++) {
Location fLoc = finalLoc.clone().add(x, -2, z);
fLoc.getBlock().setType(Material.SMOOTH_SANDSTONE_STAIRS, false);
foundationLocations.add(fLoc);
}
}
if(!Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd))
log.error("Cannot place structure \"{}\"", refCopy.name);
else
log.info("Structure {} placed at {}", refCopy.name, finalLoc);
Bukkit.getScheduler().runTaskLater(JavaPlugin.getProvidingPlugin(getClass()), () -> {
for (Location fLoc : foundationLocations) {
Block block = fLoc.getBlock();
if(block.getType() == Material.SMOOTH_SANDSTONE_STAIRS)
block.setType(Material.AIR, false);
}
}, 1);
});
// world.getChunkAtAsync(loc).thenAccept(chunk -> {
// });
// TODO Make this respect the underlying structure as best as possible
xRange = yRange = zRange = 8;
} else {
Vector extents = structure.getSize().clone().multiply(0.5);
loc.subtract(extents);
structure.place(loc, true, rotation, mirror, 0, 1, new Random());
xRange = structure.getSize().getBlockX();
yRange = structure.getSize().getBlockY();
zRange = structure.getSize().getBlockZ();
}
Optional<BlockState> bedrock = Optional.empty();
Biome newBiome = null;
boolean hasNewBiome = false;
if(structure instanceof BiomedStructure) {
newBiome = ((BiomedStructure) structure).biome;
hasNewBiome = true;
}
Set<Chunk> chunksToUpdate = new HashSet<>();
log.trace("Start search for bedrock, range {}x{}x{}", xRange, yRange, zRange);
for (int x = loc.getBlockX() - xRange; x < loc.getBlockX() + xRange; x++) {
for (int y = loc.getBlockY() - yRange; y < loc.getBlockY() + yRange; y++) {
for (int z = loc.getBlockZ() - zRange; z < loc.getBlockZ() + zRange; z++) {
Block b = world.getBlockAt(x, y, z);
if (b.getType() == Material.BEDROCK || b.getType() == Material.LODESTONE) {
System.out.println("Found " + b.getType().name() + " in placed structure, using that as centre");
bedrock = Optional.of(b.getState());
}
if(hasNewBiome) {
world.setBiome(x, y, z, newBiome);
Chunk chunkAt = world.getChunkAt(x, z);
chunksToUpdate.add(chunkAt);
}
}
}
}
if(hasNewBiome) {
log.trace("Refreshing {} chunks", chunksToUpdate.size());
chunksToUpdate.forEach(c -> c.getWorld().refreshChunk(c.getX(), c.getZ()));
}
if (bedrock.isPresent()) {
loc = bedrock.get().getLocation();
loc.add(0, 2, 0);
}
return loc;
}
private <T extends Enum<?>> T randomEnum(Class<T> enumCls) {
assert (enumCls.isEnum());
return randomElement(enumCls.getEnumConstants());
}
private <T> T randomElement(T[] array) {
int idx = (int) (Math.random() * array.length);
return array[idx];
}
public Location generateIslandNetwork(Location origin, Definition definition) {
Location centralIslandSpawnLoc = origin.clone();
TierDefinition[] tiers = definition.tiers;
for (int i = 0; i < tiers.length; i++) {
TierDefinition tier = tiers[i];
if(tier.tier == 0) {
centralIslandSpawnLoc = placeStructureAtLocation(randomElement(tier.structures), centralIslandSpawnLoc, tier.validRotations, tier.validMirrors);
continue;
}
var slots = MathsUtils.sampleUniqueInts(tier.slotCount, tier.structures.length);
var step = 360.0 / tier.slotCount;
placeIslandSets(origin, tier.structures, tier.spacing, slots, step, tier.validRotations, tier.validMirrors, tier.yNoise);
}
return centralIslandSpawnLoc;
}
public void placeIslandSets(Location origin, StructureRef[] islands, float islandSpacing, int[] slots, double step, StructureRotation[] validRotations, Mirror[] validMirrors, Supplier<Float> yNoiseFunc) {
for (int i = 0; i < islands.length; i++) {
StructureRef island = islands[i];
int slot = slots[i];
double angle = step * slot;
double x = Math.cos(angle) * islandSpacing;
double z = Math.sin(angle) * islandSpacing;
double y = yNoiseFunc.get();
Location pos = origin.clone().add(x, y, z);
placeStructureAtLocation(island, pos, validRotations, validMirrors);
}
}
}

View file

@ -0,0 +1,32 @@
package com.ncguy.usefulskyblock;
import com.ncguy.usefulskyblock.pdc.AnonymousDataContainerRef;
import com.ncguy.usefulskyblock.pdc.IDataContainerRef;
import com.ncguy.usefulskyblock.pdc.VectorPersistentDataType;
import io.papermc.paper.datacomponent.DataComponentType;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.util.BlockVector;
import org.jetbrains.annotations.NotNull;
public class Reference {
public static NamespacedKey key(String key) {
// return new NamespacedKey(JavaPlugin.getProvidingPlugin(Reference.class), key);
return new NamespacedKey("usefulskyblock", key);
}
public static IDataContainerRef<BlockVector, World> SKYBLOCK_TEAM_ROOT = new AnonymousDataContainerRef<>(key("world.skyblock.team"), VectorPersistentDataType.Instance);
public static IDataContainerRef<Integer, World> SKYBLOCK_TEAM_COUNT = SKYBLOCK_TEAM_ROOT.withSuffix(".count").withType(PersistentDataType.INTEGER);
public static IDataContainerRef<Boolean, World> WORLD_INIT = new AnonymousDataContainerRef<>(key("world.init"), PersistentDataType.BOOLEAN);
public static IDataContainerRef<BlockVector, Player> ISLAND_HOME_LOC = new AnonymousDataContainerRef<>(key("player.island.home.loc"), VectorPersistentDataType.Instance);
public static IDataContainerRef<Boolean, Item> ITEM_LAVA_IMMUNE = new AnonymousDataContainerRef<>(key("item.lava.immune"), PersistentDataType.BOOLEAN);
}

View file

@ -0,0 +1,150 @@
package com.ncguy.usefulskyblock;
import com.ncguy.usefulskyblock.events.TeamProgressionEvent;
import com.ncguy.usefulskyblock.pdc.IDataContainerRef;
import com.ncguy.usefulskyblock.pdc.VectorPersistentDataType;
import com.ncguy.usefulskyblock.utils.TextUtils;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.advancement.Advancement;
import org.bukkit.entity.Player;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.scoreboard.Team;
import static com.ncguy.usefulskyblock.Reference.SKYBLOCK_TEAM_ROOT;
public class SkyblockTeam {
// Once deployed, the values of these can never change
public static enum Tiers {
TIER_0(0),
TIER_1(1),
TIER_2(1 << 1),
TIER_3(1 << 2),
NETHER_TIER_1(1 << 8),
NETHER_TIER_2(1 << 9),
NETHER_TIER_3(1 << 10),
;
Tiers(long value) {
this.value = value;
this.displayName = TextUtils.toTitleCase(name());
}
private final long value;
private final String displayName;
public String displayName() {
return displayName;
}
public boolean isSet(long mask) {
return (mask & value) == value;
}
public long set(long mask) {
return mask | value;
}
public long unset(long mask) {
return mask & ~value;
}
}
private final NamespacedKey worldKey = new NamespacedKey("minecraft", "overworld");
private final World world;
private final Team team;
private IDataContainerRef<Location, World> homeIslandLoc;
private IDataContainerRef<Long, World> tierProgress;
public SkyblockTeam(Team team) {
this.team = team;
this.world = Bukkit.getWorld(worldKey);
String sanitizedTeamName = TextUtils.sanitizeTeamName(team);
var teamRoot = SKYBLOCK_TEAM_ROOT.withSuffix("." + sanitizedTeamName);
this.homeIslandLoc = teamRoot.withType(VectorPersistentDataType.Instance)
.assign(world)
.map(x -> new Location(world, x.getX(), x.getY(), x.getZ()), loc -> loc.toVector().toBlockVector());
this.tierProgress = teamRoot
.withSuffix(".progress")
.withType(PersistentDataType.LONG)
.assign(world);
}
public Location getHomeIslandLoc() {
return this.homeIslandLoc.get();
}
public Team getTeam() {
return team;
}
public static Tiers isUnlockingAdvancement(Advancement adv) {
Key key = adv.key();
if (key == Advancements.skyblock.SKYBLOCK_BEGIN.key())
return Tiers.TIER_1;
if (key == Advancements.skyblock.GET_COBBLE.key())
return Tiers.TIER_2;
if (key == Advancements.skyblock.VOID_BIN.key())
return Tiers.TIER_3;
return null;
}
public void tryUnlockTier(Tiers unlockingTier) {
if (unlockingTier == Tiers.TIER_0)
return;
unlockTier(unlockingTier);
}
public long getCurrentTier() {
return tierProgress.getOrDefault(Tiers.TIER_0.value);
}
public boolean unlockTier(Tiers tier) {
if(tier.isSet(tierProgress.getOrDefault(0L))) {
return false;
}
new TeamProgressionEvent(team, tier).callEvent();
tierProgress.set(tier.set(tierProgress.getOrDefault(0L)));
return true;
}
public boolean isAdvancementComplete(Advancement adv) {
for (String entry : team.getEntries()) {
Player player = Bukkit.getPlayer(entry);
if (player == null) continue;
if (player.getAdvancementProgress(adv).isDone())
return true;
}
return false;
}
// Helpers
public void broadcast(ComponentLike text) {
for (Audience a : team.audiences()) {
a.sendMessage(text);
}
}
}

View file

@ -33,16 +33,18 @@ public class StructureRef implements Structure {
this.name = name;
}
private Structure getActualStructure() {
public Structure getActualStructure() {
if (actualStructure == null) {
StructureManager structureManager = Bukkit.getStructureManager();
actualStructure = structureManager.loadStructure(name);
if(actualStructure == null) {
try (InputStream resourceAsStream = getClass().getResourceAsStream("/datapacks/usefulskyblock/data/" + name.getNamespace() + "/structures/" + name.getKey() + ".nbt")) {
actualStructure = structureManager.loadStructure(Objects.requireNonNull(resourceAsStream));
actualStructure = structureManager.loadStructure(resourceAsStream);
structureManager.registerStructure(name, actualStructure);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException | IOException e) {
e.printStackTrace();
return null;
}
}
}

View file

@ -1,66 +1,67 @@
package com.ncguy.usefulskyblock;
import com.destroystokyo.paper.event.player.PlayerPostRespawnEvent;
import com.ncguy.usefulskyblock.events.SkyblockConfigReloadEvent;
import com.ncguy.usefulskyblock.handlers.*;
import com.ncguy.usefulskyblock.recipe.BiomeRod;
import com.ncguy.usefulskyblock.utils.Remark;
import com.ncguy.usefulskyblock.utils.RemarkSet;
import com.ncguy.usefulskyblock.recipe.IRecipeProvider;
import com.ncguy.usefulskyblock.recipe.SmeltingCraftingHandler;
import com.ncguy.usefulskyblock.world.PortalHandler;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.structure.Mirror;
import org.bukkit.block.structure.StructureRotation;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.*;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.server.ServerLoadEvent;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.inventory.Recipe;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Random;
import java.util.Iterator;
public final class UsefulSkyblock extends JavaPlugin implements Listener {
private static final Logger log = LoggerFactory.getLogger(UsefulSkyblock.class);
private BiomeRod biomeRodListener;
@Override
public void onEnable() {
System.out.println("OM EMABL:ED");
saveDefaultConfig();
// Plugin startup logic
BiomeRod biomeRod = new BiomeRod();
SmeltingCraftingHandler smeltingCraftingHandler = new SmeltingCraftingHandler();
IRecipeProvider[] recipeProviders = new IRecipeProvider[]{
biomeRod,
smeltingCraftingHandler
};
PluginManager pluginManager = Bukkit.getPluginManager();
pluginManager.registerEvents(this, this);
pluginManager.registerEvents(new PortalHandler(), this);
biomeRodListener = new BiomeRod();
pluginManager.registerEvents(biomeRodListener, this);
BiomeRod.Init(Bukkit.getServer());
pluginManager.registerEvents(biomeRod, this);
pluginManager.registerEvents(new CauldronCraftingHandler(), this);
pluginManager.registerEvents(new ItemEventHandler(), this);
pluginManager.registerEvents(new InitialisationHandler(), this);
pluginManager.registerEvents(new TeamProgressHandler(), this);
pluginManager.registerEvents(new FishingHandler(), this);
Server server = Bukkit.getServer();
for (int i = recipeProviders.length-1; i >= 0; i--) {
IRecipeProvider provider = recipeProviders[i];
Iterable<Recipe> recipes = provider.provideRecipes();
Iterator<Recipe> iterator = recipes.iterator();
boolean isLastProvider = i == 0;
while (iterator.hasNext())
server.addRecipe(iterator.next(), isLastProvider && !iterator.hasNext());
}
Advancements.init();
new SkyblockConfigReloadEvent(getConfig()).callEvent();
}
@Override
public void reloadConfig() {
super.reloadConfig();
if(biomeRodListener == null)
return;
RemarkSet remarks = biomeRodListener.reloadBiomeMap();
for (Remark remark : remarks) {
Bukkit.getServer().broadcast(Component.text(remark.domain, Style.style(TextColor.fromHexString("#777"))).appendSpace().append(Component.text(remark.message, Style.style(TextColor.fromHexString("#fff")))));
}
new SkyblockConfigReloadEvent(getConfig()).callEvent();
}
@Override
@ -68,82 +69,4 @@ public final class UsefulSkyblock extends JavaPlugin implements Listener {
// Plugin shutdown logic
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
player.sendMessage(Component.text("Hello, " + player.getName() + "!"));
PersistentDataContainer pdc = player.getPersistentDataContainer();
NamespacedKey initKey = new NamespacedKey(this, "player_init");
// if (pdc.has(initKey))
// return;
NamespacedKey worldKey = new NamespacedKey(this, "void");
World world = Bukkit.getWorld(worldKey);
player.teleport(world.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
player.setRespawnLocation(world.getSpawnLocation());
pdc.set(initKey, PersistentDataType.BOOLEAN, true);
player.sendMessage(Component.text("This is your first time playing on this server."));
}
@EventHandler
public void onPlayerPostRespawn(PlayerPostRespawnEvent event) {
Player player = event.getPlayer();
player.sendMessage(Component.text("Hello, " + player.getName() + "!"));
PersistentDataContainer pdc = player.getPersistentDataContainer();
NamespacedKey initKey = new NamespacedKey(this, "player_init");
// if (pdc.has(initKey))
// return;
NamespacedKey worldKey = new NamespacedKey(this, "void");
World world = Bukkit.getWorld(worldKey);
player.teleport(world.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
player.setRespawnLocation(world.getSpawnLocation());
pdc.set(initKey, PersistentDataType.BOOLEAN, true);
player.sendMessage(Component.text("This is your first time playing on this server."));
}
@EventHandler
public void onServerLoad(ServerLoadEvent event) {
if(event.getType() != ServerLoadEvent.LoadType.STARTUP)
return;
World world = Bukkit.getWorld(new NamespacedKey(this, "void"));
if (world == null)
return;
PersistentDataContainer pdc = world.getPersistentDataContainer();
NamespacedKey initKey = new NamespacedKey(this, "world_init");
if(pdc.has(initKey))
return;
pdc.set(initKey, PersistentDataType.BOOLEAN, true);
log.info("Generating spawn point for world {}", world.getName());
StructureRef spawn = new StructureRef(new NamespacedKey(this, "spawn"));
log.info("Entities in spawn: {}", spawn.getEntities().size());
log.info("Palettes in spawn: {}", spawn.getPalettes().size());
log.info("Palette count in spawn: {}", spawn.getPaletteCount());
log.info("spawn.getSize(): {}", spawn.getSize());
spawn.place(new Location(world, 0, 64, 0), true, StructureRotation.NONE, Mirror.NONE, 0, 1, new Random(System.currentTimeMillis()));
world.setSpawnLocation(0, 64, 0);
}
@EventHandler
public void onWorldLoad(WorldLoadEvent event) {
World world = event.getWorld();
if (!world.getKey().equals(new NamespacedKey(this, "void")))
return;
PersistentDataContainer pdc = world.getPersistentDataContainer();
NamespacedKey initKey = new NamespacedKey(this, "world_init");
if(pdc.has(initKey))
return;
pdc.set(initKey, PersistentDataType.BOOLEAN, true);
log.info("Generating spawn point for world {}", world.getName());
StructureRef spawn = new StructureRef(new NamespacedKey(this, "spawn"));
spawn.place(new Location(world, 0, 64, 0), true, StructureRotation.NONE, Mirror.NONE, 0, 1, new Random(System.currentTimeMillis()));
world.setSpawnLocation(0, 64, 0);
}
}

View file

@ -1,5 +1,6 @@
package com.ncguy.usefulskyblock.command;
import com.ncguy.usefulskyblock.Reference;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.Server;
@ -33,11 +34,7 @@ public abstract class AbstractSkyblockCommand {
}
protected NamespacedKey key(String name) {
return new NamespacedKey("usefulskyblock", name);
}
protected NamespacedKey overworldKey = key("void");
protected NamespacedKey overworldKey = Reference.key("void");
private Server serverInstance;
protected Server getServer() {

View file

@ -4,17 +4,23 @@ import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.LiteralCommandNode;
import com.ncguy.usefulskyblock.Reference;
import com.ncguy.usefulskyblock.SkyblockTeam;
import com.ncguy.usefulskyblock.StructureRef;
import com.ncguy.usefulskyblock.handlers.InitialisationHandler;
import com.ncguy.usefulskyblock.utils.BoxVisualizer;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.Commands;
import io.papermc.paper.command.brigadier.argument.CustomArgumentType;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import org.bukkit.*;
import org.bukkit.block.structure.Mirror;
import org.bukkit.block.structure.StructureRotation;
import org.bukkit.entity.Entity;
@ -22,6 +28,9 @@ import org.bukkit.entity.Player;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.ScoreboardManager;
import org.bukkit.scoreboard.Team;
import org.bukkit.structure.Structure;
import org.bukkit.structure.StructureManager;
import org.bukkit.util.RayTraceResult;
@ -32,242 +41,343 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
public class SkyblockAdminCommand extends AbstractSkyblockCommand {
private static final Logger log = LoggerFactory.getLogger(SkyblockAdminCommand.class);
private static final Logger log = LoggerFactory.getLogger(SkyblockAdminCommand.class);
private BoxVisualizer box = new BoxVisualizer();
private BoxVisualizer box = new BoxVisualizer();
public int executeStructuresList(CommandContext<CommandSourceStack> ctx) {
public int executeStructuresList(CommandContext<CommandSourceStack> ctx) {
StructureManager structureManager = getServer().getStructureManager();
Map<NamespacedKey, Structure> structureMap = structureManager.getStructures();
StructureManager structureManager = getServer().getStructureManager();
Map<NamespacedKey, Structure> structureMap = structureManager.getStructures();
// Set<NamespacedKey> filteredKeys = structureMap.keySet().stream().filter(k -> k.getNamespace() == "usefulskyblock").collect(Collectors.toSet());
Set<NamespacedKey> filteredKeys = structureMap.keySet();
Set<NamespacedKey> filteredKeys = structureMap.keySet();
ctx.getSource().getExecutor().sendMessage("Found " + filteredKeys.size() + " structures");
filteredKeys.forEach(k -> ctx.getSource().getExecutor().sendMessage(k.toString()));
ctx.getSource().getExecutor().sendMessage("Found " + filteredKeys.size() + " structures");
filteredKeys.forEach(k -> ctx.getSource().getExecutor().sendMessage(k.toString()));
log.info("Classic: {}", structureManager.loadStructure(key("classic")) != null);
log.info("Classic-sand: {}", structureManager.loadStructure(key("classic-sand")) != null);
log.info("skyblock: {}", structureManager.loadStructure(key("skyblock")) != null);
return 0;
}
return 0;
public int executeSave(CommandContext<CommandSourceStack> ctx) {
int radius = ctx.getArgument("radius", Integer.class);
String name = ctx.getArgument("name", String.class);
StructureManager structureManager = getServer().getStructureManager();
Structure structure = structureManager.createStructure();
Location origin = ctx.getSource().getLocation();
Location start = origin.clone().subtract(radius, radius, radius);
Location end = origin.clone().add(radius, radius, radius);
structure.fill(start, end, true);
ctx.getSource().getExecutor().sendMessage("From " + start + " to " + end);
ctx.getSource().getExecutor().sendMessage("Palette count: " + (long) structure.getPaletteCount());
ctx.getSource().getExecutor().sendMessage("Block count: " + structure.getPalettes().getFirst().getBlockCount());
NamespacedKey nmKey = Reference.key(name);
try {
File f = new File("structures/" + nmKey.getKey() + ".nbt");
//noinspection ResultOfMethodCallIgnored
f.getParentFile().mkdirs();
structureManager.saveStructure(f, structure);
log.info("Saved structure {} to {}", name, f.getAbsolutePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
public int executeSave(CommandContext<CommandSourceStack> ctx) {
int radius = ctx.getArgument("radius", Integer.class);
String name = ctx.getArgument("name", String.class);
return 0;
}
StructureManager structureManager = getServer().getStructureManager();
public int executeDropStructure(CommandContext<CommandSourceStack> ctx) {
Structure structure = structureManager.createStructure();
Structure structure = ctx.getArgument("structure", Structure.class);
Location origin = ctx.getSource().getLocation();
Location start = origin.clone().subtract(radius, radius, radius);
Location end = origin.clone().add(radius, radius, radius);
structure.fill(start, end, true);
ctx.getSource().getExecutor().sendMessage("From " + start + " to " + end);
ctx.getSource().getExecutor().sendMessage("Palette count: " + (long) structure.getPaletteCount());
ctx.getSource().getExecutor().sendMessage("Block count: " + structure.getPalettes().getFirst().getBlockCount());
NamespacedKey nmKey = key(name);
try {
File f = new File("structures/" + nmKey.getKey() + ".nbt");
//noinspection ResultOfMethodCallIgnored
f.getParentFile().mkdirs();
structureManager.saveStructure(f, structure);
log.info("Saved structure {} to {}", name, f.getAbsolutePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
return 0;
if (structure == null) {
ctx.getSource().getExecutor().sendMessage("Structure not found");
return -1;
}
public int executeDropStructure(CommandContext<CommandSourceStack> ctx) {
Location loc = ctx.getSource().getLocation();
Vector halfSize = structure.getSize().divide(new Vector(2, 2, 2));
loc.subtract(halfSize);
Structure structure = ctx.getArgument("structure", Structure.class);
structure.place(loc, false, StructureRotation.NONE, Mirror.NONE, 0, 1, new Random(System.currentTimeMillis()));
if (structure == null) {
ctx.getSource().getExecutor().sendMessage("Structure not found");
return -1;
}
return 0;
}
Location loc = ctx.getSource().getLocation();
Vector halfSize = structure.getSize().divide(new Vector(2, 2, 2));
loc.subtract(halfSize);
public int executePlaceAnchor(CommandContext<CommandSourceStack> ctx) {
ctx.getSource().getExecutor().sendMessage("Placing anchor");
Location target = ctx.getSource().getExecutor().getLocation().subtract(0, 1, 0);
target.getBlock().setType(Material.STONE);
return 0;
}
structure.place(loc, false, StructureRotation.NONE, Mirror.NONE, 0, 1, new Random(System.currentTimeMillis()));
public int executeP0(CommandContext<CommandSourceStack> ctx) {
Entity executor = ctx.getSource().getExecutor();
executor.sendMessage("Placing P0");
return 0;
if (executor instanceof Player) {
Player p = (Player) executor;
RayTraceResult rayTraceResult = p.rayTraceBlocks(8);
Vector hitPosition = rayTraceResult.getHitPosition();
PersistentDataContainer pdc = p.getPersistentDataContainer();
pdc.set(Reference.key("_p0"), PersistentDataType.INTEGER_ARRAY,
new int[]{hitPosition.getBlockX(), hitPosition.getBlockY(), hitPosition.getBlockZ()});
Location p0 = new Location(p.getWorld(), hitPosition.getX(), hitPosition.getY(), hitPosition.getZ());
Location p1 = p0.clone();
NamespacedKey p1Key = Reference.key("_p1");
if (pdc.has(p1Key, PersistentDataType.INTEGER_ARRAY)) {
int[] ints = pdc.get(p1Key, PersistentDataType.INTEGER_ARRAY);
p1.set(ints[0], ints[1], ints[2]);
}
box.createBox(p0, p1);
}
public int executePlaceAnchor(CommandContext<CommandSourceStack> ctx) {
ctx.getSource().getExecutor().sendMessage("Placing anchor");
Location target = ctx.getSource().getExecutor().getLocation().subtract(0, 1, 0);
target.getBlock().setType(Material.STONE);
return 0;
return 0;
}
public int executeP1(CommandContext<CommandSourceStack> ctx) {
Entity executor = ctx.getSource().getExecutor();
executor.sendMessage("Placing P1");
if (executor instanceof Player) {
Player p = (Player) executor;
RayTraceResult rayTraceResult = p.rayTraceBlocks(8);
Vector hitPosition = rayTraceResult.getHitPosition();
PersistentDataContainer pdc = p.getPersistentDataContainer();
pdc.set(Reference.key("_p1"), PersistentDataType.INTEGER_ARRAY,
new int[]{hitPosition.getBlockX(), hitPosition.getBlockY(), hitPosition.getBlockZ()});
Location p1 = new Location(p.getWorld(), hitPosition.getX(), hitPosition.getY(), hitPosition.getZ());
Location p0 = p1.clone();
NamespacedKey p0Key = Reference.key("_p0");
if (pdc.has(p0Key, PersistentDataType.INTEGER_ARRAY)) {
int[] ints = pdc.get(p0Key, PersistentDataType.INTEGER_ARRAY);
p0.set(ints[0], ints[1], ints[2]);
}
box.createBox(p0, p1);
}
public int executeP0(CommandContext<CommandSourceStack> ctx) {
Entity executor = ctx.getSource().getExecutor();
executor.sendMessage("Placing P0");
return 0;
}
if (executor instanceof Player) {
Player p = (Player) executor;
RayTraceResult rayTraceResult = p.rayTraceBlocks(8);
public int executePSave(CommandContext<CommandSourceStack> ctx) {
Entity executor = ctx.getSource().getExecutor();
String name = ctx.getArgument("name", String.class);
executor.sendMessage("Saving structure");
Vector hitPosition = rayTraceResult.getHitPosition();
PersistentDataContainer pdc = p.getPersistentDataContainer();
pdc.set(key("_p0"), PersistentDataType.INTEGER_ARRAY, new int[]{hitPosition.getBlockX(), hitPosition.getBlockY(), hitPosition.getBlockZ()});
if (!(executor instanceof Player)) return 0;
Player p = (Player) executor;
Location p0 = new Location(p.getWorld(), hitPosition.getX(), hitPosition.getY(), hitPosition.getZ());
Location p1 = p0.clone();
PersistentDataContainer pdc = p.getPersistentDataContainer();
int[] p0 = pdc.get(Reference.key("_p0"), PersistentDataType.INTEGER_ARRAY);
int[] p1 = pdc.get(Reference.key("_p1"), PersistentDataType.INTEGER_ARRAY);
NamespacedKey p1Key = key("_p1");
if(pdc.has(p1Key, PersistentDataType.INTEGER_ARRAY)) {
int[] ints = pdc.get(p1Key, PersistentDataType.INTEGER_ARRAY);
p1.set(ints[0], ints[1], ints[2]);
}
box.createBox(p0, p1);
}
return 0;
if (p0 == null) {
executor.sendMessage("No P0 found");
return -1;
} else if (p1 == null) {
executor.sendMessage("No P1 found");
return -1;
}
public int executeP1(CommandContext<CommandSourceStack> ctx) {
Entity executor = ctx.getSource().getExecutor();
executor.sendMessage("Placing P1");
Structure structure = getServer().getStructureManager().createStructure();
if (executor instanceof Player) {
Player p = (Player) executor;
RayTraceResult rayTraceResult = p.rayTraceBlocks(8);
World world = executor.getWorld();
Location p0Loc = new Location(world, p0[0], p0[1], p0[2]);
Location p1Loc = new Location(world, p1[0], p1[1], p1[2]);
Vector hitPosition = rayTraceResult.getHitPosition();
PersistentDataContainer pdc = p.getPersistentDataContainer();
pdc.set(key("_p1"), PersistentDataType.INTEGER_ARRAY, new int[]{hitPosition.getBlockX(), hitPosition.getBlockY(), hitPosition.getBlockZ()});
structure.fill(p0Loc, p1Loc, true);
Location p1 = new Location(p.getWorld(), hitPosition.getX(), hitPosition.getY(), hitPosition.getZ());
Location p0 = p1.clone();
ctx.getSource().getExecutor().sendMessage("From " + p0Loc + " to " + p1Loc);
ctx.getSource().getExecutor().sendMessage("Palette count: " + (long) structure.getPaletteCount());
ctx.getSource().getExecutor().sendMessage("Block count: " + structure.getPalettes().getFirst().getBlockCount());
NamespacedKey p0Key = key("_p0");
if(pdc.has(p0Key, PersistentDataType.INTEGER_ARRAY)) {
int[] ints = pdc.get(p0Key, PersistentDataType.INTEGER_ARRAY);
p0.set(ints[0], ints[1], ints[2]);
}
NamespacedKey nmKey = Reference.key(name);
box.createBox(p0, p1);
}
return 0;
try {
File f = new File("structures/" + nmKey.getKey() + ".nbt");
//noinspection ResultOfMethodCallIgnored
f.getParentFile().mkdirs();
getServer().getStructureManager().saveStructure(f, structure);
log.info("Saved structure {} to {}", name, f.getAbsolutePath());
pdc.remove(Reference.key("_p0"));
pdc.remove(Reference.key("_p1"));
box.cleanup();
} catch (IOException e) {
throw new RuntimeException(e);
}
public int executePSave(CommandContext<CommandSourceStack> ctx) {
Entity executor = ctx.getSource().getExecutor();
String name = ctx.getArgument("name", String.class);
executor.sendMessage("Saving structure");
return 0;
}
if (!(executor instanceof Player)) return 0;
Player p = (Player) executor;
public static LiteralCommandNode<CommandSourceStack> create() {
var root = Commands.literal("skyblock-admin");
var cmd = Get(SkyblockAdminCommand.class);
PersistentDataContainer pdc = p.getPersistentDataContainer();
int[] p0 = pdc.get(key("_p0"), PersistentDataType.INTEGER_ARRAY);
int[] p1 = pdc.get(key("_p1"), PersistentDataType.INTEGER_ARRAY);
root.requires(cmd::auth);
if (p0 == null) {
executor.sendMessage("No P0 found");
return -1;
} else if (p1 == null) {
executor.sendMessage("No P1 found");
return -1;
}
root.then(Commands.literal("reload").executes(cmd::executeReloadConfig));
Structure structure = getServer().getStructureManager().createStructure();
var structures = Commands.literal("structures");
structures.then(Commands.literal("list").executes(cmd::executeStructuresList));
structures.then(Commands.literal("save")
.then(Commands.argument("radius", IntegerArgumentType.integer())
.then(Commands.argument("name", StringArgumentType.word())
.executes(cmd::executeSave))));
structures.then(Commands.literal("load")
.then(Commands.argument("structure", new SkyblockStructureArgument())
.executes(cmd::executeDropStructure)));
structures.then(Commands.literal("anchor").executes(cmd::executePlaceAnchor));
structures.then(Commands.literal("end").executes(cmd::executeBuildCentralEndPortal));
Location p0Loc = new Location(getOverworld(), p0[0], p0[1], p0[2]);
Location p1Loc = new Location(getOverworld(), p1[0], p1[1], p1[2]);
structures.then(Commands.literal("p0").executes(cmd::executeP0));
structures.then(Commands.literal("p1").executes(cmd::executeP1));
structures.then(Commands.literal("pSave")
.then(Commands.argument("name", StringArgumentType.word()).executes(cmd::executePSave)));
structure.fill(p0Loc, p1Loc, true);
var islands = Commands.literal("islands");
var unlockCmd = Commands.literal("unlock");
unlockCmd.then(
Commands.argument("tier", new EnumArgument<>(SkyblockTeam.Tiers.class)).executes(cmd::executeIslandUnlock));
unlockCmd.then(Commands.argument("team", new SkyblockGenCommand.SkyblockTeamArgument())
.executes(cmd::executeIslandUnlockForTeam));
islands.then(unlockCmd);
ctx.getSource().getExecutor().sendMessage("From " + p0Loc + " to " + p1Loc);
ctx.getSource().getExecutor().sendMessage("Palette count: " + (long) structure.getPaletteCount());
ctx.getSource().getExecutor().sendMessage("Block count: " + structure.getPalettes().getFirst().getBlockCount());
root.then(structures);
root.then(islands);
NamespacedKey nmKey = key(name);
return root.build();
}
try {
File f = new File("structures/" + nmKey.getKey() + ".nbt");
//noinspection ResultOfMethodCallIgnored
f.getParentFile().mkdirs();
getServer().getStructureManager().saveStructure(f, structure);
log.info("Saved structure {} to {}", name, f.getAbsolutePath());
pdc.remove(key("_p0"));
pdc.remove(key("_p1"));
box.cleanup();
} catch (IOException e) {
throw new RuntimeException(e);
}
private int executeBuildCentralEndPortal(CommandContext<CommandSourceStack> ctx) {
ctx.getSource().getExecutor().sendMessage("Building central end portal");
InitialisationHandler.initOverworld(ctx.getSource().getLocation().getWorld(), true);
return 0;
}
return 0;
private int executeIslandUnlock(CommandContext<CommandSourceStack> ctx) {
SkyblockTeam.Tiers tier = ctx.getArgument("tier", SkyblockTeam.Tiers.class);
Entity executor = ctx.getSource().getExecutor();
if (!(executor instanceof Player player)) {
executor.sendMessage("Only players can use this command");
return -1;
}
public static LiteralCommandNode<CommandSourceStack> create() {
var root = Commands.literal("skyblock-admin");
var cmd = Get(SkyblockAdminCommand.class);
ScoreboardManager scoreboardManager = Bukkit.getServer().getScoreboardManager();
Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
Team team = scoreboard.getPlayerTeam(player);
root.requires(cmd::auth);
root.then(Commands.literal("reload").executes(cmd::executeReloadConfig));
var structures = Commands.literal("structures");
structures.then(Commands.literal("list").executes(cmd::executeStructuresList));
structures.then(Commands.literal("save").then(Commands.argument("radius", IntegerArgumentType.integer()).then(Commands.argument("name", StringArgumentType.word()).executes(cmd::executeSave))));
structures.then(Commands.literal("load").then(Commands.argument("structure", new SkyblockStructureArgument()).executes(cmd::executeDropStructure)));
structures.then(Commands.literal("anchor").executes(cmd::executePlaceAnchor));
structures.then(Commands.literal("p0").executes(cmd::executeP0));
structures.then(Commands.literal("p1").executes(cmd::executeP1));
structures.then(Commands.literal("pSave").then(Commands.argument("name", StringArgumentType.word()).executes(cmd::executePSave)));
root.then(structures);
return root.build();
if (team == null) {
player.sendMessage("No team found");
return -1;
}
private int executeReloadConfig(CommandContext<CommandSourceStack> context) {
JavaPlugin.getProvidingPlugin(SkyblockAdminCommand.class).reloadConfig();
if (!new SkyblockTeam(team).unlockTier(tier)) player.sendMessage("Failed to unlock tier, likely already unlocked.");
else player.sendMessage("Successfully unlocked tier for team \"" + team.getName() + "\"");
context.getSource().getExecutor().sendMessage("Reloaded config");
return 0;
return 0;
}
private int executeIslandUnlockForTeam(CommandContext<CommandSourceStack> ctx) {
SkyblockTeam.Tiers tier = ctx.getArgument("tier", SkyblockTeam.Tiers.class);
Entity executor = ctx.getSource().getExecutor();
if (!(executor instanceof Player player)) {
executor.sendMessage("Only players can use this command");
return -1;
}
private boolean auth(CommandSourceStack stack) {
return stack.getSender().isOp();
player.sendMessage("Unlocking island tier " + tier);
Team team = ctx.getArgument("team", Team.class);
if (team == null) {
player.sendMessage("No team found");
return -1;
}
if (!new SkyblockTeam(team).unlockTier(tier)) player.sendMessage("Failed to unlock tier, likely already unlocked.");
else player.sendMessage("Successfully unlocked tier for team \"" + team.getName() + "\"");
public static final class SkyblockStructureArgument implements CustomArgumentType<Structure, String> {
return 0;
}
@Override
public Structure parse(StringReader reader) throws CommandSyntaxException {
NamespacedKey key = new NamespacedKey("usefulskyblock", reader.readString());
return new StructureRef(key);
}
private int executeReloadConfig(CommandContext<CommandSourceStack> context) {
JavaPlugin.getProvidingPlugin(SkyblockAdminCommand.class).reloadConfig();
@Override
public ArgumentType<String> getNativeType() {
return StringArgumentType.word();
}
context.getSource().getExecutor().sendMessage("Reloaded config");
return 0;
}
private boolean auth(CommandSourceStack stack) {
return stack.getSender().isOp();
}
public static final class SkyblockStructureArgument implements CustomArgumentType<Structure, String> {
@Override
public Structure parse(StringReader reader) throws CommandSyntaxException {
NamespacedKey key = new NamespacedKey("usefulskyblock", reader.readString());
return new StructureRef(key);
}
@Override
public ArgumentType<String> getNativeType() {
return StringArgumentType.word();
}
}
public static final class EnumArgument<T extends Enum<T>> implements CustomArgumentType<T, String> {
private final Class<T> enumClass;
public EnumArgument(Class<T> enumClass) {
this.enumClass = enumClass;
}
@Override
public T parse(StringReader reader) throws CommandSyntaxException {
T[] enumConstants = enumClass.getEnumConstants();
String input = reader.readString();
for (T enumConstant : enumConstants) {
if (enumConstant.name().equals(input)) return enumConstant;
}
return null;
}
@Override
public ArgumentType<String> getNativeType() {
return StringArgumentType.word();
}
@Override
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
T[] enumConstants = enumClass.getEnumConstants();
for (T c : enumConstants) {
builder.suggest(c.name());
}
return CustomArgumentType.super.listSuggestions(context, builder);
}
}
}

View file

@ -1,225 +1,270 @@
package com.ncguy.usefulskyblock.command;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.LiteralCommandNode;
import com.ncguy.usefulskyblock.Advancements;
import com.ncguy.usefulskyblock.IslandNetworkGenerator;
import com.ncguy.usefulskyblock.Reference;
import com.ncguy.usefulskyblock.StructureRef;
import com.ncguy.usefulskyblock.data.BiomedStructure;
import com.ncguy.usefulskyblock.utils.MathsUtils;
import com.ncguy.usefulskyblock.utils.TextUtils;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.Commands;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import io.papermc.paper.command.brigadier.argument.CustomArgumentType;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.*;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.block.Biome;
import org.bukkit.block.structure.Mirror;
import org.bukkit.block.structure.StructureRotation;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.ScoreboardManager;
import org.bukkit.scoreboard.Team;
import org.bukkit.structure.Structure;
import org.bukkit.util.Vector;
import org.bukkit.util.BlockVector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.function.Supplier;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import static com.ncguy.usefulskyblock.Reference.key;
public class SkyblockGenCommand extends AbstractSkyblockCommand {
private static final Logger log = LoggerFactory.getLogger(SkyblockGenCommand.class);
private StructureRef[] centralIslands = {
new StructureRef(key("classic")),
};
private float playerIslandSpacing = 4096;
private StructureRef[] t1Islands = {
new StructureRef(key("classic-sand")),
new StructureRef(key("classic-sand")),
new StructureRef(key("classic-sand")),
new StructureRef(key("classic-sand")),
};
private StructureRef[] centralIslands = {new BiomedStructure(key("classic"), Biome.OCEAN),};
private StructureRef[] t2Islands = {
new StructureRef(key("classic")),
new StructureRef(key("classic-sand")),
new StructureRef(key("classic")),
new StructureRef(key("classic-sand")),
new StructureRef(key("classic")),
new StructureRef(key("classic-sand")),
new StructureRef(key("classic")),
new StructureRef(key("classic-sand")),
};
private static StructureRef[] t1Islands = {new BiomedStructure(key("classic-sand"), Biome.BEACH),};
private StructureRef[] t3Islands = {
new StructureRef(key("classic")),
new StructureRef(key("classic-sand")),
new StructureRef(key("classic")),
new StructureRef(key("classic-sand")),
new StructureRef(key("classic")),
new StructureRef(key("classic-sand")),
new StructureRef(key("classic")),
new StructureRef(key("classic-sand")),
new StructureRef(key("classic")),
new StructureRef(key("classic-sand")),
new StructureRef(key("classic")),
new StructureRef(key("classic-sand")),
new StructureRef(key("classic")),
new StructureRef(key("classic-sand")),
new StructureRef(key("classic")),
new StructureRef(key("classic-sand")),
};
private static StructureRef[] t2Islands = {new BiomedStructure(NamespacedKey.minecraft("igloo/top"),
Biome.SNOWY_PLAINS), new BiomedStructure(
NamespacedKey.minecraft("swamp_hut"), Biome.SWAMP),};
private float playerIslandSpacing = 1024;
private static StructureRef[] t3Islands = {new BiomedStructure(NamespacedKey.minecraft("monument"),
Biome.DEEP_OCEAN), new BiomedStructure(
NamespacedKey.minecraft("trail_ruins"), Biome.TAIGA), new BiomedStructure(NamespacedKey.minecraft("stronghold"),
Biome.THE_VOID), new BiomedStructure(
NamespacedKey.minecraft("mansion"), Biome.JUNGLE), new BiomedStructure(NamespacedKey.minecraft("ancient_city"),
Biome.DEEP_DARK),};
private static final int T1_ISLAND_SLOTS = 8;
private static final int T2_ISLAND_SLOTS = 24;
private static final int T3_ISLAND_SLOTS = 48;
private static final int T2_ISLAND_SLOTS = t2Islands.length;
private static final int T3_ISLAND_SLOTS = t3Islands.length;
private static float T1_ISLAND_SPACING = T1_ISLAND_SLOTS << 3;
private static float T2_ISLAND_SPACING = T2_ISLAND_SLOTS << 3;
private static float T3_ISLAND_SPACING = T3_ISLAND_SLOTS << 3;
private Location placeStructureAtLocation(Structure structure, Location loc, boolean canRotate, boolean canMirror) {
Vector extents = structure.getSize().clone().multiply(0.5);
loc.subtract(extents);
StructureRotation rotation = canRotate ? randomEnum(StructureRotation.class) : StructureRotation.NONE;
Mirror mirror = canMirror ? randomEnum(Mirror.class) : Mirror.NONE;
structure.place(loc, true, rotation, mirror, 0, 1, new Random());
// Optional<BlockState> bedrock2 = structure.getPalettes().getFirst().getBlocks().stream().filter(b -> b.getBlockData().getMaterial() == Material.BEDROCK).findFirst();
Optional<BlockState> bedrock = Optional.empty();
for(int x = loc.getBlockX(); x < loc.getBlockX() + structure.getSize().getBlockX(); x++) {
for(int y = loc.getBlockY(); y < loc.getBlockY() + structure.getSize().getBlockY(); y++) {
for(int z = loc.getBlockZ(); z < loc.getBlockZ() + structure.getSize().getBlockZ(); z++) {
Block b = loc.getWorld().getBlockAt(x, y, z);
if(b.getType() == Material.BEDROCK) {
bedrock = Optional.of(b.getState());
}
}
}
}
if(bedrock.isPresent()) {
loc = bedrock.get().getLocation();
loc.add(0, 2, 0);
}
return loc;
}
private <T extends Enum<?>> T randomEnum(Class<T> enumCls) {
assert(enumCls.isEnum());
return randomElement(enumCls.getEnumConstants());
}
private <T> T randomElement(T[] array) {
int idx = (int) (Math.random() * array.length);
return array[idx];
}
private Location generateIslandNetwork(Location origin) {
Location centralIslandSpawnLoc = placeStructureAtLocation(randomElement(centralIslands), origin.clone(), false, false);
int[] t1Slots = MathsUtils.sampleUniqueInts(T1_ISLAND_SLOTS, t1Islands.length);
int[] t2Slots = MathsUtils.sampleUniqueInts(T2_ISLAND_SLOTS, t2Islands.length);
int[] t3Slots = MathsUtils.sampleUniqueInts(T3_ISLAND_SLOTS, t3Islands.length);
double t1Step = 360.0 / T1_ISLAND_SLOTS;
double t2Step = 360.0 / T2_ISLAND_SLOTS;
double t3Step = 360.0 / T3_ISLAND_SLOTS;
Supplier<Float> yNoiseFunc = () -> 0f;
extracted(origin, t1Islands, T1_ISLAND_SPACING, t1Slots, t1Step, yNoiseFunc);
extracted(origin, t2Islands, T2_ISLAND_SPACING, t2Slots, t2Step, yNoiseFunc);
extracted(origin, t3Islands, T3_ISLAND_SPACING, t3Slots, t3Step, yNoiseFunc);
return centralIslandSpawnLoc;
}
private void extracted(Location origin, StructureRef[] islands, float islandSpacing, int[] slots, double step, Supplier<Float> yNoiseFunc) {
for (int i = 0; i < islands.length; i++) {
StructureRef island = islands[i];
int slot = slots[i];
double angle = step * slot;
double x = Math.cos(angle) * islandSpacing;
double z = Math.sin(angle) * islandSpacing;
double y = yNoiseFunc.get();
Location pos = origin.clone().add(x, y, z);
placeStructureAtLocation(island, pos, true, true);
}
}
private static float T2_ISLAND_SPACING = 192; // 24 << 3
private static float T3_ISLAND_SPACING = 384; // 48 << 3
public int executeGenerate(CommandContext<CommandSourceStack> ctx) {
Entity executor = ctx.getSource().getExecutor();
if(!(executor instanceof Player))
return 0;
if (!(executor instanceof Player player)) return 0;
World overworld = getServer().getWorld(new NamespacedKey("minecraft", "overworld"));
PersistentDataContainer pdc = executor.getPersistentDataContainer();
ScoreboardManager scoreboardManager = getServer().getScoreboardManager();
Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
Player player = (Player) executor;
Team playerTeam = scoreboard.getPlayerTeam(player);
if(playerTeam == null) {
if (playerTeam == null) {
executor.sendMessage("Not part of a team, can't generate skyblock world");
return 0;
}
// TODO Sanitize playerTeam.getName()
String playerTeamName = playerTeam.getName();
String sanitizedTeamName = playerTeamName.replaceAll("[^a-z0-9.]", "_");
PersistentDataContainer worldPDC = overworld.getPersistentDataContainer();
// worldPDC.set(key("skyblock.team." + playerTeam.getName()), PersistentDataType.INTEGER_ARRAY, new int[]{tpLoc.getBlockX(), tpLoc.getBlockY(), tpLoc.getBlockZ()});
if(worldPDC.has(key("skyblock.team." + playerTeamName), PersistentDataType.INTEGER_ARRAY)) {
int[] ints = worldPDC.get(key("skyblock.team." + playerTeamName), PersistentDataType.INTEGER_ARRAY);
Location loc = new Location(overworld, ints[0], ints[1], ints[2]);
executor.teleport(loc);
pdc.set(key("island.home.loc"), PersistentDataType.INTEGER_ARRAY, ints);
executor.sendMessage("Teleported to island home");
var teamSpawn = Reference.SKYBLOCK_TEAM_ROOT.withSuffix("." + sanitizedTeamName).assign(overworld);
var islandHomeLoc = Reference.ISLAND_HOME_LOC.assign(player);
var worldTeamCount = Reference.SKYBLOCK_TEAM_COUNT.assign(overworld);
if (islandHomeLoc.has()) {
executor.sendMessage("You already have an island assigned to you.");
return 0;
}
executor.sendMessage("Generating skyblock world for " + playerTeam.getName() + "...");
int count = 0;
if(worldPDC.has(key("skyblock.team.count"), PersistentDataType.INTEGER)) {
//noinspection DataFlowIssue
count = worldPDC.get(key("skyblock.team.count"), PersistentDataType.INTEGER);
if (teamSpawn.has()) {
BlockVector vec = teamSpawn.get();
Location loc = new Location(overworld, vec.getX(), vec.getY(), vec.getZ());
executor.teleport(loc);
islandHomeLoc.set(vec);
executor.sendMessage("Teleported to island home..");
return 0;
}
Location originLoc;
if(count == 0) {
originLoc = new Location(overworld, 0, 96, 0);
}else{
int ring = MathsUtils.getRing(count);
int slot = MathsUtils.getSlot(count);
executor.sendMessage(
Component.text("Generating skyblock world for ")
.append(playerTeam.displayName(), Component.text(" >>> ")));
int numSlots = MathsUtils.getSlotsInRing(ring);
int count = worldTeamCount.getOrDefault(0);
float step = 360f / numSlots;
float angle = slot * step;
float x = (float) Math.cos(angle) * playerIslandSpacing;
float y = (float) Math.sin(angle) * playerIslandSpacing;
originLoc = new Location(overworld, x, 96, y);
}
// int ring = MathsUtils.getRing(count);
// int slot = MathsUtils.getSlot(count);
// int numSlots = MathsUtils.getSlotsInRing(ring);
Location tpLoc = generateIslandNetwork(originLoc);
int ring = (int) (Math.floor(count / 24.0) + 1);
int slot = count % 24;
int numSlots = 24;
worldPDC.set(key("skyblock.team." + playerTeamName), PersistentDataType.INTEGER_ARRAY, new int[]{tpLoc.getBlockX(), tpLoc.getBlockY(), tpLoc.getBlockZ()});
worldPDC.set(key("skyblock.team.count"), PersistentDataType.INTEGER, count + 1);
float step = 360f / numSlots;
float angle = slot * step;
float x = (float) Math.cos(angle) * (playerIslandSpacing * ring);
float y = (float) Math.sin(angle) * (playerIslandSpacing * ring);
Location originLoc = new Location(overworld, x, 96, y);
Location tpLoc = generateTier(originLoc,
new IslandNetworkGenerator.TierDefinition(0, 1, 0, centralIslands, false, false));
teamSpawn.set(tpLoc.toVector().toBlockVector());
worldTeamCount.set(count + 1);
executor.teleport(tpLoc);
pdc.set(key("island.home.loc"), PersistentDataType.INTEGER_ARRAY, new int[]{tpLoc.getBlockX(), tpLoc.getBlockY(), tpLoc.getBlockZ()});
islandHomeLoc.set(tpLoc.toVector().toBlockVector());
Advancement a = Advancements.skyblock.SKYBLOCK_BEGIN;
AdvancementProgress aprog = player.getAdvancementProgress(a);
if (!aprog.isDone()) aprog.getRemainingCriteria().forEach(aprog::awardCriteria);
return 0;
}
public static IslandNetworkGenerator.TierDefinition T1_DEF = new IslandNetworkGenerator.TierDefinition(1,
T1_ISLAND_SLOTS,
T1_ISLAND_SPACING,
t1Islands);
public static IslandNetworkGenerator.TierDefinition T2_DEF = new IslandNetworkGenerator.TierDefinition(2,
T2_ISLAND_SLOTS,
T2_ISLAND_SPACING,
t2Islands);
public static IslandNetworkGenerator.TierDefinition T3_DEF = new IslandNetworkGenerator.TierDefinition(3,
T3_ISLAND_SLOTS,
T3_ISLAND_SPACING,
t3Islands);
public static Location generateTier(Location origin, IslandNetworkGenerator.TierDefinition tierDef) {
IslandNetworkGenerator.Definition def = new IslandNetworkGenerator.Definition();
def.tiers = new IslandNetworkGenerator.TierDefinition[]{tierDef};
IslandNetworkGenerator gen = new IslandNetworkGenerator();
return gen.generateIslandNetwork(origin, def);
}
public static Location generateNetherIslands(Location origin, Axis axis) {
IslandNetworkGenerator gen = new IslandNetworkGenerator();
StructureRef netherPortalStructure = new StructureRef(Reference.key("nether-start"));
StructureRef[] t1Islands = {
new BiomedStructure(Reference.key("nether-warped-island"), Biome.WARPED_FOREST),
new BiomedStructure(Reference.key("nether-crimson-island"), Biome.CRIMSON_FOREST),};
StructureRef[] t2Islands = {
new BiomedStructure(Reference.key("nether-warped-island"), Biome.WARPED_FOREST),
new BiomedStructure(Reference.key("nether-crimson-island"), Biome.CRIMSON_FOREST),
new BiomedStructure(Reference.key("nether-warped-island"), Biome.WARPED_FOREST),
new BiomedStructure(Reference.key("nether-crimson-island"), Biome.CRIMSON_FOREST),
new BiomedStructure(Reference.key("blaze-spawner"), Biome.BASALT_DELTAS),
new BiomedStructure(Reference.key("blaze-spawner"), Biome.SOUL_SAND_VALLEY),
new StructureRef(NamespacedKey.minecraft("ruined_portal_nether")),
new StructureRef(NamespacedKey.minecraft("ruined_portal_nether"))};
StructureRef[] t3Islands = {
new BiomedStructure(Reference.key("nether-warped-island"), Biome.WARPED_FOREST),
new BiomedStructure(Reference.key("nether-crimson-island"), Biome.CRIMSON_FOREST),
new BiomedStructure(Reference.key("nether-warped-island"), Biome.WARPED_FOREST),
new BiomedStructure(Reference.key("nether-crimson-island"), Biome.CRIMSON_FOREST),
new BiomedStructure(Reference.key("nether-warped-island"), Biome.WARPED_FOREST),
new BiomedStructure(Reference.key("nether-crimson-island"), Biome.CRIMSON_FOREST),
new BiomedStructure(Reference.key("nether-warped-island"), Biome.WARPED_FOREST),
new BiomedStructure(Reference.key("nether-crimson-island"), Biome.CRIMSON_FOREST)};
StructureRotation[] validCentralRotations = new StructureRotation[2];
if (axis == Axis.X) {
validCentralRotations[0] = StructureRotation.NONE;
validCentralRotations[1] = StructureRotation.CLOCKWISE_180;
} else {
validCentralRotations[0] = StructureRotation.CLOCKWISE_90;
validCentralRotations[1] = StructureRotation.COUNTERCLOCKWISE_90;
}
IslandNetworkGenerator.Definition def = new IslandNetworkGenerator.Definition();
def.tiers = new IslandNetworkGenerator.TierDefinition[]{new IslandNetworkGenerator.TierDefinition(0, 1, 0,
new StructureRef[]{netherPortalStructure},
validCentralRotations,
new Mirror[]{Mirror.NONE},
() -> 0f),
new IslandNetworkGenerator.TierDefinition(
1, t1Islands.length, T1_ISLAND_SPACING, t1Islands), new IslandNetworkGenerator.TierDefinition(2,
t2Islands.length,
T2_ISLAND_SPACING,
t2Islands),
new IslandNetworkGenerator.TierDefinition(
3, t3Islands.length, T3_ISLAND_SPACING, t3Islands),};
return gen.generateIslandNetwork(origin, def);
}
private int executeRoot(CommandContext<CommandSourceStack> ctx) {
Entity executor = ctx.getSource().getExecutor();
if (!(executor instanceof Player)) return 0;
Player player = (Player) executor;
var islandHomeLoc = Reference.ISLAND_HOME_LOC.assign(player);
World overworld = getServer().getWorld(new NamespacedKey("minecraft", "overworld"));
if (islandHomeLoc.has()) {
BlockVector blockVector = islandHomeLoc.get();
Location loc = new Location(overworld, blockVector.getX(), blockVector.getY(), blockVector.getZ());
if (!player.teleport(loc, PlayerTeleportEvent.TeleportCause.COMMAND)) {
player.sendMessage("Failed to teleport you to your island home, please notify an administrator");
return -1;
}
return 0;
}
ScoreboardManager scoreboardManager = getServer().getScoreboardManager();
Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
Team playerTeam = scoreboard.getPlayerTeam(player);
if (playerTeam == null) {
executor.sendMessage(Component.text("Please join a team first via ")
.append(Component.text("/skyblock team",
Style.style(TextColor.color(0x00, 0xff, 0xff)))));
return 0;
}
String playerTeamName = playerTeam.getName();
String sanitizedTeamName = playerTeamName.replaceAll("[^a-z0-9_.]", "_");
var teamSpawn = Reference.SKYBLOCK_TEAM_ROOT.withSuffix("." + sanitizedTeamName).assign(overworld);
if (teamSpawn.has()) {
islandHomeLoc.set(teamSpawn.get());
executor.sendMessage("Teleported to island home..");
BlockVector blockVector = islandHomeLoc.get();
Location loc = new Location(overworld, blockVector.getX(), blockVector.getY(), blockVector.getZ());
if (!player.teleport(loc, PlayerTeleportEvent.TeleportCause.COMMAND)) {
player.sendMessage("Failed to teleport you to your island home, please notify an administrator");
return -1;
}
return 0;
}
executor.sendMessage(Component.text("You have not yet generated a skyblock world for ")
.append(playerTeam.displayName(), Component.text(". Please generate one via "))
.append(Component.text("/skyblock generate",
Style.style(TextColor.color(0x00, 0xff, 0xff)))));
return 0;
}
@ -227,9 +272,307 @@ public class SkyblockGenCommand extends AbstractSkyblockCommand {
var root = Commands.literal("skyblock");
var cmd = Get(SkyblockGenCommand.class);
root.then(Commands.literal("generate").executes(cmd::executeGenerate));
var team = Commands.literal("team");
team.then(Commands.literal("create")
.then(Commands.argument("name", StringArgumentType.word()).executes(cmd::executeTeamCreate)));
team.then(Commands.literal("join")
.then(Commands.argument("team", new SkyblockTeamArgument()).executes(cmd::executeTeamJoin)));
team.then(Commands.literal("list").executes(cmd::executeTeamList));
// Team required
team.then(Commands.literal("leave").requires(cmd::inTeam).executes(cmd::executeTeamLeave));
team.then(Commands.literal("sync").requires(cmd::inTeam).executes(cmd::executeSyncAdvancements));
team.then(Commands.literal("progress").requires(cmd::inTeam).executes(cmd::executeGetProgress));
// Auth required
team.then(Commands.literal("prune").requires(cmd::auth).executes(cmd::executeTeamPrune));
team.then(Commands.literal("remove")
.requires(cmd::auth)
.then(Commands.argument("team", new SkyblockTeamArgument())
.executes(cmd::executeTeamRemove)));
root.then(team);
root.executes(cmd::executeRoot);
return root.build();
}
private int executeGetProgress(CommandContext<CommandSourceStack> ctx) {
Entity executor = ctx.getSource().getExecutor();
if (!(executor instanceof Player player)) return -1;
Component text = Component.text("Skyblock progress: ").appendNewline();
Advancement[][] groups = Advancements.groups();
Style defaultStyle = Style.style(TextColor.color(1.0f, 1.0f, 0.0f));
Style completeStyle = Style.style(TextColor.color(0.0f, 1.0f, 0.0f));
for (int i = 0; i < groups.length; i++) {
Advancement[] group = groups[i];
String groupName = Advancements.getGroupName(i);
int unlocked = 0;
for (Advancement advancement : group) {
if (player.getAdvancementProgress(advancement).isDone()) unlocked++;
}
if (unlocked == 0) continue;
boolean complete = unlocked == group.length;
float percent = (float) unlocked / group.length;
int percStr = Math.round(percent * 100);
Style style = complete ? completeStyle : defaultStyle;
groupName = TextUtils.toTitleCase(groupName);
text = text.append(
Component.text(groupName, style).append(Component.text(": ")).append(Component.text(unlocked)));
text = text.append(Component.text(" [").append(Component.text(percStr).append(Component.text("%]"))));
text = text.appendNewline();
}
player.sendMessage(text);
return 0;
}
private int executeSyncAdvancements(CommandContext<CommandSourceStack> ctx) {
Entity executor = ctx.getSource().getExecutor();
if (!(executor instanceof Player player)) {
return -1;
}
ScoreboardManager scoreboardManager = Bukkit.getServer().getScoreboardManager();
Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
Team team = scoreboard.getPlayerTeam(player);
assert team != null;
team.getEntries().forEach(entry -> {
Player otherPlayer = Bukkit.getPlayer(entry);
if (otherPlayer == null || otherPlayer.equals(player)) return;
for (Advancement adv : Advancements.values()) {
AdvancementProgress advancementProgress = player.getAdvancementProgress(adv);
advancementProgress.getAwardedCriteria().forEach(criterion -> {
otherPlayer.getAdvancementProgress(adv).awardCriteria(criterion);
});
}
});
return 0;
}
private int executeTeamRemove(CommandContext<CommandSourceStack> ctx) {
Team team = ctx.getArgument("team", Team.class);
Entity executor = Objects.requireNonNull(ctx.getSource().getExecutor());
if (team.getSize() == 0) {
team.unregister();
executor.sendMessage("Team \"" + team.getName() + "\" removed");
return 0;
}
executor.sendMessage("Team \"" + team.getName() + "\" is not empty, you cannot remove non-empty teams.");
return 0;
}
private int executeTeamPrune(CommandContext<CommandSourceStack> ctx) {
Entity executor = Objects.requireNonNull(ctx.getSource().getExecutor());
ScoreboardManager scoreboardManager = Bukkit.getServer().getScoreboardManager();
Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
Set<Team> teams = scoreboard.getTeams();
int initialSize = teams.size();
teams.stream().filter(team -> team.getSize() == 0).forEach(Team::unregister);
int newSize = scoreboard.getTeams().size();
if (initialSize == newSize) {
executor.sendMessage("No teams were empty, nothing was removed");
return 0;
}
executor.sendMessage("Removed " + (initialSize - newSize) + " empty teams");
return 0;
}
private boolean auth(CommandSourceStack stack) {
return stack.getSender().isOp();
}
private boolean inTeam(CommandSourceStack stack) {
ScoreboardManager scoreboardManager = Bukkit.getServer().getScoreboardManager();
Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
return stack.getSender() instanceof Player player && scoreboard.getPlayerTeam(player) != null;
}
private int executeTeamList(CommandContext<CommandSourceStack> ctx) {
Entity executor = Objects.requireNonNull(ctx.getSource().getExecutor());
ScoreboardManager scoreboardManager = Bukkit.getServer().getScoreboardManager();
Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
Set<Team> teams = scoreboard.getTeams();
Component c = Component.text("Teams: ").appendNewline();
c = c.append(Component.text("==========================").appendNewline());
for (Team team : teams) {
if (team.hasEntity(executor)) c = c.append(Component.text(" >> "));
else c = c.append(Component.text(" "));
c = c.append(team.displayName());
c = c.append(Component.text(" [")
.append(Component.text(team.getSize()))
.append(Component.text(" players]")));
c = c.appendNewline();
}
executor.sendMessage(c);
return 0;
}
private int executeTeamCreate(CommandContext<CommandSourceStack> ctx) {
String name = ctx.getArgument("name", String.class);
Entity executor = Objects.requireNonNull(ctx.getSource().getExecutor());
if (!(executor instanceof Player player)) {
executor.sendMessage("Only players can create teams");
return -1;
}
if (name.matches("[^a-z0-9_.]")) {
player.sendMessage("Team name must only contain alphanumeric characters and underscores");
return -1;
}
ScoreboardManager scoreboardManager = Bukkit.getServer().getScoreboardManager();
Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
Team team = scoreboard.getPlayerTeam(player);
if (team != null) {
player.sendMessage("You are already part of a team, please leave it before creating a new one.");
return -1;
}
team = scoreboard.getTeam(name);
if (team != null) {
player.sendMessage(
"Team already exists with the name \"" + name + "\". Please choose a different name, or run the command " + "`/skyblock team join " + name + "` to join the team.");
return -1;
}
team = scoreboard.registerNewTeam(name);
player.sendMessage("Created team \"" + name + "\"");
doTeamJoin(player, team);
return 0;
}
private void doTeamJoin(Entity entity, Team team) {
team.addEntity(entity);
}
private int executeTeamJoin(CommandContext<CommandSourceStack> ctx) {
Entity executor = Objects.requireNonNull(ctx.getSource().getExecutor());
if (!(executor instanceof Player player)) {
executor.sendMessage("Only players can join teams");
return -1;
}
ScoreboardManager scoreboardManager = Bukkit.getServer().getScoreboardManager();
Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
Team team = scoreboard.getPlayerTeam(player);
if (team != null) {
player.sendMessage("You are already part of a team, please leave it before joining a new one.");
return -1;
}
Team teamToJoin = ctx.getArgument("team", Team.class);
if (teamToJoin == null) {
player.sendMessage("Team not found");
return -1;
}
player.sendMessage("Joining team \"" + teamToJoin.getName() + "\"");
Optional<Player> first = teamToJoin.getEntries()
.stream()
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
.findFirst();
doTeamJoin(player, teamToJoin);
if (first.isPresent()) {
Player teamPlayer = first.get();
for (Advancement adv : Advancements.values()) {
final AdvancementProgress advancementProgress = teamPlayer.getAdvancementProgress(adv);
final AdvancementProgress playerAdvProg = player.getAdvancementProgress(adv);
advancementProgress.getAwardedCriteria().forEach(playerAdvProg::awardCriteria);
}
}
return 0;
}
private int executeTeamLeave(CommandContext<CommandSourceStack> ctx) {
Entity executor = Objects.requireNonNull(ctx.getSource().getExecutor());
if (!(executor instanceof Player player)) {
executor.sendMessage("Only players can leave teams");
return -1;
}
ScoreboardManager scoreboardManager = Bukkit.getServer().getScoreboardManager();
Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
Team team = scoreboard.getPlayerTeam(player);
assert (team != null);
player.sendMessage("Leaving team \"" + team.getName() + "\"");
team.removePlayer(player);
Reference.ISLAND_HOME_LOC.assign(player).remove();
for (Advancement adv : Advancements.values()) {
AdvancementProgress prg = player.getAdvancementProgress(adv);
prg.getAwardedCriteria().forEach(prg::revokeCriteria);
}
return 0;
}
public static final class SkyblockTeamArgument implements CustomArgumentType<Team, String> {
@Override
public Team parse(StringReader reader) throws CommandSyntaxException {
String teamName = reader.readString();
ScoreboardManager scoreboardManager = Bukkit.getServer().getScoreboardManager();
Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
return Objects.requireNonNull(scoreboard.getTeams())
.stream()
.filter(x -> x.getName().equals(teamName))
.findFirst()
.get();
}
@Override
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
ScoreboardManager scoreboardManager = Bukkit.getServer().getScoreboardManager();
Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
scoreboard.getTeams().forEach(team -> builder.suggest(team.getName()));
return CustomArgumentType.super.listSuggestions(context, builder);
}
@Override
public ArgumentType<String> getNativeType() {
return StringArgumentType.word();
}
}
}

View file

@ -0,0 +1,15 @@
package com.ncguy.usefulskyblock.data;
import com.ncguy.usefulskyblock.StructureRef;
import org.bukkit.NamespacedKey;
import org.bukkit.block.Biome;
public class BiomedStructure extends StructureRef {
public final Biome biome;
public BiomedStructure(NamespacedKey name, Biome biome) {
super(name);
this.biome = biome;
}
}

View file

@ -0,0 +1,27 @@
package com.ncguy.usefulskyblock.events;
import org.bukkit.configuration.Configuration;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public class SkyblockConfigReloadEvent extends Event {
public Configuration config;
public SkyblockConfigReloadEvent(Configuration config) {
this.config = config;
}
private static final HandlerList HANDLER_LIST = new HandlerList();
@Override
public @NotNull HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View file

@ -0,0 +1,29 @@
package com.ncguy.usefulskyblock.events;
import com.ncguy.usefulskyblock.SkyblockTeam;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.bukkit.scoreboard.Team;
import org.jetbrains.annotations.NotNull;
public class TeamProgressionEvent extends Event {
public Team team;
public SkyblockTeam.Tiers unlockedTier;
public TeamProgressionEvent(Team team, SkyblockTeam.Tiers unlockedTier) {
this.team = team;
this.unlockedTier = unlockedTier;
}
private static final HandlerList HANDLER_LIST = new HandlerList();
@Override
public @NotNull HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View file

@ -0,0 +1,98 @@
package com.ncguy.usefulskyblock.handlers;
import com.ncguy.usefulskyblock.Advancements;
import com.ncguy.usefulskyblock.Reference;
import io.papermc.paper.datacomponent.DataComponentType;
import io.papermc.paper.datacomponent.DataComponentTypes;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.block.data.BlockData;
import org.bukkit.damage.DamageType;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import java.util.UUID;
import static net.kyori.adventure.sound.Sound.Source.BLOCK;
public class CauldronCraftingHandler implements Listener {
@EventHandler
public void onCauldronInteract(PlayerInteractEvent event) {
Block clickedBlock = event.getClickedBlock();
if (clickedBlock == null) return;
Material type = clickedBlock.getType();
if (type == Material.LAVA_CAULDRON) {
if (handleLavaCauldron(event)) return;
}
}
@EventHandler
public void onEntityDamage(EntityDamageEvent event) {
if (event.getDamageSource().getDamageType() != DamageType.LAVA) return;
if (event.getEntityType() != EntityType.ITEM) return;
Item item = (Item) event.getEntity();
Material type = item.getItemStack().getType();
boolean isImmune = Reference.ITEM_LAVA_IMMUNE.assign(item).getOrDefault(false);
if (type != Material.COBBLESTONE && !isImmune) return;
int health = item.getHealth();
if (health - event.getFinalDamage() > 0) return;
ItemStack newItemStack = new ItemStack(Material.NETHERRACK, item.getItemStack().getAmount());
item.getWorld().dropItem(item.getLocation(), newItemStack, i -> {
Reference.ITEM_LAVA_IMMUNE.assign(i).set(true);
i.setHealth(8);
i.playSound(Sound.sound().type(new NamespacedKey("minecraft", "entity.generic.burn")).source(BLOCK).build());
});
// Grant advancement
UUID thrower = item.getThrower();
if(thrower == null) return;
Player player = Bukkit.getPlayer(thrower);
if(player == null) return;
AdvancementProgress advancementProgress = player.getAdvancementProgress(Advancements.nether.CRAFT_NETHERRACK);
advancementProgress.getRemainingCriteria().forEach(advancementProgress::awardCriteria);
}
public boolean handleLavaCauldron(PlayerInteractEvent event) {
if (event.getBlockFace() != BlockFace.UP) return false;
if (!event.isBlockInHand()) return false;
ItemStack item = event.getItem();
if (item == null) return false;
if (item.getType() == Material.COBBLESTONE) {
EquipmentSlot hand = event.getHand();
if (hand == null) return false;
event.setUseInteractedBlock(Event.Result.DENY);
event.setUseItemInHand(Event.Result.DENY);
event.getPlayer().getInventory().setItem(hand, item.withType(Material.NETHERRACK));
return true;
}
return false;
}
}

View file

@ -0,0 +1,43 @@
package com.ncguy.usefulskyblock.handlers;
import com.ncguy.usefulskyblock.events.SkyblockConfigReloadEvent;
import com.ncguy.usefulskyblock.utils.MathsUtils;
import org.bukkit.Material;
import org.bukkit.entity.FishHook;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerFishEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
public class FishingHandler implements Listener {
private int sandChance = 800;
private final int sandMax = 1000;
@EventHandler
public void onConfigLoad(SkyblockConfigReloadEvent event) {
double chance = Math.clamp(event.config.getDouble("skyblock.fishing.sand_chance", 0.8), 0.0, 1.0);
this.sandChance = Math.toIntExact(Math.round(chance * sandMax));
}
@EventHandler
public void onPlayerFish(PlayerFishEvent event) {
if(event.getState() != PlayerFishEvent.State.REEL_IN)
return;
final FishHook hook = event.getHook();
int chance = Math.toIntExact(Math.round(Math.random() * sandMax));
if(chance >= sandChance)
return;
ItemStack stack = ItemStack.of(Material.SAND);
hook.getWorld().dropItem(hook.getLocation(), stack, item -> {
Vector vel = MathsUtils.getVelocityToTarget(item.getLocation().toVector(),
event.getPlayer().getLocation().toVector());
item.setVelocity(vel.multiply(0.75));
});
}
}

View file

@ -0,0 +1,170 @@
package com.ncguy.usefulskyblock.handlers;
import com.destroystokyo.paper.event.player.PlayerPostRespawnEvent;
import com.ncguy.usefulskyblock.Reference;
import com.ncguy.usefulskyblock.StructureRef;
import com.ncguy.usefulskyblock.pdc.IDataContainerRef;
import net.kyori.adventure.text.Component;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.EndPortalFrame;
import org.bukkit.block.structure.Mirror;
import org.bukkit.block.structure.StructureRotation;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.server.ServerLoadEvent;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Random;
import static com.ncguy.usefulskyblock.Reference.key;
public class InitialisationHandler implements Listener {
private static final Logger log = LoggerFactory.getLogger(InitialisationHandler.class);
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
player.sendMessage(Component.text("Hello, " + player.getName() + "!"));
if(Reference.ISLAND_HOME_LOC.assign(player).has())
return;
// If the player doesn't have a home island, respawn them in the server lobby
World world = Bukkit.getWorld(key("void"));
player.teleport(world.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
player.setRespawnLocation(world.getSpawnLocation());
}
@EventHandler
public void onPlayerPostRespawn(PlayerPostRespawnEvent event) {
Player player = event.getPlayer();
player.sendMessage(Component.text("Hello, " + player.getName() + "!"));
var islandHome = Reference.ISLAND_HOME_LOC.assign(player);
if(islandHome.has()) {
// TODO Handle respawning and such, especially when bed is missing
return;
}
// If the player doesn't have a home island, respawn them in the server lobby
World world = Bukkit.getWorld(key("void"));
player.teleport(world.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
player.setRespawnLocation(world.getSpawnLocation());
}
@EventHandler
public void onServerLoad(ServerLoadEvent event) {
if(event.getType() != ServerLoadEvent.LoadType.STARTUP)
return;
World world = Bukkit.getWorld(NamespacedKey.minecraft("overworld"));
if(world != null)
initOverworld(world);
world = Bukkit.getWorld(key("void"));
if (world == null)
return;
PersistentDataContainer pdc = world.getPersistentDataContainer();
NamespacedKey initKey = key("world_init");
if(pdc.has(initKey))
return;
pdc.set(initKey, PersistentDataType.BOOLEAN, true);
log.info("Generating spawn point for world {}", world.getName());
StructureRef spawn = new StructureRef(key("spawn"));
log.info("Entities in spawn: {}", spawn.getEntities().size());
log.info("Palettes in spawn: {}", spawn.getPalettes().size());
log.info("Palette count in spawn: {}", spawn.getPaletteCount());
log.info("spawn.getSize(): {}", spawn.getSize());
spawn.place(new Location(world, 0, 64, 0), true, StructureRotation.NONE, Mirror.NONE, 0, 1, new Random(System.currentTimeMillis()));
world.setSpawnLocation(0, 64, 0);
Location loc = world.getSpawnLocation().clone().subtract(0, 2, 0);
loc.getBlock().setType(Material.BEDROCK);
}
@EventHandler
public void onWorldLoad(WorldLoadEvent event) {
World world = event.getWorld();
if (world.getKey().equals(key("void")))
initVoid(world);
if(world.getKey().equals(NamespacedKey.minecraft("overworld")))
initOverworld(world);
}
private void initVoid(World world) {
var world_init = Reference.WORLD_INIT.assign(world);
if(world_init.has())
return;
world_init.set(true);
log.info("Generating spawn point for world {}", world.getName());
StructureRef spawn = new StructureRef(key("spawn"));
spawn.place(new Location(world, 0, 64, 0), true, StructureRotation.NONE, Mirror.NONE, 0, 1, new Random(System.currentTimeMillis()));
world.setSpawnLocation(0, 64, 0);
Location loc = world.getSpawnLocation().clone().subtract(0, 2, 0);
loc.getBlock().setType(Material.BEDROCK);
}
private static class EndFrameLocation {
public Location loc;
public BlockFace facing;
public EndFrameLocation(Location loc, BlockFace facing) {
this.loc = loc;
this.facing = facing;
}
}
public static void initOverworld(World world) {
initOverworld(world, false);
}
public static void initOverworld(World world, boolean override) {
var world_init = Reference.WORLD_INIT.assign(world);
if (!override && world_init.has()) return;
EndFrameLocation[] frameLocs = new EndFrameLocation[] {
new EndFrameLocation(new Location(world, -2, 0, -1), BlockFace.EAST),
new EndFrameLocation(new Location(world, -2, 0, -0), BlockFace.EAST),
new EndFrameLocation(new Location(world, -2, 0, 1), BlockFace.EAST),
new EndFrameLocation(new Location(world, -1, 0, -2), BlockFace.SOUTH),
new EndFrameLocation(new Location(world, 0, 0, -2), BlockFace.SOUTH),
new EndFrameLocation(new Location(world, 1, 0, -2), BlockFace.SOUTH),
new EndFrameLocation(new Location(world, 2, 0, -1), BlockFace.WEST),
new EndFrameLocation(new Location(world, 2, 0, -0), BlockFace.WEST),
new EndFrameLocation(new Location(world, 2, 0, 1), BlockFace.WEST),
new EndFrameLocation(new Location(world, -1, 0, 2), BlockFace.NORTH),
new EndFrameLocation(new Location(world, 0, 0, 2), BlockFace.NORTH),
new EndFrameLocation(new Location(world, 1, 0, 2), BlockFace.NORTH),
};
for (EndFrameLocation frameLoc : frameLocs) {
world.getChunkAtAsync(frameLoc.loc, true).thenAccept(chunk -> {
Block block = frameLoc.loc.getBlock();
block.setType(Material.END_PORTAL_FRAME);
EndPortalFrame frame = (EndPortalFrame) block.getBlockData();
frame.setFacing(frameLoc.facing);
frame.setEye(false);
block.setBlockData(frame);
});
}
world_init.set(true);
log.info("Generated end portal frame");
}
}

View file

@ -0,0 +1,48 @@
package com.ncguy.usefulskyblock.handlers;
import com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent;
import com.ncguy.usefulskyblock.Advancements;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Statistic;
import org.bukkit.World;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.damage.DamageType;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.ItemDespawnEvent;
import org.bukkit.event.player.PlayerPickupItemEvent;
import java.util.UUID;
public class ItemEventHandler implements Listener {
@EventHandler
public void onEntityRemoveFromWorld(EntityRemoveFromWorldEvent event) {
if(event.getEntityType() != EntityType.ITEM)
return;
Item item = (Item) event.getEntity();
Location location = item.getLocation();
World world = location.getWorld();
if(location.getY() > world.getMinHeight())
return;
UUID throwerId = item.getThrower();
if(throwerId == null)
return;
Player player = Bukkit.getPlayer(throwerId);
if(player == null)
return;
AdvancementProgress advancementProgress = player.getAdvancementProgress(Advancements.skyblock.VOID_BIN);
if(!advancementProgress.isDone())
advancementProgress.getRemainingCriteria().forEach(advancementProgress::awardCriteria);
}
}

View file

@ -0,0 +1,79 @@
package com.ncguy.usefulskyblock.handlers;
import com.ncguy.usefulskyblock.SkyblockTeam;
import com.ncguy.usefulskyblock.command.SkyblockGenCommand;
import com.ncguy.usefulskyblock.events.TeamProgressionEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerAdvancementDoneEvent;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.ScoreboardManager;
import org.bukkit.scoreboard.Team;
import java.util.Set;
public class TeamProgressHandler implements Listener {
@EventHandler
public void onAdvancement(PlayerAdvancementDoneEvent event) {
Player player = event.getPlayer();
Advancement advancement = event.getAdvancement();
// Only share usefulskyblock advancements
if(!advancement.getKey().getNamespace().equals("usefulskyblock"))
return;
ScoreboardManager scoreboardManager = Bukkit.getServer().getScoreboardManager();
Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
Team team = scoreboard.getPlayerTeam(player);
if(team == null)
return;
SkyblockTeam.Tiers unlockingTier = SkyblockTeam.isUnlockingAdvancement(advancement);
if(unlockingTier != null)
new SkyblockTeam(team).tryUnlockTier(unlockingTier);
// FIXME Caveat: only shares advancements among online members (and maybe offline, thanks Bukkit..)
Set<String> entries = team.getEntries();
entries.forEach(entry -> {
Player entryPlayer = Bukkit.getPlayer(entry);
if (entryPlayer == null || entryPlayer == player)
return;
AdvancementProgress prg = entryPlayer.getAdvancementProgress(advancement);
prg.getRemainingCriteria().forEach(prg::awardCriteria);
});
}
@EventHandler
public void onTeamProgression(TeamProgressionEvent event) {
SkyblockTeam sTeam = new SkyblockTeam(event.team);
Location homeLoc = sTeam.getHomeIslandLoc();
sTeam.broadcast(Component.text("Tier ").append(Component.text(event.unlockedTier.displayName(), Style.style(
TextColor.color(0x00, 0xff, 0xff))), Component.space(), Component.text("unlocked!")));
switch(event.unlockedTier) {
case TIER_1:
SkyblockGenCommand.generateTier(homeLoc, SkyblockGenCommand.T1_DEF);
break;
case TIER_2:
SkyblockGenCommand.generateTier(homeLoc, SkyblockGenCommand.T2_DEF);
break;
case TIER_3:
SkyblockGenCommand.generateTier(homeLoc, SkyblockGenCommand.T3_DEF);
break;
}
}
}

View file

@ -0,0 +1,56 @@
package com.ncguy.usefulskyblock.pdc;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataHolder;
import org.bukkit.persistence.PersistentDataType;
public class AnonymousDataContainerRef<T, H extends PersistentDataHolder> implements IDataContainerRef<T, H> {
protected final NamespacedKey ref;
protected final PersistentDataType<?, T> type;
public <P> AnonymousDataContainerRef(NamespacedKey ref, PersistentDataType<P, T> type) {
this.ref = ref;
this.type = type;
}
@Override
public IDataContainerRef<T, H> withSuffix(String suffix) {
return new AnonymousDataContainerRef<>(new NamespacedKey(ref.getNamespace(), ref.getKey() + suffix), type);
}
@Override
public IDataContainerRef<T, H> assign(H holder) {
return new StrictDataContainerRef<>(holder, ref, type);
}
@Override
public <P, C> IDataContainerRef<C, H> withType(PersistentDataType<P, C> newType) {
return new AnonymousDataContainerRef<>(ref, newType);
}
@Override
public <NewH extends PersistentDataHolder> IDataContainerRef<T, NewH> restrict(Class<NewH> hClass) {
return new AnonymousDataContainerRef<T, NewH>(ref, type);
}
@Override
public T get() {
throw new UnsupportedOperationException("AnonymousDataContainerRef does not have an assigned holder");
}
@Override
public void set(T val) {
throw new UnsupportedOperationException("AnonymousDataContainerRef does not have an assigned holder");
}
@Override
public boolean has() {
throw new UnsupportedOperationException("AnonymousDataContainerRef does not have an assigned holder");
}
@Override
public void remove() {
throw new UnsupportedOperationException("AnonymousDataContainerRef does not have an assigned holder");
}
}

View file

@ -0,0 +1,32 @@
package com.ncguy.usefulskyblock.pdc;
import org.bukkit.persistence.PersistentDataHolder;
import org.bukkit.persistence.PersistentDataType;
import java.util.function.Function;
public interface IDataContainerRef<T, HOLDER extends PersistentDataHolder> {
public IDataContainerRef<T, HOLDER> assign(HOLDER holder);
public IDataContainerRef<T, HOLDER> withSuffix(String suffix);
public <P, C> IDataContainerRef<C, HOLDER> withType(PersistentDataType<P, C> newType);
public <NewH extends PersistentDataHolder> IDataContainerRef<T, NewH> restrict(Class<NewH> hClass);
public T get();
public boolean has();
public void set(T val);
void remove();
default public T getOrDefault(T def) {
return has() ? get() : def;
}
default public <NewT> IDataContainerRef<NewT, HOLDER> map(Function<T, NewT> mapper) {
return map(mapper, null);
}
default public <NewT> IDataContainerRef<NewT, HOLDER> map(Function<T, NewT> mapper, Function<NewT, T> reverseMapper) {
return new MappedDataContainerRef<>(this, mapper, reverseMapper);
}
}

View file

@ -0,0 +1,62 @@
package com.ncguy.usefulskyblock.pdc;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataHolder;
import org.bukkit.persistence.PersistentDataType;
import java.util.function.Function;
public class MappedDataContainerRef<T, U, H extends PersistentDataHolder> implements IDataContainerRef<T, H> {
private IDataContainerRef<U, H> underlying;
private Function<U, T> mapper;
private Function<T, U> reverseMapper;
public MappedDataContainerRef(IDataContainerRef<U, H> underlying, Function<U, T> mapper, Function<T, U> reverseMapper) {
this.underlying = underlying;
this.mapper = mapper;
this.reverseMapper = reverseMapper;
}
@Override
public IDataContainerRef<T, H> assign(H holder) {
return new MappedDataContainerRef<>(underlying.assign(holder), mapper, reverseMapper);
}
@Override
public IDataContainerRef<T, H> withSuffix(String suffix) {
return new MappedDataContainerRef<>(underlying.withSuffix(suffix), mapper, reverseMapper);
}
@Override
public <P, C> IDataContainerRef<C, H> withType(PersistentDataType<P, C> newType) {
throw new UnsupportedOperationException("MappedDataContainerRef does not support type conversion");
}
@Override
public <NewH extends PersistentDataHolder> IDataContainerRef<T, NewH> restrict(Class<NewH> newHClass) {
return new MappedDataContainerRef<>(underlying.restrict(newHClass), mapper, reverseMapper);
}
@Override
public T get() {
return mapper.apply(underlying.get());
}
@Override
public boolean has() {
return underlying.has();
}
@Override
public void set(T val) {
if(reverseMapper == null)
throw new UnsupportedOperationException("MappedDataContainerRef does not support setting values without a reverse mapper");
underlying.set(reverseMapper.apply(val));
}
@Override
public void remove() {
underlying.remove();
}
}

View file

@ -0,0 +1,57 @@
package com.ncguy.usefulskyblock.pdc;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataHolder;
import org.bukkit.persistence.PersistentDataType;
public class StrictDataContainerRef<T, H extends PersistentDataHolder> extends AnonymousDataContainerRef<T, H> {
private final H holder;
public <P> StrictDataContainerRef(H holder, NamespacedKey ref, PersistentDataType<P, T> type) {
super(ref, type);
this.holder = holder;
}
@Override
public IDataContainerRef<T, H> assign(PersistentDataHolder holder) {
throw new UnsupportedOperationException("Cannot assign a holder to a strict ref");
}
@Override
public IDataContainerRef<T, H> withSuffix(String suffix) {
return new StrictDataContainerRef<>(holder, new NamespacedKey(ref.getNamespace(), ref.getKey() + suffix), type);
}
@Override
public <P, C> IDataContainerRef<C, H> withType(PersistentDataType<P, C> newType) {
return new StrictDataContainerRef<>(holder, ref, newType);
}
private PersistentDataContainer pdc() {
return holder.getPersistentDataContainer();
}
@Override
public T get() {
if(holder == null)
return super.get();
return pdc().get(ref, type);
}
@Override
public boolean has() {
return pdc().has(ref, type);
}
@Override
public void set(T val) {
pdc().set(ref, type, val);
}
@Override
public void remove() {
pdc().remove(ref);
}
}

View file

@ -0,0 +1,37 @@
package com.ncguy.usefulskyblock.pdc;
import org.bukkit.persistence.PersistentDataAdapterContext;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.util.BlockVector;
import org.jetbrains.annotations.NotNull;
import org.joml.Vector3i;
public class VectorPersistentDataType implements PersistentDataType<int[], BlockVector> {
public static final PersistentDataType<int[], BlockVector> Instance = new VectorPersistentDataType();
@Override
public @NotNull Class<int[]> getPrimitiveType() {
return int[].class;
}
@Override
public @NotNull Class<BlockVector> getComplexType() {
return BlockVector.class;
}
@Override
public int @NotNull [] toPrimitive(@NotNull BlockVector complex, @NotNull PersistentDataAdapterContext context) {
return new int[] {
complex.getBlockX(),
complex.getBlockY(),
complex.getBlockZ()
};
}
@Override
public @NotNull BlockVector fromPrimitive(int @NotNull [] primitive, @NotNull PersistentDataAdapterContext context) {
return new BlockVector(primitive[0], primitive[1], primitive[2]);
}
}

View file

@ -1,18 +1,27 @@
package com.ncguy.usefulskyblock.recipe;
import com.ncguy.usefulskyblock.UsefulSkyblock;
import com.ncguy.usefulskyblock.events.SkyblockConfigReloadEvent;
import com.ncguy.usefulskyblock.utils.BossBarProgressMonitor;
import com.ncguy.usefulskyblock.utils.IProgressMonitor;
import com.ncguy.usefulskyblock.utils.Remark;
import com.ncguy.usefulskyblock.utils.RemarkSet;
import io.papermc.paper.entity.LookAnchor;
import io.papermc.paper.math.Position;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.util.Tick;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.*;
import org.bukkit.block.Biome;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Directional;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.BlockDisplay;
import org.bukkit.entity.Player;
@ -20,6 +29,7 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Recipe;
import org.bukkit.inventory.ShapedRecipe;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
@ -29,206 +39,239 @@ import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitScheduler;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Transformation;
import org.bukkit.util.Vector;
import org.joml.AxisAngle4f;
import org.joml.Vector3f;
import java.time.Duration;
import java.util.*;
public class BiomeRod implements Listener {
public class BiomeRod implements Listener, IRecipeProvider {
private static String Namespace = "usefulskyblock";
private static String Namespace = "usefulskyblock";
private static NamespacedKey rodkey = new NamespacedKey(Namespace, "biomerod");
private static NamespacedKey flag = new NamespacedKey(Namespace, "flag_biomerod");
private static NamespacedKey rodkey = new NamespacedKey(Namespace, "biomerod");
private static NamespacedKey flag = new NamespacedKey(Namespace, "flag_biomerod");
public static void Init(Server server) {
@Override
public Iterable<Recipe> provideRecipes() {
ItemStack sign = ItemStack.of(Material.END_ROD);
ItemMeta meta = sign.getItemMeta();
PersistentDataContainer pdc = meta.getPersistentDataContainer();
pdc.set(flag, PersistentDataType.STRING, flag.asString());
meta.setMaxStackSize(1);
meta.customName(Component.text("Biome Rod"));
meta.lore(Arrays.stream(new String[]{"A mystical tool allowing manipulation of the world."})
.map(Component::text)
.toList());
sign.setItemMeta(meta);
ItemStack sign = ItemStack.of(Material.END_ROD);
ItemMeta meta = sign.getItemMeta();
PersistentDataContainer pdc = meta.getPersistentDataContainer();
pdc.set(flag, PersistentDataType.STRING, flag.asString());
meta.setMaxStackSize(1);
meta.customName(Component.text("Biome Rod"));
meta.lore(Arrays.stream(new String[]{
"A mystical tool allowing manipulation of the world."
}).map(Component::text).toList());
sign.setItemMeta(meta);
ShapedRecipe signRecipe = new ShapedRecipe(rodkey, sign);
signRecipe.shape(" ", "DSD", " ");
signRecipe.setIngredient('D', Material.DIAMOND);
signRecipe.setIngredient('S', Material.STICK);
ShapedRecipe signRecipe = new ShapedRecipe(rodkey, sign);
signRecipe.shape(" ", "DSD", " ");
signRecipe.setIngredient('D', Material.DIAMOND);
signRecipe.setIngredient('S', Material.STICK);
return List.of(signRecipe);
}
server.addRecipe(signRecipe);
public BiomeRod() {
Plugin plugin = Bukkit.getPluginManager().getPlugin("UsefulSkyblock");
reloadBiomeMap(plugin.getConfig());
}
public RemarkSet reloadBiomeMap(Configuration config) {
RemarkSet set = new RemarkSet();
List<Map<String, String>> o = (List<Map<String, String>>) config.getList("skyblock.biomes.biome-map");
Registry<Biome> biomeRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.BIOME);
biomeMap.clear();
for (Map<String, String> m : Objects.requireNonNull(o)) {
String materialName = m.get("material");
String biomeName = m.get("biome");
Material material = Material.getMaterial(materialName);
Biome biome = biomeRegistry.get(Objects.requireNonNull(NamespacedKey.fromString(biomeName)));
if (material == null || biome == null) {
System.err.println("Invalid biome rod material or biome: " + materialName + " " + biomeName);
if (material == null) set.add("biome-map", "Invalid biome rod material: " + materialName);
if (biome == null) set.add("biome-map", "Invalid biome rod biome: " + biomeName);
continue;
}
biomeMap.put(material, biome);
}
return set;
}
private boolean isItemStackBiomeSign(ItemStack itemStack) {
if (itemStack == null) return false;
PersistentDataContainer pdc = itemStack.getItemMeta().getPersistentDataContainer();
return pdc.has(flag, PersistentDataType.STRING);
}
private Map<Material, Biome> biomeMap = new HashMap<>();
private boolean isBlockValidBiomeKey(Block block) {
return biomeMap.containsKey(block.getType());
}
@EventHandler
public void onSkyblockConfigReload(SkyblockConfigReloadEvent event) {
reloadBiomeMap(event.config).handleRemarks(remark -> {
Bukkit.getServer()
.broadcast(Component.text(remark.domain, Style.style(TextColor.fromHexString("#777")))
.appendSpace()
.append(Component.text(remark.message, Style.style(TextColor.fromHexString("#fff")))));
});
}
@EventHandler
public void onBiomeSignPlace(BlockPlaceEvent event) {
ItemStack itemInHand = event.getItemInHand();
if (!isItemStackBiomeSign(itemInHand)) return;
if (!isBlockValidBiomeKey(event.getBlockAgainst())) {
event.setBuild(false);
Player player = event.getPlayer();
player.sendMessage(Component.text(event.getBlockAgainst().getType().name() + " is not a valid biome anchor."));
return;
}
public BiomeRod() {
reloadBiomeMap();
}
final World world = event.getBlockAgainst().getWorld();
public RemarkSet reloadBiomeMap() {
RemarkSet set = new RemarkSet();
Plugin plugin = Bukkit.getPluginManager().getPlugin("UsefulSkyblock");
FileConfiguration config = plugin.getConfig();
List<Map<String, String>> o = (List<Map<String, String>>) config.getList("skyblock.biomes.biome-map");
List<Location> biomeLocations = new ArrayList<>();
Registry<Biome> biomeRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.BIOME);
JavaPlugin plugin = JavaPlugin.getPlugin(UsefulSkyblock.class);
FileConfiguration config = plugin.getConfig();
final int radius = config.getInt("skyblock.biomes.rod-radius", 4);
biomeMap.clear();
System.out.println("Modifying biomes with radius: " + radius);
for(Map<String, String> m : Objects.requireNonNull(o)) {
String materialName = m.get("material");
String biomeName = m.get("biome");
Material material = Material.getMaterial(materialName);
Biome biome = biomeRegistry.get(Objects.requireNonNull(NamespacedKey.fromString(biomeName)));
BlockVector center = new BlockVector(event.getBlockAgainst().getX(), event.getBlockAgainst().getY(),
event.getBlockAgainst().getZ());
if(material == null || biome == null) {
System.err.println("Invalid biome rod material or biome: " + materialName + " " + biomeName);
if(material == null)
set.add("biome-map", "Invalid biome rod material: " + materialName);
if(biome == null)
set.add("biome-map", "Invalid biome rod biome: " + biomeName);
continue;
}
biomeMap.put(material, biome);
for (int x = -radius; x <= radius; x += 4) {
for (int y = -radius; y <= radius; y += 4) {
for (int z = -radius; z <= radius; z += 4) {
BlockVector vec = new BlockVector(x, y, z);
vec.add(center);
if (vec.distance(center) <= radius)
biomeLocations.add(new Location(world, vec.getX(), vec.getY(), vec.getZ()));
}
return set;
}
}
Biome biome = biomeMap.get(event.getBlockAgainst().getType());
event.getPlayer()
.sendMessage(Component.text(
"Converting to biome " + biome + " with radius " + radius + " at " + center + ". Blocks to change: " + biomeLocations.size()));
Queue<Location> locationQueue = new LinkedList<>(biomeLocations.stream()
.sorted((a, b) -> (int) Math.round(a.toVector()
.distanceSquared(
center) - b.toVector()
.distanceSquared(
center)))
.toList());
BukkitScheduler scheduler = Bukkit.getServer().getScheduler();
BossBarProgressMonitor progress = new BossBarProgressMonitor(
BossBar.bossBar(Component.text(""), 0, BossBar.Color.GREEN, BossBar.Overlay.PROGRESS), event.getPlayer());
progress.setTitle(
Component.text("Converting biome to").appendSpace().append(Component.translatable(biome.translationKey())));
progress.setMaxProgress(biomeLocations.size());
progress.setInverseProgress(true);
int batchSize = Math.max(1, biomeLocations.size() / sinkDurationTicks);
final int REASONABLE_THRESHOLD = 64;
if (biomeLocations.size() < REASONABLE_THRESHOLD) batchSize = biomeLocations.size();
modifyBiomeAsync(world, locationQueue, biome, plugin, scheduler, batchSize, progress);
sinkRodIntoBlock(event.getBlockPlaced(), plugin, scheduler);
}
private static int sinkDurationTicks = Tick.tick().fromDuration(Duration.ofSeconds(2));
private static void sinkRodIntoBlock(Block block, JavaPlugin plugin, BukkitScheduler scheduler) {
final BlockData blockData = block.getBlockData();
BlockFace facing = blockData instanceof Directional ? ((Directional) blockData).getFacing() : BlockFace.SELF;
World world = block.getWorld();
world.spawn(block.getLocation(), BlockDisplay.class, e -> {
e.setBlock(blockData);
e.setTransformation(new Transformation(new Vector3f(0, 0, 0), new AxisAngle4f(0, 0, 0, 1), new Vector3f(1, 1, 1),
new AxisAngle4f(0, 0, 0, 1)));
e.setPersistent(false);
sinkRodIntoBlock(e, plugin, scheduler, sinkDurationTicks, facing);
});
block.setType(Material.AIR);
}
private static void sinkRodIntoBlock(BlockDisplay display, JavaPlugin plugin, BukkitScheduler scheduler,
int remainder, BlockFace facing) {
if (remainder == 0) {
display.remove();
return;
}
private boolean isItemStackBiomeSign(ItemStack itemStack) {
if(itemStack == null)
return false;
PersistentDataContainer pdc = itemStack.getItemMeta().getPersistentDataContainer();
return pdc.has(flag, PersistentDataType.STRING);
float progress = (float) remainder / sinkDurationTicks;
float scale = 1.0f - progress;
float meshScale = (progress * 0.5f) + 0.5f;
float meshOffset = scale * 0.5f;
Vector direction = facing.getDirection();
Vector3f scaleVec = new Vector3f(direction.toVector3f().absolute());
scaleVec.mul(1f - progress);
scaleVec = new Vector3f(1, 1, 1).sub(scaleVec);
Vector3f offset = new Vector3f();
final float EPSILON = 0.00001f;
if (direction.getX() < -EPSILON || direction.getY() < -EPSILON || direction.getZ() < -EPSILON) {
offset.set(direction.toVector3f().mul(-1)).mul(1 - progress);
}
private Map<Material, Biome> biomeMap = new HashMap<>();
display.setTransformation(new Transformation(
// new Vector3f(-meshOffset, -meshOffset, -meshOffset).mul(facing.getDirection().toVector3f()),
offset, new AxisAngle4f(0, 0, 0, 1), scaleVec, new AxisAngle4f(0, 0, 0, 1)));
private boolean isBlockValidBiomeKey(Block block) {
return biomeMap.containsKey(block.getType());
scheduler.runTaskLater(plugin, () -> sinkRodIntoBlock(display, plugin, scheduler, remainder - 1, facing), 1);
}
private static boolean modifyBiomeAsync(World world, Queue<Location> locations, Biome biome, JavaPlugin plugin,
BukkitScheduler scheduler, int batchSize, IProgressMonitor progress) {
if (locations.isEmpty()) {
progress.finish();
return true;
}
@EventHandler
public void onBiomeSignPlace(BlockPlaceEvent event) {
ItemStack itemInHand = event.getItemInHand();
if(!isItemStackBiomeSign(itemInHand))
return;
if(!isBlockValidBiomeKey(event.getBlockAgainst())) {
event.setBuild(false);
Player player = event.getPlayer();
player.sendMessage(Component.text(event.getBlockAgainst().getType().name() + " is not a valid biome anchor."));
return;
}
Set<Chunk> chunksToRefresh = new HashSet<>();
final World world = event.getBlockAgainst().getWorld();
List<Location> biomeLocations = new ArrayList<>();
JavaPlugin plugin = JavaPlugin.getPlugin(UsefulSkyblock.class);
FileConfiguration config = plugin.getConfig();
final int radius = config.getInt("skyblock.biomes.rod-radius", 4);
System.out.println("Modifying biomes with radius: " + radius);
BlockVector center = new BlockVector(event.getBlockAgainst().getX(), event.getBlockAgainst().getY(), event.getBlockAgainst().getZ());
for(int x = -radius; x <= radius; x++) {
for(int y = -radius; y <= radius; y++) {
for(int z = -radius; z <= radius; z++) {
BlockVector vec = new BlockVector(x, y, z);
vec.add(center);
if(vec.distance(center) <= radius)
biomeLocations.add(new Location(world, vec.getX(), vec.getY(), vec.getZ()));
}
}
}
Biome biome = biomeMap.get(event.getBlockAgainst().getType());
event.getPlayer().sendMessage(Component.text("Converting to biome " + biome + " with radius " + radius + " at " + center + ". Blocks to change: " + biomeLocations.size()));
Queue<Location> locationQueue = new LinkedList<>(biomeLocations.stream().sorted((a, b) -> (int) Math.round(a.toVector().distanceSquared(center) - b.toVector().distanceSquared(center))).toList());
BukkitScheduler scheduler = Bukkit.getServer().getScheduler();
BossBarProgressMonitor progress = new BossBarProgressMonitor(BossBar.bossBar(Component.text(""), 0, BossBar.Color.GREEN, BossBar.Overlay.PROGRESS), event.getPlayer());
progress.setTitle(Component.text("Converting biome to").appendSpace().append(Component.translatable(biome.translationKey())));
progress.setMaxProgress(biomeLocations.size());
progress.setInverseProgress(true);
int batchSize = biomeLocations.size() / sinkDurationTicks;
modifyBiomeAsync(world, locationQueue, biome, plugin, scheduler, batchSize, progress);
sinkRodIntoBlock(event.getBlockPlaced(), plugin, scheduler);
for (int i = 0; i < Math.min(batchSize, locations.size()); i++) {
Location loc = locations.remove();
if (loc == null) break;
world.setBiome(loc, biome);
Chunk chunkAt = world.getChunkAt(loc);
chunksToRefresh.add(chunkAt);
}
private static int sinkDurationTicks = Tick.tick().fromDuration(Duration.ofSeconds(2));
private static void sinkRodIntoBlock(Block block, JavaPlugin plugin, BukkitScheduler scheduler) {
World world = block.getWorld();
world.spawn(block.getLocation(), BlockDisplay.class, e -> {
e.setBlock(block.getBlockData());
e.setTransformation(new Transformation(
new Vector3f(0, 0, 0),
new AxisAngle4f(0, 0, 0, 1),
new Vector3f(1, 1, 1),
new AxisAngle4f(0, 0, 0, 1)
));
progress.setProgress(locations.size());
e.setPersistent(false);
sinkRodIntoBlock(e, plugin, scheduler, sinkDurationTicks);
});
block.setType(Material.AIR);
}
private static void sinkRodIntoBlock(BlockDisplay display, JavaPlugin plugin, BukkitScheduler scheduler, int remainder) {
if(remainder == 0) {
display.remove();
return;
}
chunksToRefresh.forEach(x -> world.refreshChunk(x.getX(), x.getZ()));
float progress = (float) remainder / sinkDurationTicks;
float scale = 1.0f - progress;
float meshScale = (progress * 0.5f) + 0.5f;
float meshOffset = scale * 0.5f;
display.setTransformation(new Transformation(
new Vector3f(0, -meshOffset, 0),
new AxisAngle4f(0, 0, 0, 1),
new Vector3f(1, meshScale, 1),
new AxisAngle4f(0, 0, 0, 1)
));
scheduler.runTaskLater(plugin, () -> sinkRodIntoBlock(display, plugin, scheduler, remainder - 1), 1);
if (locations.isEmpty()) {
progress.finish();
return true;
}
private static boolean modifyBiomeAsync(World world, Queue<Location> locations, Biome biome, JavaPlugin plugin, BukkitScheduler scheduler, int batchSize, IProgressMonitor progress) {
if(locations.isEmpty()) {
progress.finish();
return true;
}
Set<Chunk> chunksToRefresh = new HashSet<>();
for(int i = 0; i < Math.min(batchSize, locations.size()); i++) {
Location loc = locations.remove();
if(loc == null) {
progress.finish();
return true;
}
world.setBiome(loc, biome);
Chunk chunkAt = world.getChunkAt(loc);
chunksToRefresh.add(chunkAt);
}
progress.setProgress(locations.size());
chunksToRefresh.forEach(x -> world.refreshChunk(x.getX(), x.getZ()));
if(locations.isEmpty()) {
progress.finish();
return true;
}
scheduler.runTaskLater(plugin, () -> modifyBiomeAsync(world, locations, biome, plugin, scheduler, batchSize, progress), 1);
return false;
}
scheduler.runTaskLater(plugin,
() -> modifyBiomeAsync(world, locations, biome, plugin, scheduler, batchSize, progress), 1);
return false;
}
}

View file

@ -0,0 +1,11 @@
package com.ncguy.usefulskyblock.recipe;
import org.bukkit.Server;
import org.bukkit.inventory.Recipe;
@FunctionalInterface
public interface IRecipeProvider {
Iterable<Recipe> provideRecipes();
}

View file

@ -0,0 +1,32 @@
package com.ncguy.usefulskyblock.recipe;
import com.ncguy.usefulskyblock.Reference;
import org.bukkit.Material;
import org.bukkit.inventory.FurnaceRecipe;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Recipe;
import org.bukkit.inventory.RecipeChoice;
import java.util.ArrayList;
import java.util.List;
public class SmeltingCraftingHandler implements IRecipeProvider {
private final List<Recipe> recipes;
public SmeltingCraftingHandler() {
recipes = new ArrayList<>();
init();
}
private void init() {
recipes.add(new FurnaceRecipe(Reference.key("netherrack_to_brick"), ItemStack.of(Material.NETHER_BRICK),
Material.NETHERRACK, 0.7f, 200));
}
@Override
public Iterable<Recipe> provideRecipes() {
return recipes;
}
}

View file

@ -1,5 +1,7 @@
package com.ncguy.usefulskyblock.utils;
import org.bukkit.util.Vector;
import java.util.Random;
import java.util.stream.IntStream;
@ -70,4 +72,38 @@ public class MathsUtils {
return ring << 2;
}
public static Vector getVelocityToTarget(Vector from, Vector to) {
if (from == null || to == null) {
throw new IllegalArgumentException("from and to must not be null");
}
Vector delta = to.clone().subtract(from);
if (delta.lengthSquared() == 0.0)
return new Vector(0, 0, 0);
double dx = delta.getX();
double dy = delta.getY();
double dz = delta.getZ();
double horizontal = Math.hypot(dx, dz);
// Entity gravity magnitude in vanilla Minecraft
final double g = 0.08;
// Flight time in ticks based on horizontal distance
final double minT = 5.0; // 0.25s
final double maxT = 60.0; // 3.0s
double T = Math.max(minT, Math.min(maxT, horizontal * 1.5));
if (horizontal == 0.0)
// Vertical shot, adjust flight time based on vertical delta
T = Math.max(minT, Math.min(maxT, Math.abs(dy) * 2.0));
// Solve v from delta = v*T + 0.5*a*T^2 => v = (delta - 0.5*a*T^2)/T
double vx = dx / T;
double vz = dz / T;
double vy = (dy + 0.5 * g * T * T) / T;
return new Vector(vx, vy, vz);
}
}

View file

@ -0,0 +1,29 @@
package com.ncguy.usefulskyblock.utils;
import java.lang.reflect.Field;
public class ReflectionUtils {
public static Field getField(Object obj, String fieldName) throws NoSuchFieldException {
Class<?> cls = obj.getClass();
try {
return cls.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
if(cls.getSuperclass() != Object.class)
return getField(cls.getSuperclass(), fieldName);
throw e;
}
}
public static <T> T get(Object obj, String fieldName) {
try {
Field field = getField(obj, fieldName);
field.setAccessible(true);
return (T) field.get(obj);
} catch (IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}
}

View file

@ -1,6 +1,7 @@
package com.ncguy.usefulskyblock.utils;
import java.util.HashSet;
import java.util.function.Consumer;
public class RemarkSet extends HashSet<Remark> {
@ -8,4 +9,9 @@ public class RemarkSet extends HashSet<Remark> {
add(new Remark(domain, message));
}
public RemarkSet handleRemarks(Consumer<Remark> handler) {
this.forEach(handler);
return this;
}
}

View file

@ -0,0 +1,37 @@
package com.ncguy.usefulskyblock.utils;
import org.bukkit.scoreboard.Team;
public class TextUtils {
public static String sanitizeTeamName(Team team) {
return team.getName().replaceAll("[^a-z0-9_.]", "_");
}
public static String toTitleCase(String text) {
var lower = text.toLowerCase();
StringBuilder sb = new StringBuilder();
char[] charArray = lower.toCharArray();
for (int i = 0; i < charArray.length; i++) {
if (i == 0) {
sb.append(Character.toUpperCase(charArray[i]));
continue;
}
if(charArray[i] == '_' || charArray[i] == ' ') {
sb.append(' ');
continue;
}
if(charArray[i-1] == ' ' || charArray[i-1] == '_') {
sb.append(Character.toUpperCase(charArray[i]));
continue;
}
sb.append(charArray[i]);
}
return sb.toString();
}
}

View file

@ -1,35 +1,69 @@
package com.ncguy.usefulskyblock.world;
import com.ncguy.usefulskyblock.command.SkyblockGenCommand;
import io.papermc.paper.math.BlockPosition;
import org.bukkit.Axis;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.BlockState;
import org.bukkit.block.data.Orientable;
import org.bukkit.entity.Entity;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.PortalCreateEvent;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.util.BoundingBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Optional;
import static org.bukkit.plugin.java.JavaPlugin.getProvidingPlugin;
public class PortalHandler implements Listener {
private static final Logger log = LoggerFactory.getLogger(PortalHandler.class);
@EventHandler
public void onPortalCreate(PortalCreateEvent event) {
log.info("Portal created: {}, reason: {}", event, event.getReason().name());
if(event.getReason() != PortalCreateEvent.CreateReason.NETHER_PAIR)
return;
Entity entity = event.getEntity();
log.info("Portal entity: {}", entity);
if(entity == null)
return;
BoundingBox bb = new BoundingBox();
List<BlockState> blocks = event.getBlocks();
bb.shift(blocks.getFirst().getLocation());
for (int i = 1; i < blocks.size(); i++)
bb.expand(blocks.get(i).getLocation().toVector());
BlockState first = blocks.getFirst();
boolean isXForward = bb.getWidthX() < bb.getWidthZ();
Optional<BlockState> portalState = event.getBlocks().stream().filter(x -> x.getBlock().getType() == Material.NETHER_PORTAL).findFirst();
for (BlockState block : event.getBlocks()) {
log.info("Block: {}, type: {}", block, block.getType());
if(block.getType() == Material.NETHER_PORTAL) {
portalState = Optional.of(block);
log.info("Found portal block: {}", block);
break;
}
}
BlockState blockState1 = portalState.get();
final Axis portalAxis = ((Orientable) blockState1.getBlockData()).getAxis();
Location location = first.getLocation();
log.info("Removing old portal, block count: {}", blocks.size());
blocks.forEach(blockState -> blockState.setType(Material.AIR));
log.info("Created portal at {}", location);
Bukkit.getScheduler().runTaskLater(getProvidingPlugin(getClass()), () -> {
Location generateOrigin = SkyblockGenCommand.generateNetherIslands(location, portalAxis);
entity.teleport(generateOrigin);
}, 1);
}
}

View file

@ -1,4 +1,6 @@
skyblock:
fishing:
sand_chance: 0.4
biomes:
rod-radius: 16
biome-map:
@ -14,4 +16,5 @@ skyblock:
biome: "minecraft:forest"
- material: "SNOW_BLOCK"
biome: "minecraft:snowy_plains"
- material: "CRAFTING_TABLE"
biome: "minecraft:nether_wastes"

View file

@ -0,0 +1,8 @@
{
"criteria": {
"never": { "trigger": "minecraft:impossible" }
},
"requirements": [
["never"]
]
}

View file

@ -0,0 +1,8 @@
{
"criteria": {
"never": { "trigger": "minecraft:impossible" }
},
"requirements": [
["never"]
]
}

View file

@ -0,0 +1,8 @@
{
"criteria": {
"never": { "trigger": "minecraft:impossible" }
},
"requirements": [
["never"]
]
}

View file

@ -0,0 +1,8 @@
{
"criteria": {
"never": { "trigger": "minecraft:impossible" }
},
"requirements": [
["never"]
]
}

View file

@ -1,11 +1,12 @@
{
"type": "minecraft:the_nether",
"generator": {
"type": "flat",
"settings":{
"layers": [],
"biome": "minecraft:hell",
"structure_overrides": []
"type": "minecraft:flat",
"settings": {
"biome": "minecraft:nether_wastes",
"lakes": false,
"features": true,
"layers": []
}
}
}

View file

@ -0,0 +1,20 @@
{
"display": {
"icon": {
"id": "minecraft:wheat"
},
"title": "Farming",
"description": "What do you intend to eat",
"background": "minecraft:block/oak_log",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": true
},
"criteria": {
"eat-food-for-farm": {
"trigger": "minecraft:consume_item"
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,28 @@
{
"display": {
"icon": {
"id": "minecraft:beetroot"
},
"title": "Get a Carrot",
"description": "See in the dark",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:farming/farm_unlock",
"criteria": {
"collect-wheat": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:beetroot",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,28 @@
{
"display": {
"icon": {
"id": "minecraft:cactus"
},
"title": "Get cactus",
"description": "Don't get pricked",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:farming/farm_unlock",
"criteria": {
"collect-wheat": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:cactus",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,28 @@
{
"display": {
"icon": {
"id": "minecraft:carrot"
},
"title": "Get a Carrot",
"description": "See in the dark",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:farming/farm_unlock",
"criteria": {
"collect-wheat": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:carrot",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,20 @@
{
"display": {
"icon": {
"id": "minecraft:tropical_fish"
},
"title": "Catch a fish",
"description": "Teach a man how to fish...",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:farming/farm_unlock",
"criteria": {
"collect-wheat": {
"trigger": "minecraft:fishing_rod_hooked"
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,40 @@
{
"display": {
"icon": {
"id": "minecraft:honey_bottle"
},
"title": "Do You Want To Bee Friends?",
"description":"Harvest Honey from a Bee Nest from a tree grown near flowers",
"frame": "task",
"show_toast": true,
"announce_to_chat": true,
"hidden": false
},
"parent": "usefulskyblock:farming/farm_unlock",
"criteria": {
"harvest_honey": {
"trigger": "minecraft:item_used_on_block",
"conditions": {
"location": [
{
"condition": "minecraft:location_check",
"predicate": {
"block": {
"blocks": "#minecraft:beehives"
}
}
},
{
"condition": "minecraft:match_tool",
"predicate": {
"items": [
"minecraft:glass_bottle"
]
}
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,39 @@
{
"display": {
"icon": {
"id": "minecraft:red_mushroom"
},
"title": "Mushroom!",
"description": "There's plenty of room here, wait... I messed up",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:farming/farm_unlock",
"criteria": {
"get-red-mushroom": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:red_mushroom",
"count": 1
}
]
}
},
"get-brown-mushroom": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:brown_mushroom",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,28 @@
{
"display": {
"icon": {
"id": "minecraft:pumpkin"
},
"title": "Get a Pumpkin",
"description": "TODO",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:farming/farm_unlock",
"criteria": {
"collect-wheat": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:pumpkin",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,28 @@
{
"display": {
"icon": {
"id": "minecraft:sugar_cane"
},
"title": "Get some Sugarcane",
"description": "Sweet!",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:farming/farm_unlock",
"criteria": {
"collect-wheat": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:sugar_cane",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,28 @@
{
"display": {
"icon": {
"id": "minecraft:wheat"
},
"title": "Get Wheat",
"description": "If only we had a millstone",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:farming/farm_unlock",
"criteria": {
"collect-wheat": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:wheat",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,21 @@
{
"display": {
"icon": {
"id": "minecraft:porkchop"
},
"title": "New Friends",
"description": "Create a patch of grass and stand 24-48 blocks away to let passive mobs spawn. Then breed them.",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:farming/farm_unlock",
"criteria": {
"breed": {
"trigger": "minecraft:bred_animals",
"conditions": {}
}
},
"sends_telemetry_event": true
}

View file

@ -0,0 +1,29 @@
{
"display": {
"icon": {
"id": "minecraft:copper_ingot"
},
"title": "Underwater Mob Farm",
"description": "Obtain a Copper Ingot from killing Drowned",
"frame": "task",
"show_toast": true,
"announce_to_chat": true,
"hidden": false
},
"parent": "usefulskyblock:monsters/zombie_hunter",
"criteria": {
"drowned": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": [
"minecraft:copper_ingot"
]
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,29 @@
{
"display": {
"icon": {
"id": "minecraft:gunpowder"
},
"title": "It Would Be A Ssshame...",
"description": "Obtain Gunpowder",
"frame": "task",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:monsters/monster_unlock",
"criteria": {
"gunpowder": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": [
"minecraft:gunpowder"
]
}
]
}
}
},
"sends_telemetry_event": true
}

View file

@ -0,0 +1,31 @@
{
"display": {
"icon": {
"id": "minecraft:wither_skeleton_skull"
},
"title": "Defeat the Wither",
"description": "A significant milestone",
"frame": "challenge",
"show_toast": true,
"announce_to_chat": true,
"hidden": false
},
"parent": "usefulskyblock:monsters/skeleton_hunter",
"criteria": {
"slay-wither": {
"trigger": "minecraft:player_killed_entity",
"conditions": {
"entity": [
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"type": "minecraft:wither"
}
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,29 @@
{
"display": {
"icon": {
"id": "minecraft:ender_pearl"
},
"title": "Enderman Hunter",
"description": "Obtain an Ender Pearl",
"frame": "task",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:monsters/monster_unlock",
"criteria": {
"ender_pearl": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": [
"minecraft:ender_pearl"
]
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,43 @@
{
"display": {
"icon": {
"id": "minecraft:lightning_rod"
},
"title": "Terrifying Lightning",
"description": "Witness a Pig being struck by lightning and transforming into a Zombified Piglin",
"frame": "goal",
"show_toast": true,
"announce_to_chat": true,
"hidden": false
},
"parent": "usefulskyblock:monsters/copper",
"criteria": {
"lightning_rod_pigs": {
"trigger": "minecraft:lightning_strike",
"conditions": {
"player": {
"type_specific": {
"type": "minecraft:player",
"looking_at": {
"type": "minecraft:zombified_piglin"
}
}
},
"lightning": [
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"distance": {
"absolute": {
"max": 30
}
}
}
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,72 @@
{
"display": {
"icon": {
"id": "minecraft:stone_sword"
},
"title": "Time to Get Deadly",
"description": "Build a mob farm",
"background": "minecraft:iron_block",
"frame": "challenge",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"criteria": {
"hostile_mob": {
"trigger": "minecraft:player_killed_entity",
"conditions": {
"player": [],
"entity": [
{
"condition": "minecraft:any_of",
"terms": [
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"type": "minecraft:creeper"
}
},
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"type": "minecraft:enderman"
}
},
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"type": "minecraft:skeleton"
}
},
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"type": "minecraft:spider"
}
},
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"type": "minecraft:witch"
}
},
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"type": "minecraft:zombie"
}
}
]
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,31 @@
{
"display": {
"icon": {
"id": "minecraft:bone"
},
"title": "Skeleton Hunter",
"description": "Obtain Bones",
"frame": "task",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:monsters/monster_unlock",
"criteria": {
"skeleton": {
"trigger": "minecraft:player_killed_entity",
"conditions": {
"entity": [
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"type": "minecraft:skeleton"
}
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,31 @@
{
"display": {
"icon": {
"id": "minecraft:string"
},
"title": "Spider Hunter",
"description": "Kill a Spider",
"frame": "task",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:monsters/monster_unlock",
"criteria": {
"spider": {
"trigger": "minecraft:player_killed_entity",
"conditions": {
"entity": [
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"type": "minecraft:spider"
}
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,39 @@
{
"display": {
"icon": {
"id": "minecraft:potion",
"components": {
"minecraft:potion_contents": "healing"
}
},
"title": "Dangerous Passenger",
"description": "Capture a Witch in a Boat",
"frame": "task",
"show_toast": true,
"announce_to_chat": true,
"hidden": false
},
"parent": "usefulskyblock:monsters/monster_unlock",
"criteria": {
"witch_passenger": {
"trigger": "minecraft:started_riding",
"conditions": {
"player": [
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"vehicle": {
"type": "#minecraft:boat",
"passenger": {
"type": "minecraft:witch"
}
}
}
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,41 @@
{
"display": {
"icon": {
"id": "minecraft:splash_potion",
"components": {
"minecraft:potion_contents": {
"potion": "minecraft:weakness"
}
}
},
"title": "(Slightly Less) Dangerous Passenger",
"description": "Capture a Zombie Villager in a Boat",
"frame": "task",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:monsters/witch_hostage",
"criteria": {
"zombie_villager_passenger": {
"trigger": "minecraft:started_riding",
"conditions": {
"player": [
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"vehicle": {
"type": "#minecraft:boat",
"passenger": {
"type": "minecraft:zombie_villager"
}
}
}
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,31 @@
{
"display": {
"icon": {
"id": "minecraft:rotten_flesh"
},
"title": "Zombie Hunter",
"description": "Kill a Zombie",
"frame": "task",
"show_toast": false,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:monsters/monster_unlock",
"criteria": {
"zombie": {
"trigger": "minecraft:player_killed_entity",
"conditions": {
"entity": [
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"type": "minecraft:zombie"
}
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,20 @@
{
"display": {
"icon": {
"id": "minecraft:netherrack"
},
"title": "Create Netherrack",
"description": "This doesn't seem right\n\n",
"frame": "challenge",
"show_toast": true,
"announce_to_chat": false,
"hidden": true
},
"parent": "usefulskyblock:nether/tier_1_unlock",
"criteria": {
"craft-netherrack": {
"trigger": "minecraft:impossible"
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,20 @@
{
"display": {
"icon": {
"id": "minecraft:bell"
},
"title": "Saving a villager",
"description": "They seem a bit hungry for something other than brains now",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": true
},
"parent": "usefulskyblock:nether/golden_apple",
"criteria": {
"cure-villager": {
"trigger": "minecraft:cured_zombie_villager"
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,24 @@
{
"display": {
"icon": {
"id": "minecraft:grass_block"
},
"title": "The Nether!",
"description": "It's a bit toasty here",
"frame": "task",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:nether/tier_1_unlock",
"criteria": {
"enter-nether": {
"trigger": "minecraft:changed_dimension",
"conditions": {
"from": "minecraft:overworld",
"to": "minecraft:the_nether"
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,28 @@
{
"display": {
"icon": {
"id": "minecraft:gold_ingot"
},
"title": "Gold",
"description": "Pigs love it",
"frame": "task",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:nether/enter_nether",
"criteria": {
"attain-cobble": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:gold_ingot",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,28 @@
{
"display": {
"icon": {
"id": "minecraft:gravel"
},
"title": "Gravel",
"description": "Worth it's weight in gold, somehow",
"frame": "task",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:nether/enter_nether",
"criteria": {
"attain-cobble": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:gravel",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,28 @@
{
"display": {
"icon": {
"id": "minecraft:golden_apple"
},
"title": "I've got a Golden Apple",
"description": "What would warrant such a thing",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:nether/get_gold",
"criteria": {
"get-gold-apple": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:golden_apple",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,28 @@
{
"display": {
"icon": {
"id": "minecraft:nether_brick"
},
"title": "Smelting netherrack",
"description": "Something about bricks",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:nether/craft_netherrack",
"criteria": {
"smelt-netherbrick": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:nether_brick",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,31 @@
{
"display": {
"icon": {
"id": "minecraft:oak_sapling"
},
"title": "The first tier",
"description": "Your new home between the clouds",
"background": "minecraft:block/oak_log",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": true
},
"criteria": {
"cobble-stack": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"player": {
"type_specific": {
"type": "minecraft:player",
"advancements": {
"usefulskyblock:skyblock/collect_64_cobble": true,
"usefulskyblock:skyblock/eat_food": true
}
}
}
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,34 @@
{
"display": {
"icon": {
"id": "minecraft:cobblestone",
"count": 64
},
"title": "10K of Cobblestone\n...Why?",
"description": "No, really; Why?",
"frame": "challenge",
"announce_to_chat": true,
"hidden": false
},
"parent": "usefulskyblock:skyblock/collect_1728_cobble",
"criteria": {
"attain-cobble-10000": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"player": {
"type_specific": {
"type": "minecraft:player",
"stats": [
{
"type": "minecraft:mined",
"stat": "minecraft:cobblestone",
"value": 10000
}
]
}
}
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,34 @@
{
"display": {
"icon": {
"id": "minecraft:cobblestone",
"count": 16
},
"title": "A tonne of Cobblestone!",
"description": "A healthy amount",
"frame": "challenge",
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:skyblock/collect_64_cobble",
"criteria": {
"attain-cobble-1024": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"player": {
"type_specific": {
"type": "minecraft:player",
"stats": [
{
"type": "minecraft:mined",
"stat": "minecraft:cobblestone",
"value": 1024
}
]
}
}
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,34 @@
{
"display": {
"icon": {
"id": "minecraft:cobblestone",
"count": 32
},
"title": "A single chest of Cobblestone!",
"description": "You can start building now",
"frame": "challenge",
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:skyblock/collect_1024_cobble",
"criteria": {
"attain-cobble-1728": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"player": {
"type_specific": {
"type": "minecraft:player",
"stats": [
{
"type": "minecraft:mined",
"stat": "minecraft:cobblestone",
"value": 1728
}
]
}
}
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,34 @@
{
"display": {
"icon": {
"id": "minecraft:cobblestone",
"count": 8
},
"title": "A stack of Cobblestone!",
"description": "A good start",
"frame": "challenge",
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:skyblock/get_cobble",
"criteria": {
"attain-cobble-64": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"player": {
"type_specific": {
"type": "minecraft:player",
"stats": [
{
"type": "minecraft:mined",
"stat": "minecraft:cobblestone",
"value": 64
}
]
}
}
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,25 @@
{
"display": {
"icon": {
"id": "minecraft:beacon"
},
"title": "The beacons are lit",
"description": "Gondor calls for aid",
"frame": "task",
"show_toast": true,
"announce_to_chat": true,
"hidden": false
},
"parent": "usefulskyblock:monsters/defeat_wither",
"criteria": {
"example": {
"trigger": "minecraft:construct_beacon",
"conditions": {
"level": {
"min": 1
}
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,20 @@
{
"display": {
"icon": {
"id": "minecraft:apple"
},
"title": "Eat food",
"description": "400 kCal is not a meal!",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": true
},
"parent": "usefulskyblock:skyblock/skyblock_begin",
"criteria": {
"eat-food": {
"trigger": "minecraft:consume_item"
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,29 @@
{
"display": {
"icon": {
"id": "minecraft:clay"
},
"title": "Dried Wet Dirt",
"description": "Place a Pointed Dripstone underneath Mud to drip out its moisture and create Clay",
"frame": "task",
"show_toast": true,
"announce_to_chat": true,
"hidden": false
},
"parent": "usefulskyblock:skyblock/get_mud",
"criteria": {
"clay_ball": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": [
"minecraft:clay_ball"
]
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,28 @@
{
"display": {
"icon": {
"id": "minecraft:cobblestone"
},
"title": "Cobblestone!",
"description": "The first of many..",
"frame": "task",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:skyblock/skyblock_begin",
"criteria": {
"attain-cobble": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:cobblestone",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,28 @@
{
"display": {
"icon": {
"id": "minecraft:glass"
},
"title": "Glass time",
"description": "Don't smash it\n\nIf you can't figure out how to get more sand, take some time away and have a good meal. I hear fish can help you think",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:skyblock/skyblock_begin",
"criteria": {
"get-seeds": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:glass",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,27 @@
{
"display": {
"icon": {
"id": "minecraft:iron_ingot"
},
"title": "Iron",
"description": "Iron you glad I didn't say banana",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:monsters/monster_unlock",
"criteria": {
"get-iron": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": ["minecraft:iron_ingot"]
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,41 @@
{
"display": {
"icon": {
"id": "minecraft:iron_pickaxe"
},
"title": "Iron Tools",
"description": "The Iron Age!\n\n - Craft an iron pickaxe, axe, hoe, and shovel",
"frame": "task",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:skyblock/get_stone_tools",
"criteria": {
"get-pickaxe": {
"trigger": "minecraft:recipe_crafted",
"conditions": {
"recipe_id": "minecraft:iron_pickaxe"
}
},
"get-axe": {
"trigger": "minecraft:recipe_crafted",
"conditions": {
"recipe_id": "minecraft:iron_axe"
}
},
"get-hoe": {
"trigger": "minecraft:recipe_crafted",
"conditions": {
"recipe_id": "minecraft:iron_hoe"
}
},
"get-shovel": {
"trigger": "minecraft:recipe_crafted",
"conditions": {
"recipe_id": "minecraft:iron_shovel"
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,34 @@
{
"display": {
"icon": {
"id": "minecraft:oak_log"
},
"title": "Timber Collection",
"description": "A solid oak floor",
"frame": "task",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:skyblock/skyblock_begin",
"criteria": {
"get-logs": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"condition": "minecraft:match_tool",
"predicate": {
"items": [
{
"tag": "minecraft:oak_logs"
}
]
}
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,40 @@
{
"display": {
"icon": {
"id": "minecraft:mud"
},
"title": "Glorious mud",
"description": "Use a bottle of water on some dirt",
"frame": "task",
"show_toast": true,
"announce_to_chat": true,
"hidden": false
},
"parent": "usefulskyblock:skyblock/renew_dirt",
"criteria": {
"clay_ball": {
"trigger": "minecraft:item_used_on_block",
"conditions": {
"location": [
{
"condition": "minecraft:match_tool",
"predicate": {
"items": [
"minecraft:potion"
]
}
},
{
"condition": "minecraft:location_check",
"predicate": {
"block": {
"blocks": "minecraft:dirt"
}
}
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,28 @@
{
"display": {
"icon": {
"id": "minecraft:wheat_seeds"
},
"title": "Wheat",
"description": "Let's get this bread",
"frame": "goal",
"show_toast": true,
"announce_to_chat": false,
"hidden": true
},
"parent": "usefulskyblock:skyblock/skyblock_begin",
"criteria": {
"get-seeds": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"items": "minecraft:wheat_seeds",
"count": 1
}
]
}
}
},
"sends_telemetry_event": false
}

View file

@ -0,0 +1,41 @@
{
"display": {
"icon": {
"id": "minecraft:stone_pickaxe"
},
"title": "Stone Tools",
"description": "Rock and Stone!\n\n - Craft a stone pickaxe, axe, hoe, and shovel",
"frame": "task",
"show_toast": true,
"announce_to_chat": false,
"hidden": false
},
"parent": "usefulskyblock:skyblock/get_cobble",
"criteria": {
"get-pickaxe": {
"trigger": "minecraft:recipe_crafted",
"conditions": {
"recipe_id": "minecraft:stone_pickaxe"
}
},
"get-axe": {
"trigger": "minecraft:recipe_crafted",
"conditions": {
"recipe_id": "minecraft:stone_axe"
}
},
"get-hoe": {
"trigger": "minecraft:recipe_crafted",
"conditions": {
"recipe_id": "minecraft:stone_hoe"
}
},
"get-shovel": {
"trigger": "minecraft:recipe_crafted",
"conditions": {
"recipe_id": "minecraft:stone_shovel"
}
}
},
"sends_telemetry_event": false
}

Some files were not shown because too many files have changed in this diff Show more