diff --git a/achievement-crafter/build.gradle b/achievement-crafter/build.gradle new file mode 100644 index 0000000..771258a --- /dev/null +++ b/achievement-crafter/build.gradle @@ -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"] \ No newline at end of file diff --git a/achievement-crafter/src/main/java/com/ncguy/achievements/Crafter.java b/achievement-crafter/src/main/java/com/ncguy/achievements/Crafter.java new file mode 100644 index 0000000..a9e7baf --- /dev/null +++ b/achievement-crafter/src/main/java/com/ncguy/achievements/Crafter.java @@ -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); + } +} diff --git a/achievement-crafter/src/main/java/com/ncguy/achievements/data/TreeNode.java b/achievement-crafter/src/main/java/com/ncguy/achievements/data/TreeNode.java new file mode 100644 index 0000000..9981e9e --- /dev/null +++ b/achievement-crafter/src/main/java/com/ncguy/achievements/data/TreeNode.java @@ -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> { + + public List children; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public Optional 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); + } +} diff --git a/achievement-crafter/src/main/java/com/ncguy/achievements/data/gen/Advancement.java b/achievement-crafter/src/main/java/com/ncguy/achievements/data/gen/Advancement.java new file mode 100644 index 0000000..946e806 --- /dev/null +++ b/achievement-crafter/src/main/java/com/ncguy/achievements/data/gen/Advancement.java @@ -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 { + + 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 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> 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 getCriteria() { + return criteria; + } + + public void setCriteria(Map criteria) { + this.criteria = criteria; + } + + public List> getRequirements() { + return requirements; + } + + public void setRequirements(List> 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 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 getComponents() { + return components; + } + + public void setComponents(Map components) { + this.components = components; + } + } + + /** + * Rewards section of an advancement. + */ + public static class Rewards { + private Integer experience; + private List recipes; + private List loot; + private String function; + + public Rewards() {} + + public Integer getExperience() { + return experience; + } + + public void setExperience(Integer experience) { + this.experience = experience; + } + + public List getRecipes() { + return recipes; + } + + public void setRecipes(List recipes) { + this.recipes = recipes; + } + + public List getLoot() { + return loot; + } + + public void setLoot(List loot) { + this.loot = loot; + } + + public String getFunction() { + return function; + } + + public void setFunction(String function) { + this.function = function; + } + } +} diff --git a/achievement-crafter/src/main/java/com/ncguy/achievements/data/gen/AdvancementTreeSetLoader.java b/achievement-crafter/src/main/java/com/ncguy/achievements/data/gen/AdvancementTreeSetLoader.java new file mode 100644 index 0000000..6c33f6d --- /dev/null +++ b/achievement-crafter/src/main/java/com/ncguy/achievements/data/gen/AdvancementTreeSetLoader.java @@ -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 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 first = orphanedAdvancements.stream().filter(x -> Objects.equals(x.getId(), last.parentId)).findFirst(); + first.ifPresent(parent -> { + parent.addChild(last); + }); + orphanedAdvancements.remove(last); + } + } + +} diff --git a/achievement-crafter/src/main/java/com/ncguy/achievements/data/gen/AdvancementTypeAdapter.java b/achievement-crafter/src/main/java/com/ncguy/achievements/data/gen/AdvancementTypeAdapter.java new file mode 100644 index 0000000..f52a533 --- /dev/null +++ b/achievement-crafter/src/main/java/com/ncguy/achievements/data/gen/AdvancementTypeAdapter.java @@ -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 { + + 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>() {}.getType(); + adv.setCriteria(gson.fromJson(obj.get(K_CRITERIA), t)); + } + if (obj.has(K_REQUIREMENTS)) { + Type t = new TypeToken>>() {}.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(); + } +} diff --git a/achievement-crafter/src/main/java/com/ncguy/achievements/ui/TreeSplitPanePanel.java b/achievement-crafter/src/main/java/com/ncguy/achievements/ui/TreeSplitPanePanel.java new file mode 100644 index 0000000..a5e20c7 --- /dev/null +++ b/achievement-crafter/src/main/java/com/ncguy/achievements/ui/TreeSplitPanePanel.java @@ -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; + } + } + } +} diff --git a/achievement-crafter/src/main/java/com/ncguy/achievements/ui/editor/AdvancementEditorPanel.java b/achievement-crafter/src/main/java/com/ncguy/achievements/ui/editor/AdvancementEditorPanel.java new file mode 100644 index 0000000..4aa2d96 --- /dev/null +++ b/achievement-crafter/src/main/java/com/ncguy/achievements/ui/editor/AdvancementEditorPanel.java @@ -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 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 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("")); + 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 enhance(String fieldPath, T component) { + // Let TypeAdvisor replace or decorate the editor + if(advisor != null) { + Optional 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>() {}.getType(); + advancement.setCriteria(parseJsonNullable(criteriaJson.getText(), critType)); + Type reqType = new TypeToken>>() {}.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>() {}.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 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; + } +} diff --git a/achievement-crafter/src/main/java/com/ncguy/achievements/ui/editor/extensions/AutoCompleteProvider.java b/achievement-crafter/src/main/java/com/ncguy/achievements/ui/editor/extensions/AutoCompleteProvider.java new file mode 100644 index 0000000..84f02c6 --- /dev/null +++ b/achievement-crafter/src/main/java/com/ncguy/achievements/ui/editor/extensions/AutoCompleteProvider.java @@ -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> suggestions(String fieldPath, String currentInput) { + return Optional.empty(); + } +} diff --git a/achievement-crafter/src/main/java/com/ncguy/achievements/ui/editor/extensions/TypeAdvisor.java b/achievement-crafter/src/main/java/com/ncguy/achievements/ui/editor/extensions/TypeAdvisor.java new file mode 100644 index 0000000..305cef0 --- /dev/null +++ b/achievement-crafter/src/main/java/com/ncguy/achievements/ui/editor/extensions/TypeAdvisor.java @@ -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 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> provideValueAdapter(String fieldPath) { + return Optional.empty(); + } + + /** + * Simple bidirectional conversion contract for editor values. + */ + interface ValueAdapter { + /** 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); + } +} diff --git a/achievement-crafter/src/main/java/com/ncguy/achievements/utils/ReflectionUtils.java b/achievement-crafter/src/main/java/com/ncguy/achievements/utils/ReflectionUtils.java new file mode 100644 index 0000000..146f7fb --- /dev/null +++ b/achievement-crafter/src/main/java/com/ncguy/achievements/utils/ReflectionUtils.java @@ -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 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; + } + +} diff --git a/annotation-processor/src/main/java/com/ncguy/annotations/Remarkable.java b/annotation-processor/src/main/java/com/ncguy/annotations/Remarkable.java deleted file mode 100644 index 35ee21d..0000000 --- a/annotation-processor/src/main/java/com/ncguy/annotations/Remarkable.java +++ /dev/null @@ -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 { -} diff --git a/annotation-processor/src/main/java/com/ncguy/processors/RemarkableProcessor.java b/annotation-processor/src/main/java/com/ncguy/processors/RemarkableProcessor.java deleted file mode 100644 index 2f4e87a..0000000 --- a/annotation-processor/src/main/java/com/ncguy/processors/RemarkableProcessor.java +++ /dev/null @@ -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 annotations, RoundEnvironment roundEnv) { - return false; - } - -} diff --git a/build.gradle b/build.gradle index 4353375..f3548b8 100644 --- a/build.gradle +++ b/build.gradle @@ -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 \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..01916dd --- /dev/null +++ b/buildSrc/build.gradle @@ -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") +} \ No newline at end of file diff --git a/buildSrc/src/main/java/com/ncguy/usefulskyblock/generators/AdvancementGenerator.java b/buildSrc/src/main/java/com/ncguy/usefulskyblock/generators/AdvancementGenerator.java new file mode 100644 index 0000000..48fe229 --- /dev/null +++ b/buildSrc/src/main/java/com/ncguy/usefulskyblock/generators/AdvancementGenerator.java @@ -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 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 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(); + } + +} diff --git a/buildSrc/src/main/java/com/ncguy/usefulskyblock/generators/ClassBuilder.java b/buildSrc/src/main/java/com/ncguy/usefulskyblock/generators/ClassBuilder.java new file mode 100644 index 0000000..2d60d5b --- /dev/null +++ b/buildSrc/src/main/java/com/ncguy/usefulskyblock/generators/ClassBuilder.java @@ -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 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 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; + } + + } + +} diff --git a/outline.txt b/outline.txt new file mode 100644 index 0000000..6be9e63 --- /dev/null +++ b/outline.txt @@ -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 generator’s 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 witch’s 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. diff --git a/settings.gradle b/settings.gradle index df924b5..cc0b71d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,4 +8,5 @@ plugins { } rootProject.name = 'UsefulSkyblock' -include 'annotation-processor' \ No newline at end of file +include(':achievement-crafter') + diff --git a/src/main/java/com/ncguy/usefulskyblock/AdvancementRef.java b/src/main/java/com/ncguy/usefulskyblock/AdvancementRef.java new file mode 100644 index 0000000..f937528 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/AdvancementRef.java @@ -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 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 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(); + } +} diff --git a/src/main/java/com/ncguy/usefulskyblock/IslandNetworkGenerator.java b/src/main/java/com/ncguy/usefulskyblock/IslandNetworkGenerator.java new file mode 100644 index 0000000..b4fff54 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/IslandNetworkGenerator.java @@ -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 yNoise; + + public TierDefinition(int tier, int slotCount, float spacing, StructureRef[] structures, boolean canRotate, boolean canMirror, Supplier 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 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 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 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 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 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 bedrock = Optional.empty(); + + + + Biome newBiome = null; + boolean hasNewBiome = false; + if(structure instanceof BiomedStructure) { + newBiome = ((BiomedStructure) structure).biome; + hasNewBiome = true; + } + + Set 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 randomEnum(Class enumCls) { + assert (enumCls.isEnum()); + return randomElement(enumCls.getEnumConstants()); + } + + private 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 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); + } + } + +} diff --git a/src/main/java/com/ncguy/usefulskyblock/Reference.java b/src/main/java/com/ncguy/usefulskyblock/Reference.java new file mode 100644 index 0000000..8c59dfd --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/Reference.java @@ -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 SKYBLOCK_TEAM_ROOT = new AnonymousDataContainerRef<>(key("world.skyblock.team"), VectorPersistentDataType.Instance); + public static IDataContainerRef SKYBLOCK_TEAM_COUNT = SKYBLOCK_TEAM_ROOT.withSuffix(".count").withType(PersistentDataType.INTEGER); + public static IDataContainerRef WORLD_INIT = new AnonymousDataContainerRef<>(key("world.init"), PersistentDataType.BOOLEAN); + + public static IDataContainerRef ISLAND_HOME_LOC = new AnonymousDataContainerRef<>(key("player.island.home.loc"), VectorPersistentDataType.Instance); + + public static IDataContainerRef ITEM_LAVA_IMMUNE = new AnonymousDataContainerRef<>(key("item.lava.immune"), PersistentDataType.BOOLEAN); + +} diff --git a/src/main/java/com/ncguy/usefulskyblock/SkyblockTeam.java b/src/main/java/com/ncguy/usefulskyblock/SkyblockTeam.java new file mode 100644 index 0000000..7bee29d --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/SkyblockTeam.java @@ -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 homeIslandLoc; + private IDataContainerRef 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); + } + } +} diff --git a/src/main/java/com/ncguy/usefulskyblock/StructureRef.java b/src/main/java/com/ncguy/usefulskyblock/StructureRef.java index f1afdc3..fa51342 100644 --- a/src/main/java/com/ncguy/usefulskyblock/StructureRef.java +++ b/src/main/java/com/ncguy/usefulskyblock/StructureRef.java @@ -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; } } } diff --git a/src/main/java/com/ncguy/usefulskyblock/UsefulSkyblock.java b/src/main/java/com/ncguy/usefulskyblock/UsefulSkyblock.java index 989da38..4aa02be 100644 --- a/src/main/java/com/ncguy/usefulskyblock/UsefulSkyblock.java +++ b/src/main/java/com/ncguy/usefulskyblock/UsefulSkyblock.java @@ -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 recipes = provider.provideRecipes(); + Iterator 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); - } - - } diff --git a/src/main/java/com/ncguy/usefulskyblock/command/AbstractSkyblockCommand.java b/src/main/java/com/ncguy/usefulskyblock/command/AbstractSkyblockCommand.java index 7617424..ae6e19a 100644 --- a/src/main/java/com/ncguy/usefulskyblock/command/AbstractSkyblockCommand.java +++ b/src/main/java/com/ncguy/usefulskyblock/command/AbstractSkyblockCommand.java @@ -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() { diff --git a/src/main/java/com/ncguy/usefulskyblock/command/SkyblockAdminCommand.java b/src/main/java/com/ncguy/usefulskyblock/command/SkyblockAdminCommand.java index 6e185ec..4936637 100644 --- a/src/main/java/com/ncguy/usefulskyblock/command/SkyblockAdminCommand.java +++ b/src/main/java/com/ncguy/usefulskyblock/command/SkyblockAdminCommand.java @@ -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 ctx) { + public int executeStructuresList(CommandContext ctx) { - StructureManager structureManager = getServer().getStructureManager(); - Map structureMap = structureManager.getStructures(); + StructureManager structureManager = getServer().getStructureManager(); + Map structureMap = structureManager.getStructures(); -// Set filteredKeys = structureMap.keySet().stream().filter(k -> k.getNamespace() == "usefulskyblock").collect(Collectors.toSet()); - Set filteredKeys = structureMap.keySet(); + Set 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 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 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 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 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 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 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 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 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 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 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 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 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 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 ctx) { + ctx.getSource().getExecutor().sendMessage("Building central end portal"); + InitialisationHandler.initOverworld(ctx.getSource().getLocation().getWorld(), true); + return 0; + } - return 0; + private int executeIslandUnlock(CommandContext 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 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 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 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 { + 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 context) { + JavaPlugin.getProvidingPlugin(SkyblockAdminCommand.class).reloadConfig(); - @Override - public ArgumentType 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 { + + @Override + public Structure parse(StringReader reader) throws CommandSyntaxException { + NamespacedKey key = new NamespacedKey("usefulskyblock", reader.readString()); + return new StructureRef(key); } + @Override + public ArgumentType getNativeType() { + return StringArgumentType.word(); + } + } + + public static final class EnumArgument> implements CustomArgumentType { + + private final Class enumClass; + + public EnumArgument(Class 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 getNativeType() { + return StringArgumentType.word(); + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + T[] enumConstants = enumClass.getEnumConstants(); + for (T c : enumConstants) { + builder.suggest(c.name()); + } + return CustomArgumentType.super.listSuggestions(context, builder); + } + } + + } diff --git a/src/main/java/com/ncguy/usefulskyblock/command/SkyblockGenCommand.java b/src/main/java/com/ncguy/usefulskyblock/command/SkyblockGenCommand.java index 11bccf4..18c532c 100644 --- a/src/main/java/com/ncguy/usefulskyblock/command/SkyblockGenCommand.java +++ b/src/main/java/com/ncguy/usefulskyblock/command/SkyblockGenCommand.java @@ -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 bedrock2 = structure.getPalettes().getFirst().getBlocks().stream().filter(b -> b.getBlockData().getMaterial() == Material.BEDROCK).findFirst(); - - Optional 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 randomEnum(Class enumCls) { - assert(enumCls.isEnum()); - return randomElement(enumCls.getEnumConstants()); - } - - private 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 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 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 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 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 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 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 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 ctx) { + Entity executor = Objects.requireNonNull(ctx.getSource().getExecutor()); + + ScoreboardManager scoreboardManager = Bukkit.getServer().getScoreboardManager(); + Scoreboard scoreboard = scoreboardManager.getMainScoreboard(); + + Set 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 ctx) { + Entity executor = Objects.requireNonNull(ctx.getSource().getExecutor()); + + + ScoreboardManager scoreboardManager = Bukkit.getServer().getScoreboardManager(); + Scoreboard scoreboard = scoreboardManager.getMainScoreboard(); + Set 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 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 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 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 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 { + + @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 CompletableFuture listSuggestions(CommandContext 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 getNativeType() { + return StringArgumentType.word(); + } + } + } diff --git a/src/main/java/com/ncguy/usefulskyblock/data/BiomedStructure.java b/src/main/java/com/ncguy/usefulskyblock/data/BiomedStructure.java new file mode 100644 index 0000000..f78c4bd --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/data/BiomedStructure.java @@ -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; + } +} diff --git a/src/main/java/com/ncguy/usefulskyblock/events/SkyblockConfigReloadEvent.java b/src/main/java/com/ncguy/usefulskyblock/events/SkyblockConfigReloadEvent.java new file mode 100644 index 0000000..e48a3d1 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/events/SkyblockConfigReloadEvent.java @@ -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; + } + +} diff --git a/src/main/java/com/ncguy/usefulskyblock/events/TeamProgressionEvent.java b/src/main/java/com/ncguy/usefulskyblock/events/TeamProgressionEvent.java new file mode 100644 index 0000000..c1cf3e9 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/events/TeamProgressionEvent.java @@ -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; + } +} diff --git a/src/main/java/com/ncguy/usefulskyblock/handlers/CauldronCraftingHandler.java b/src/main/java/com/ncguy/usefulskyblock/handlers/CauldronCraftingHandler.java new file mode 100644 index 0000000..c4ee898 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/handlers/CauldronCraftingHandler.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/ncguy/usefulskyblock/handlers/FishingHandler.java b/src/main/java/com/ncguy/usefulskyblock/handlers/FishingHandler.java new file mode 100644 index 0000000..75513ee --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/handlers/FishingHandler.java @@ -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)); + }); + + } + +} diff --git a/src/main/java/com/ncguy/usefulskyblock/handlers/InitialisationHandler.java b/src/main/java/com/ncguy/usefulskyblock/handlers/InitialisationHandler.java new file mode 100644 index 0000000..12e4b9c --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/handlers/InitialisationHandler.java @@ -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"); + } + +} diff --git a/src/main/java/com/ncguy/usefulskyblock/handlers/ItemEventHandler.java b/src/main/java/com/ncguy/usefulskyblock/handlers/ItemEventHandler.java new file mode 100644 index 0000000..215eae5 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/handlers/ItemEventHandler.java @@ -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); + } + +} diff --git a/src/main/java/com/ncguy/usefulskyblock/handlers/TeamProgressHandler.java b/src/main/java/com/ncguy/usefulskyblock/handlers/TeamProgressHandler.java new file mode 100644 index 0000000..51d4967 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/handlers/TeamProgressHandler.java @@ -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 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; + + + } + } + +} diff --git a/src/main/java/com/ncguy/usefulskyblock/pdc/AnonymousDataContainerRef.java b/src/main/java/com/ncguy/usefulskyblock/pdc/AnonymousDataContainerRef.java new file mode 100644 index 0000000..c4a3504 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/pdc/AnonymousDataContainerRef.java @@ -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 implements IDataContainerRef { + + protected final NamespacedKey ref; + protected final PersistentDataType type; + + public

AnonymousDataContainerRef(NamespacedKey ref, PersistentDataType type) { + this.ref = ref; + this.type = type; + } + + @Override + public IDataContainerRef withSuffix(String suffix) { + return new AnonymousDataContainerRef<>(new NamespacedKey(ref.getNamespace(), ref.getKey() + suffix), type); + } + + @Override + public IDataContainerRef assign(H holder) { + return new StrictDataContainerRef<>(holder, ref, type); + } + + @Override + public IDataContainerRef withType(PersistentDataType newType) { + return new AnonymousDataContainerRef<>(ref, newType); + } + + @Override + public IDataContainerRef restrict(Class hClass) { + return new AnonymousDataContainerRef(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"); + } +} diff --git a/src/main/java/com/ncguy/usefulskyblock/pdc/IDataContainerRef.java b/src/main/java/com/ncguy/usefulskyblock/pdc/IDataContainerRef.java new file mode 100644 index 0000000..5ac95c4 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/pdc/IDataContainerRef.java @@ -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 { + + public IDataContainerRef assign(HOLDER holder); + public IDataContainerRef withSuffix(String suffix); + public IDataContainerRef withType(PersistentDataType newType); + public IDataContainerRef restrict(Class 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 IDataContainerRef map(Function mapper) { + return map(mapper, null); + } + default public IDataContainerRef map(Function mapper, Function reverseMapper) { + return new MappedDataContainerRef<>(this, mapper, reverseMapper); + } + + +} diff --git a/src/main/java/com/ncguy/usefulskyblock/pdc/MappedDataContainerRef.java b/src/main/java/com/ncguy/usefulskyblock/pdc/MappedDataContainerRef.java new file mode 100644 index 0000000..9338318 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/pdc/MappedDataContainerRef.java @@ -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 implements IDataContainerRef { + + private IDataContainerRef underlying; + private Function mapper; + private Function reverseMapper; + + public MappedDataContainerRef(IDataContainerRef underlying, Function mapper, Function reverseMapper) { + this.underlying = underlying; + this.mapper = mapper; + this.reverseMapper = reverseMapper; + } + + @Override + public IDataContainerRef assign(H holder) { + return new MappedDataContainerRef<>(underlying.assign(holder), mapper, reverseMapper); + } + + @Override + public IDataContainerRef withSuffix(String suffix) { + return new MappedDataContainerRef<>(underlying.withSuffix(suffix), mapper, reverseMapper); + } + + @Override + public IDataContainerRef withType(PersistentDataType newType) { + throw new UnsupportedOperationException("MappedDataContainerRef does not support type conversion"); + } + + @Override + public IDataContainerRef restrict(Class 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(); + } +} diff --git a/src/main/java/com/ncguy/usefulskyblock/pdc/StrictDataContainerRef.java b/src/main/java/com/ncguy/usefulskyblock/pdc/StrictDataContainerRef.java new file mode 100644 index 0000000..1d6e126 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/pdc/StrictDataContainerRef.java @@ -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 extends AnonymousDataContainerRef { + + private final H holder; + + public

StrictDataContainerRef(H holder, NamespacedKey ref, PersistentDataType type) { + super(ref, type); + this.holder = holder; + } + + @Override + public IDataContainerRef assign(PersistentDataHolder holder) { + throw new UnsupportedOperationException("Cannot assign a holder to a strict ref"); + } + + @Override + public IDataContainerRef withSuffix(String suffix) { + return new StrictDataContainerRef<>(holder, new NamespacedKey(ref.getNamespace(), ref.getKey() + suffix), type); + } + + @Override + public IDataContainerRef withType(PersistentDataType 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); + } +} diff --git a/src/main/java/com/ncguy/usefulskyblock/pdc/VectorPersistentDataType.java b/src/main/java/com/ncguy/usefulskyblock/pdc/VectorPersistentDataType.java new file mode 100644 index 0000000..f4ad41f --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/pdc/VectorPersistentDataType.java @@ -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 { + + public static final PersistentDataType Instance = new VectorPersistentDataType(); + + + @Override + public @NotNull Class getPrimitiveType() { + return int[].class; + } + + @Override + public @NotNull Class 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]); + } +} diff --git a/src/main/java/com/ncguy/usefulskyblock/recipe/BiomeRod.java b/src/main/java/com/ncguy/usefulskyblock/recipe/BiomeRod.java index 64e632a..6c3983c 100644 --- a/src/main/java/com/ncguy/usefulskyblock/recipe/BiomeRod.java +++ b/src/main/java/com/ncguy/usefulskyblock/recipe/BiomeRod.java @@ -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 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> o = (List>) config.getList("skyblock.biomes.biome-map"); + + Registry biomeRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.BIOME); + + biomeMap.clear(); + + for (Map 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 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> o = (List>) config.getList("skyblock.biomes.biome-map"); + List biomeLocations = new ArrayList<>(); - Registry 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 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 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 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 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 chunksToRefresh = new HashSet<>(); - final World world = event.getBlockAgainst().getWorld(); - - List 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 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 locations, Biome biome, JavaPlugin plugin, BukkitScheduler scheduler, int batchSize, IProgressMonitor progress) { - if(locations.isEmpty()) { - progress.finish(); - return true; - } - - Set 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; + } } diff --git a/src/main/java/com/ncguy/usefulskyblock/recipe/IRecipeProvider.java b/src/main/java/com/ncguy/usefulskyblock/recipe/IRecipeProvider.java new file mode 100644 index 0000000..88a196e --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/recipe/IRecipeProvider.java @@ -0,0 +1,11 @@ +package com.ncguy.usefulskyblock.recipe; + +import org.bukkit.Server; +import org.bukkit.inventory.Recipe; + +@FunctionalInterface +public interface IRecipeProvider { + + Iterable provideRecipes(); + +} diff --git a/src/main/java/com/ncguy/usefulskyblock/recipe/SmeltingCraftingHandler.java b/src/main/java/com/ncguy/usefulskyblock/recipe/SmeltingCraftingHandler.java new file mode 100644 index 0000000..0714ea6 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/recipe/SmeltingCraftingHandler.java @@ -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 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 provideRecipes() { + return recipes; + } + +} diff --git a/src/main/java/com/ncguy/usefulskyblock/utils/MathsUtils.java b/src/main/java/com/ncguy/usefulskyblock/utils/MathsUtils.java index 6728427..72311d4 100644 --- a/src/main/java/com/ncguy/usefulskyblock/utils/MathsUtils.java +++ b/src/main/java/com/ncguy/usefulskyblock/utils/MathsUtils.java @@ -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); + } + } diff --git a/src/main/java/com/ncguy/usefulskyblock/utils/ReflectionUtils.java b/src/main/java/com/ncguy/usefulskyblock/utils/ReflectionUtils.java new file mode 100644 index 0000000..7d98378 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/utils/ReflectionUtils.java @@ -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 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; + } + +} diff --git a/src/main/java/com/ncguy/usefulskyblock/utils/RemarkSet.java b/src/main/java/com/ncguy/usefulskyblock/utils/RemarkSet.java index 6594533..b2da8ea 100644 --- a/src/main/java/com/ncguy/usefulskyblock/utils/RemarkSet.java +++ b/src/main/java/com/ncguy/usefulskyblock/utils/RemarkSet.java @@ -1,6 +1,7 @@ package com.ncguy.usefulskyblock.utils; import java.util.HashSet; +import java.util.function.Consumer; public class RemarkSet extends HashSet { @@ -8,4 +9,9 @@ public class RemarkSet extends HashSet { add(new Remark(domain, message)); } + public RemarkSet handleRemarks(Consumer handler) { + this.forEach(handler); + return this; + } + } diff --git a/src/main/java/com/ncguy/usefulskyblock/utils/TextUtils.java b/src/main/java/com/ncguy/usefulskyblock/utils/TextUtils.java new file mode 100644 index 0000000..6b25978 --- /dev/null +++ b/src/main/java/com/ncguy/usefulskyblock/utils/TextUtils.java @@ -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(); + } +} diff --git a/src/main/java/com/ncguy/usefulskyblock/world/PortalHandler.java b/src/main/java/com/ncguy/usefulskyblock/world/PortalHandler.java index 5a22e62..5177a18 100644 --- a/src/main/java/com/ncguy/usefulskyblock/world/PortalHandler.java +++ b/src/main/java/com/ncguy/usefulskyblock/world/PortalHandler.java @@ -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 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 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); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 417332b..3e38b66 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -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" \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/minecraft/advancement/adventure/root.json b/src/main/resources/datapacks/usefulskyblock/data/minecraft/advancement/adventure/root.json new file mode 100644 index 0000000..5e7d9ed --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/minecraft/advancement/adventure/root.json @@ -0,0 +1,8 @@ +{ + "criteria": { + "never": { "trigger": "minecraft:impossible" } + }, + "requirements": [ + ["never"] + ] +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/minecraft/advancement/husbandry/root.json b/src/main/resources/datapacks/usefulskyblock/data/minecraft/advancement/husbandry/root.json new file mode 100644 index 0000000..5e7d9ed --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/minecraft/advancement/husbandry/root.json @@ -0,0 +1,8 @@ +{ + "criteria": { + "never": { "trigger": "minecraft:impossible" } + }, + "requirements": [ + ["never"] + ] +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/minecraft/advancement/nether/root.json b/src/main/resources/datapacks/usefulskyblock/data/minecraft/advancement/nether/root.json new file mode 100644 index 0000000..5e7d9ed --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/minecraft/advancement/nether/root.json @@ -0,0 +1,8 @@ +{ + "criteria": { + "never": { "trigger": "minecraft:impossible" } + }, + "requirements": [ + ["never"] + ] +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/minecraft/advancement/story/root.json b/src/main/resources/datapacks/usefulskyblock/data/minecraft/advancement/story/root.json new file mode 100644 index 0000000..5e7d9ed --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/minecraft/advancement/story/root.json @@ -0,0 +1,8 @@ +{ + "criteria": { + "never": { "trigger": "minecraft:impossible" } + }, + "requirements": [ + ["never"] + ] +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/minecraft/dimension/the_nether.json b/src/main/resources/datapacks/usefulskyblock/data/minecraft/dimension/the_nether.json index 39b74be..994f81e 100644 --- a/src/main/resources/datapacks/usefulskyblock/data/minecraft/dimension/the_nether.json +++ b/src/main/resources/datapacks/usefulskyblock/data/minecraft/dimension/the_nether.json @@ -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": [] } } } \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/farm_unlock.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/farm_unlock.json new file mode 100644 index 0000000..e99d40d --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/farm_unlock.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_beetroot.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_beetroot.json new file mode 100644 index 0000000..6768dcb --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_beetroot.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_cactus.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_cactus.json new file mode 100644 index 0000000..144b131 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_cactus.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_carrot.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_carrot.json new file mode 100644 index 0000000..4859897 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_carrot.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_fish.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_fish.json new file mode 100644 index 0000000..02fe129 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_fish.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_honey.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_honey.json new file mode 100644 index 0000000..7aa2e86 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_honey.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_mushrooms.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_mushrooms.json new file mode 100644 index 0000000..e8b3f2e --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_mushrooms.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_pumpkin.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_pumpkin.json new file mode 100644 index 0000000..2763ae1 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_pumpkin.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_sugarcane.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_sugarcane.json new file mode 100644 index 0000000..ed63d49 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_sugarcane.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_wheat.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_wheat.json new file mode 100644 index 0000000..cde5a34 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/get_wheat.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/passive_animals.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/passive_animals.json new file mode 100644 index 0000000..0e422d9 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/farming/passive_animals.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/copper.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/copper.json new file mode 100644 index 0000000..94d7120 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/copper.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/creeper_hunter.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/creeper_hunter.json new file mode 100644 index 0000000..9180163 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/creeper_hunter.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/defeat_wither.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/defeat_wither.json new file mode 100644 index 0000000..5cb8c76 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/defeat_wither.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/ender_hunter.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/ender_hunter.json new file mode 100644 index 0000000..9e6547d --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/ender_hunter.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/lightning_pig.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/lightning_pig.json new file mode 100644 index 0000000..a06b0d7 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/lightning_pig.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/monster_unlock.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/monster_unlock.json new file mode 100644 index 0000000..6714f83 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/monster_unlock.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/skeleton_hunter.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/skeleton_hunter.json new file mode 100644 index 0000000..0a66183 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/skeleton_hunter.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/spider_hunter.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/spider_hunter.json new file mode 100644 index 0000000..ab0da6c --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/spider_hunter.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/witch_hostage.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/witch_hostage.json new file mode 100644 index 0000000..3f73299 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/witch_hostage.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/zombie_hostage.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/zombie_hostage.json new file mode 100644 index 0000000..4775f4c --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/zombie_hostage.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/zombie_hunter.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/zombie_hunter.json new file mode 100644 index 0000000..0dc1d51 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/monsters/zombie_hunter.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/craft_netherrack.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/craft_netherrack.json new file mode 100644 index 0000000..e1be51c --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/craft_netherrack.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/cure_villager.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/cure_villager.json new file mode 100644 index 0000000..a3db722 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/cure_villager.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/enter_nether.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/enter_nether.json new file mode 100644 index 0000000..5a0d5a1 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/enter_nether.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/get_gold.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/get_gold.json new file mode 100644 index 0000000..25c5ec6 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/get_gold.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/get_gravel.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/get_gravel.json new file mode 100644 index 0000000..3df76b3 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/get_gravel.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/golden_apple.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/golden_apple.json new file mode 100644 index 0000000..d84e0f4 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/golden_apple.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/smelt_netherbrick.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/smelt_netherbrick.json new file mode 100644 index 0000000..62e3dbb --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/smelt_netherbrick.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/tier_1_unlock.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/tier_1_unlock.json new file mode 100644 index 0000000..2a50f01 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/nether/tier_1_unlock.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/collect_10000_cobble.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/collect_10000_cobble.json new file mode 100644 index 0000000..9a23179 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/collect_10000_cobble.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/collect_1024_cobble.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/collect_1024_cobble.json new file mode 100644 index 0000000..6ab635f --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/collect_1024_cobble.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/collect_1728_cobble.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/collect_1728_cobble.json new file mode 100644 index 0000000..297ac9c --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/collect_1728_cobble.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/collect_64_cobble.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/collect_64_cobble.json new file mode 100644 index 0000000..01b1744 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/collect_64_cobble.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/craft_a_beacon.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/craft_a_beacon.json new file mode 100644 index 0000000..e446d00 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/craft_a_beacon.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/eat_food.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/eat_food.json new file mode 100644 index 0000000..c87ad37 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/eat_food.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_clay.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_clay.json new file mode 100644 index 0000000..cb80f4f --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_clay.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_cobble.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_cobble.json new file mode 100644 index 0000000..f6a46d2 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_cobble.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_glass.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_glass.json new file mode 100644 index 0000000..5aa687a --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_glass.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_iron.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_iron.json new file mode 100644 index 0000000..5915001 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_iron.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_iron_tools.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_iron_tools.json new file mode 100644 index 0000000..38e5bb2 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_iron_tools.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_logs.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_logs.json new file mode 100644 index 0000000..9754e77 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_logs.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_mud.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_mud.json new file mode 100644 index 0000000..a019dc2 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_mud.json @@ -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 +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_seed.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_seed.json new file mode 100644 index 0000000..bacf7c5 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_seed.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_stone_tools.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_stone_tools.json new file mode 100644 index 0000000..fe3cb61 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_stone_tools.json @@ -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 +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_torches.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_torches.json new file mode 100644 index 0000000..23bcae7 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_torches.json @@ -0,0 +1,23 @@ +{ + "display": { + "icon": { + "id": "minecraft:torch" + }, + "title": "Get torches!", + "description": "Light up your island\n\n - Obtain charcoal by smelting logs\n- Craft a torch", + "frame": "task", + "show_toast": true, + "announce_to_chat": false, + "hidden": false + }, + "parent": "usefulskyblock:skyblock/get_logs", + "criteria": { + "get-pickaxe": { + "trigger": "minecraft:recipe_crafted", + "conditions": { + "recipe_id": "minecraft:torch" + } + } + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_wooden_tools.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_wooden_tools.json new file mode 100644 index 0000000..c233b90 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/get_wooden_tools.json @@ -0,0 +1,23 @@ +{ + "display": { + "icon": { + "id": "minecraft:wooden_pickaxe" + }, + "title": "Wooden Tools", + "description": "Don't get splinters\n\n - Craft a wooden pickaxe", + "frame": "task", + "show_toast": true, + "announce_to_chat": false, + "hidden": false + }, + "parent": "usefulskyblock:skyblock/get_logs", + "criteria": { + "get-pickaxe": { + "trigger": "minecraft:recipe_crafted", + "conditions": { + "recipe_id": "minecraft:wooden_pickaxe" + } + } + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/max_a_beacon.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/max_a_beacon.json new file mode 100644 index 0000000..9f69132 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/max_a_beacon.json @@ -0,0 +1,25 @@ +{ + "display": { + "icon": { + "id": "minecraft:beacon" + }, + "title": "Max a beacon", + "description": "Where'd you get all those blocks", + "frame": "challenge", + "show_toast": true, + "announce_to_chat": true, + "hidden": true + }, + "parent": "usefulskyblock:skyblock/craft_a_beacon", + "criteria": { + "example": { + "trigger": "minecraft:construct_beacon", + "conditions": { + "level": { + "min": 4 + } + } + } + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/more_water.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/more_water.json new file mode 100644 index 0000000..e3f86fc --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/more_water.json @@ -0,0 +1,58 @@ +{ + "display": { + "icon": { + "id": "minecraft:water_bucket" + }, + "title": "More Water", + "description": "Growing water?\n\nUse bone meal on the floor of a 2-deep water pool", + "frame": "task", + "show_toast": true, + "announce_to_chat": false, + "hidden": false + }, + "parent": "usefulskyblock:monsters/skeleton_hunter", + "criteria": { + "grow-seagrass": { + "trigger": "minecraft:item_used_on_block", + "conditions": { + "location": [ + { + "condition": "minecraft:match_tool", + "predicate": { + "items": [ + "minecraft:bone_meal" + ] + } + }, + { + "condition": "minecraft:location_check", + "predicate": { + "block": { + "tag": "minecraft:dirt" + } + } + }, + { + "condition": "minecraft:location_check", + "offsetY": 1, + "predicate": { + "fluid": { + "fluids": "#minecraft:water" + } + } + }, + { + "condition": "minecraft:location_check", + "offsetY": 2, + "predicate": { + "fluid": { + "fluids": "#minecraft:water" + } + } + } + ] + } + } + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/pointed_dripstone.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/pointed_dripstone.json new file mode 100644 index 0000000..b13279f --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/pointed_dripstone.json @@ -0,0 +1,28 @@ +{ + "display": { + "icon": { + "id": "minecraft:pointed_dripstone" + }, + "title": "Pointed Dripstone", + "description": "This unlocks a lot", + "frame": "goal", + "show_toast": false, + "announce_to_chat": false, + "hidden": true + }, + "parent": "usefulskyblock:nether/tier_1_unlock", + "criteria": { + "get-dripstone": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "items": "minecraft:pointed_dripstone", + "count": 1 + } + ] + } + } + }, + "sends_telemetry_event": false +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/renew_dirt.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/renew_dirt.json new file mode 100644 index 0000000..b45e255 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/renew_dirt.json @@ -0,0 +1,38 @@ +{ + "display": { + "icon": { + "id": "minecraft:dirt" + }, + "title": "Renewable Dirt", + "description": "This one might need some explaining", + "frame": "task", + "show_toast": true, + "announce_to_chat": true, + "hidden": false + }, + "parent": "usefulskyblock:skyblock/get_glass", + "criteria": { + "clay_ball": { + "trigger": "minecraft:item_used_on_block", + "conditions": { + "location": [ + { + "condition": "minecraft:match_tool", + "predicate": { + "items": "#minecraft:shovels" + } + }, + { + "condition": "minecraft:location_check", + "predicate": { + "block": { + "blocks": "minecraft:rooted_dirt" + } + } + } + ] + } + } + }, + "sends_telemetry_event": false +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/skyblock_begin.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/skyblock_begin.json new file mode 100644 index 0000000..d7f939f --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/skyblock_begin.json @@ -0,0 +1,20 @@ +{ + "display": { + "icon": { + "id": "minecraft:oak_sapling" + }, + "title": "It begins", + "description": "Your new home between the clouds", + "background": "minecraft:block/oak_log", + "frame": "challenge", + "show_toast": true, + "announce_to_chat": false, + "hidden": false + }, + "criteria": { + "begin-island": { + "trigger": "minecraft:impossible" + } + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/void_bin.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/void_bin.json new file mode 100644 index 0000000..7ebb196 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/skyblock/void_bin.json @@ -0,0 +1,20 @@ +{ + "display": { + "icon": { + "id": "minecraft:structure_void" + }, + "title": "A convenient rubbish bin", + "description": "You didn't need that, right?", + "frame": "challenge", + "show_toast": true, + "announce_to_chat": false, + "hidden": true + }, + "parent": "usefulskyblock:skyblock/skyblock_begin", + "criteria": { + "cast-into-the-void": { + "trigger": "minecraft:impossible" + } + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/trading/trading_unlock.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/trading/trading_unlock.json new file mode 100644 index 0000000..2088f00 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/advancement/trading/trading_unlock.json @@ -0,0 +1,29 @@ +{ + "display": { + "icon": { + "id": "minecraft:emerald" + }, + "title": "Time to Trade!", + "description": "Obtain an Emerald", + "frame": "task", + "background": "minecraft:emerald_block", + "show_toast": false, + "announce_to_chat": false, + "hidden": true + }, + "criteria": { + "emerald": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "items": [ + "minecraft:emerald" + ] + } + ] + } + } + }, + "sends_telemetry_event": false +} diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/dimension_type/skyblock_nether.json b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/dimension_type/skyblock_nether.json new file mode 100644 index 0000000..8b469b0 --- /dev/null +++ b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/dimension_type/skyblock_nether.json @@ -0,0 +1,20 @@ +{ + "ambient_light": 0.1, + "bed_works": false, + "coordinate_scale": 1, + "effects": "minecraft:the_nether", + "fixed_time": 18000, + "has_ceiling": false, + "has_raids": false, + "has_skylight": false, + "height": 256, + "infiniburn": "#minecraft:infiniburn_nether", + "logical_height": 128, + "min_y": 0, + "monster_spawn_block_light_limit": 15, + "monster_spawn_light_level": 7, + "natural": false, + "piglin_safe": true, + "respawn_anchor_works": true, + "ultrawarm": true +} \ No newline at end of file diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/structures/blaze-spawner.nbt b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/structures/blaze-spawner.nbt new file mode 100644 index 0000000..baa571e Binary files /dev/null and b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/structures/blaze-spawner.nbt differ diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/structures/nether-crimson-island.nbt b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/structures/nether-crimson-island.nbt new file mode 100644 index 0000000..a485714 Binary files /dev/null and b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/structures/nether-crimson-island.nbt differ diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/structures/nether-start.nbt b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/structures/nether-start.nbt new file mode 100644 index 0000000..1c9983c Binary files /dev/null and b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/structures/nether-start.nbt differ diff --git a/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/structures/nether-warped-island.nbt b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/structures/nether-warped-island.nbt new file mode 100644 index 0000000..8f4fca1 Binary files /dev/null and b/src/main/resources/datapacks/usefulskyblock/data/usefulskyblock/structures/nether-warped-island.nbt differ diff --git a/src/main/resources/datapacks/usefulskyblock/pack.mcmeta b/src/main/resources/datapacks/usefulskyblock/pack.mcmeta index 59be7a6..8db2522 100644 --- a/src/main/resources/datapacks/usefulskyblock/pack.mcmeta +++ b/src/main/resources/datapacks/usefulskyblock/pack.mcmeta @@ -1,6 +1,14 @@ { "pack": { "pack_format": 71, - "description": "Useful Skyblock datapack" + "description": "Useful Skyblock datapack." + }, + "filter": { + "block": [ + { + "namespace": "minecraft", + "path": "advancements/.*" + } + ] } } \ No newline at end of file diff --git a/src/main/resources/schemas/minecraft_advancement_schema.json b/src/main/resources/schemas/minecraft_advancement_schema.json new file mode 100644 index 0000000..483d8e3 --- /dev/null +++ b/src/main/resources/schemas/minecraft_advancement_schema.json @@ -0,0 +1,133 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "description": "Minecraft advancement schema", + "properties": { + "parent": { + "type": "string", + "description": "The parent advancement directory. If absent, this is a root advancement" + }, + "display": { + "type": "object", + "description": "Data related to the advancement's display", + "required": ["icon", "title", "description"], + "properties": { + "icon": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string", + "description": "An item ID" + }, + "count": { + "type": "integer", + "description": "The amount of the item", + "default": 1 + }, + "components": { + "type": "object", + "description": "Additional information about the item" + } + } + }, + "title": { + "oneOf": [ + { "type": "string" }, + { "type": "object" }, + { "type": "array" } + ], + "description": "JSON text component for the title" + }, + "description": { + "oneOf": [ + { "type": "string" }, + { "type": "object" }, + { "type": "array" } + ], + "description": "JSON text component for the description" + }, + "frame": { + "type": "string", + "enum": ["challenge", "goal", "task"], + "default": "task", + "description": "Type of frame for the icon" + }, + "background": { + "type": "string", + "description": "The directory for the background (used only for root advancement)" + }, + "show_toast": { + "type": "boolean", + "default": true, + "description": "Whether to show a toast on completion" + }, + "announce_to_chat": { + "type": "boolean", + "default": true, + "description": "Whether to announce in chat on completion" + }, + "hidden": { + "type": "boolean", + "default": false, + "description": "Whether to hide this advancement and its children until completed" + } + } + }, + "criteria": { + "type": "object", + "description": "The criteria to be tracked by this advancement", + "patternProperties": { + "^.*$": { + "type": "object", + "description": "Individual criterion definition" + } + } + }, + "requirements": { + "type": "array", + "description": "Defines how criteria are completed to grant the advancement", + "items": { + "type": "array", + "items": { + "type": "string", + "description": "Criterion name" + } + } + }, + "rewards": { + "type": "object", + "properties": { + "experience": { + "type": "integer", + "default": 0, + "description": "Amount of experience to give" + }, + "recipes": { + "type": "array", + "items": { + "type": "string", + "description": "Resource location of a recipe" + } + }, + "loot": { + "type": "array", + "items": { + "type": "string", + "description": "Resource location of a loot table" + } + }, + "function": { + "type": "string", + "description": "Function to run (function tags not allowed)" + } + } + }, + "sends_telemetry_event": { + "type": "boolean", + "default": false, + "description": "Whether telemetry data should be collected on achievement" + } + }, + "required": ["criteria"] +} diff --git a/src/test/java/com/ncguy/usefulskyblock/AdvancementTest.java b/src/test/java/com/ncguy/usefulskyblock/AdvancementTest.java new file mode 100644 index 0000000..56eede5 --- /dev/null +++ b/src/test/java/com/ncguy/usefulskyblock/AdvancementTest.java @@ -0,0 +1,37 @@ +package com.ncguy.usefulskyblock; + +import com.ncguy.usefulskyblock.data.IVisitor; +import com.ncguy.usefulskyblock.data.gen.Advancement; +import com.ncguy.usefulskyblock.data.gen.AdvancementTreeSetLoader; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; + +public class AdvancementTest { + + @Test + public void testAdvancementStructure() { + AdvancementTreeSetLoader loader = new AdvancementTreeSetLoader(); + Path directory = Path.of( + "src\\main\\resources\\datapacks\\usefulskyblock\\data\\usefulskyblock\\advancement\\skyblock"); + Advancement root = loader.load(directory); + + IVisitor visitor = new IVisitor<>() { + private int depth = 0; + @Override + public void preVisit(Advancement node) { + depth++; + System.out.print(" ".repeat(Math.max(0, depth))); + System.out.println(node.getId()); + } + + @Override + public void postVisit(Advancement node) { + depth--; + } + }; + + root.accept(visitor); + } + +} diff --git a/src/test/java/com/ncguy/usefulskyblock/data/IVisitable.java b/src/test/java/com/ncguy/usefulskyblock/data/IVisitable.java new file mode 100644 index 0000000..3b1e5ce --- /dev/null +++ b/src/test/java/com/ncguy/usefulskyblock/data/IVisitable.java @@ -0,0 +1,7 @@ +package com.ncguy.usefulskyblock.data; + +public interface IVisitable { + + void accept(IVisitor visitor); + +} diff --git a/src/test/java/com/ncguy/usefulskyblock/data/IVisitor.java b/src/test/java/com/ncguy/usefulskyblock/data/IVisitor.java new file mode 100644 index 0000000..a46d8d5 --- /dev/null +++ b/src/test/java/com/ncguy/usefulskyblock/data/IVisitor.java @@ -0,0 +1,8 @@ +package com.ncguy.usefulskyblock.data; + +public interface IVisitor { + + void preVisit(T node); + void postVisit(T node); + +} diff --git a/src/test/java/com/ncguy/usefulskyblock/data/TreeNode.java b/src/test/java/com/ncguy/usefulskyblock/data/TreeNode.java new file mode 100644 index 0000000..7bc9f78 --- /dev/null +++ b/src/test/java/com/ncguy/usefulskyblock/data/TreeNode.java @@ -0,0 +1,45 @@ +package com.ncguy.usefulskyblock.data; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class TreeNode> implements IVisitable { + + public List children; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public Optional 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); + } + + @Override + public void accept(IVisitor visitor) { + //noinspection unchecked + T t = (T) this; + + visitor.preVisit(t); + children.forEach(x -> x.accept(visitor)); + visitor.postVisit(t); + } +} diff --git a/src/test/java/com/ncguy/usefulskyblock/data/gen/Advancement.java b/src/test/java/com/ncguy/usefulskyblock/data/gen/Advancement.java new file mode 100644 index 0000000..5d67ff2 --- /dev/null +++ b/src/test/java/com/ncguy/usefulskyblock/data/gen/Advancement.java @@ -0,0 +1,286 @@ +package com.ncguy.usefulskyblock.data.gen; + +import com.ncguy.usefulskyblock.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 { + + 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 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> 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 getCriteria() { + return criteria; + } + + public void setCriteria(Map criteria) { + this.criteria = criteria; + } + + public List> getRequirements() { + return requirements; + } + + public void setRequirements(List> 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 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 getComponents() { + return components; + } + + public void setComponents(Map components) { + this.components = components; + } + } + + /** + * Rewards section of an advancement. + */ + public static class Rewards { + private Integer experience; + private List recipes; + private List loot; + private String function; + + public Rewards() {} + + public Integer getExperience() { + return experience; + } + + public void setExperience(Integer experience) { + this.experience = experience; + } + + public List getRecipes() { + return recipes; + } + + public void setRecipes(List recipes) { + this.recipes = recipes; + } + + public List getLoot() { + return loot; + } + + public void setLoot(List loot) { + this.loot = loot; + } + + public String getFunction() { + return function; + } + + public void setFunction(String function) { + this.function = function; + } + } +} diff --git a/src/test/java/com/ncguy/usefulskyblock/data/gen/AdvancementTreeSetLoader.java b/src/test/java/com/ncguy/usefulskyblock/data/gen/AdvancementTreeSetLoader.java new file mode 100644 index 0000000..298d532 --- /dev/null +++ b/src/test/java/com/ncguy/usefulskyblock/data/gen/AdvancementTreeSetLoader.java @@ -0,0 +1,72 @@ +package com.ncguy.usefulskyblock.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.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class AdvancementTreeSetLoader { + + private List 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); + String group = file.getParentFile().getName(); + String name = file.getName().substring(0, file.getName().length() - 5); + result.setId("usefulskyblock:" + group + "/" + name); + 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); + }); + return findParentsForOrphans(); + } + + private Advancement findParentsForOrphans() { + Advancement rootAdv = null; + + for (int i = orphanedAdvancements.size() - 1; i >= 0; i--) { + Advancement last = orphanedAdvancements.get(i); + if (last.parentId == null) { + rootAdv = last; + break; + } + Optional first = orphanedAdvancements.stream() + .filter(x -> Objects.equals(x.getId(), last.parentId)) + .findFirst(); + first.ifPresentOrElse(parent -> { + parent.addChild(last); + }, () -> { + throw new IllegalStateException( + "Parent \"" + last.parentId + "\" not found " + "for node \"" + last.getId() + "\""); + }); + } + + return rootAdv; + } + +} diff --git a/src/test/java/com/ncguy/usefulskyblock/data/gen/AdvancementTypeAdapter.java b/src/test/java/com/ncguy/usefulskyblock/data/gen/AdvancementTypeAdapter.java new file mode 100644 index 0000000..844403e --- /dev/null +++ b/src/test/java/com/ncguy/usefulskyblock/data/gen/AdvancementTypeAdapter.java @@ -0,0 +1,109 @@ +package com.ncguy.usefulskyblock.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 java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +import static com.ncguy.usefulskyblock.utils.ReflectionUtils.get; + +/** + * 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 { + + 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(); + + Advancement adv = new Advancement(); + + 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>() {}.getType(); + adv.setCriteria(gson.fromJson(obj.get(K_CRITERIA), t)); + } + if (obj.has(K_REQUIREMENTS)) { + Type t = new TypeToken>>() {}.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(); + } +}