Code commit

This commit is contained in:
ZenZoya 2024-04-15 10:10:12 -07:00
parent a166622f44
commit a61f3b669b
133 changed files with 7112 additions and 0 deletions

42
.gitignore vendored Normal file
View file

@ -0,0 +1,42 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

10
.idea/.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# GitHub Copilot persisted chat sessions
/copilot/chatSessions

7
.idea/discord.xml Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
</component>
</project>

17
.idea/gradle.xml Normal file
View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

10
.idea/misc.xml Normal file
View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" default="true" project-jdk-name="temurin-19 (2)" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

124
.idea/uiDesigner.xml Normal file
View file

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

6
.idea/vcs.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

BIN
Lobby/region/r.-1.-1.mca Normal file

Binary file not shown.

BIN
Lobby/region/r.-1.0.mca Normal file

Binary file not shown.

BIN
Lobby/region/r.0.-1.mca Normal file

Binary file not shown.

BIN
Lobby/region/r.0.0.mca Normal file

Binary file not shown.

14
arena/Icons.java Normal file
View file

@ -0,0 +1,14 @@
package net.minestom.arena;
public final class Icons {
public static final String SWORD = "\uD83D\uDDE1";
public static final String BOW = "\uD83C\uDFF9";
public static final String SHIELD = "\uD83D\uDEE1";
public static final String POTION = "\uD83E\uDDEA";
public static final String AXE = "\uD83E\uDE93";
public static final String STAR = "\u2606";
public static final String CHECKMARK = "\u2714";
public static final String CROSS = "\u274C";
private Icons() {}
}

27
arena/Items.java Normal file
View file

@ -0,0 +1,27 @@
package net.minestom.arena;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.arena.utils.ItemUtils;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
public final class Items {
public static final ItemStack CLOSE = ItemUtils.stripItalics(ItemStack.builder(Material.BARRIER)
.displayName(Component.text("Close", NamedTextColor.RED))
.lore(Component.text("Close this page", NamedTextColor.GRAY))
.build());
public static final ItemStack CONTINUE = ItemUtils.stripItalics(ItemStack.builder(Material.FEATHER)
.displayName(Component.text("Continue", NamedTextColor.GOLD))
.lore(Component.text("Continue to the next stage", NamedTextColor.GRAY))
.build());
public static final ItemStack BACK = ItemUtils.stripItalics(ItemStack.builder(Material.ARROW)
.displayName(Component.text("Back", NamedTextColor.AQUA))
.lore(Component.text("Go back to the previous page", NamedTextColor.GRAY))
.build());
public static final ItemStack COIN = ItemUtils.stripItalics(ItemStack.builder(Material.SUNFLOWER)
.displayName(Component.text("Coin"))
.build());
private Items() {}
}

135
arena/Main.java Normal file
View file

@ -0,0 +1,135 @@
package net.minestom.arena;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.arena.game.ArenaCommand;
import net.minestom.arena.game.mob.MobTestCommand;
import net.minestom.arena.group.GroupCommand;
import net.minestom.arena.group.GroupEvent;
import net.minestom.arena.lobby.Lobby;
import net.minestom.arena.utils.ResourceUtils;
import net.minestom.server.MinecraftServer;
import net.minestom.server.adventure.audience.Audiences;
import net.minestom.server.command.CommandManager;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.event.GlobalEventHandler;
import net.minestom.server.event.player.PlayerChatEvent;
import net.minestom.server.event.player.PlayerDisconnectEvent;
import net.minestom.server.event.player.PlayerLoginEvent;
import net.minestom.server.event.player.PlayerSpawnEvent;
import net.minestom.server.event.server.ServerTickMonitorEvent;
import net.minestom.server.extras.MojangAuth;
import net.minestom.server.extras.lan.OpenToLAN;
import net.minestom.server.extras.velocity.VelocityProxy;
import net.minestom.server.monitoring.TickMonitor;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.MathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicReference;
import static net.minestom.arena.config.ConfigHandler.CONFIG;
final class Main {
private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
MinecraftServer minecraftServer = MinecraftServer.init();
if (CONFIG.prometheus().enabled()) Metrics.init();
try {
ResourceUtils.extractResource("lobby");
} catch (Exception e) {
throw new RuntimeException(e);
}
// Commands
{
CommandManager manager = MinecraftServer.getCommandManager();
manager.setUnknownCommandCallback((sender, c) -> Messenger.warn(sender, "Command not found."));
manager.register(new GroupCommand());
manager.register(new ArenaCommand());
manager.register(new MobTestCommand());
SimpleCommands.register(manager);
}
// Events
{
GlobalEventHandler handler = MinecraftServer.getGlobalEventHandler();
// Group events
GroupEvent.hook(handler);
// Server list
ServerList.hook(handler);
// Login
handler.addListener(PlayerLoginEvent.class, event -> {
final Player player = event.getPlayer();
event.setSpawningInstance(Lobby.INSTANCE);
player.setRespawnPoint(new Pos(0.5, 16, 0.5));
if (CONFIG.permissions().operators().contains(player.getUsername())) {
player.setPermissionLevel(4);
}
Audiences.all().sendMessage(Component.text(
player.getUsername() + " has joined",
NamedTextColor.GREEN
));
});
handler.addListener(PlayerSpawnEvent.class, event -> {
if (!event.isFirstSpawn()) return;
final Player player = event.getPlayer();
Messenger.info(player, "Welcome to Minestom Arena, use /arena to play!");
player.setGameMode(GameMode.ADVENTURE);
player.playSound(Sound.sound(SoundEvent.ENTITY_PLAYER_LEVELUP, Sound.Source.MASTER, 1f, 1f));
player.setEnableRespawnScreen(false);
});
// Logout
handler.addListener(PlayerDisconnectEvent.class, event -> Audiences.all().sendMessage(Component.text(
event.getPlayer().getUsername() + " has left",
NamedTextColor.RED
)));
// Chat
handler.addListener(PlayerChatEvent.class, chatEvent -> {
chatEvent.setChatFormat((event) -> Component.text(event.getEntity().getUsername())
.append(Component.text(" | ", NamedTextColor.DARK_GRAY)
.append(Component.text(event.getMessage(), NamedTextColor.WHITE))));
});
// Monitoring
AtomicReference<TickMonitor> lastTick = new AtomicReference<>();
handler.addListener(ServerTickMonitorEvent.class, event -> {
final TickMonitor monitor = event.getTickMonitor();
Metrics.TICK_TIME.observe(monitor.getTickTime());
Metrics.ACQUISITION_TIME.observe(monitor.getAcquisitionTime());
lastTick.set(monitor);
});
MinecraftServer.getExceptionManager().setExceptionHandler(e -> {
LOGGER.error("Global exception handler", e);
Metrics.EXCEPTIONS.labels(e.getClass().getSimpleName()).inc();
});
// Header/footer
}
if (CONFIG.proxy().enabled()) {
VelocityProxy.enable(CONFIG.proxy().secret());
} else {
OpenToLAN.open();
if (CONFIG.server().mojangAuth()) MojangAuth.init();
}
minecraftServer.start(CONFIG.server().address());
System.out.println("Server startup done! Using configuration " + CONFIG);
}
}

56
arena/Messenger.java Normal file
View file

@ -0,0 +1,56 @@
package net.minestom.arena;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.title.Title;
import net.minestom.server.MinecraftServer;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.timer.TaskSchedule;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
public final class Messenger {
public static final TextColor PINK_COLOR = TextColor.color(209, 72, 212);
public static final TextColor ORANGE_COLOR = TextColor.color(232, 175, 53);
public static void info(Audience audience, String message) {
info(audience, Component.text(message));
}
public static void info(Audience audience, Component message) {
audience.sendMessage(Component.text("! ", PINK_COLOR)
.append(message.color(NamedTextColor.GRAY)));
}
public static void warn(Audience audience, String message) {
warn(audience, Component.text(message));
}
public static void warn(Audience audience, Component message) {
audience.sendMessage(Component.text("* ", ORANGE_COLOR)
.append(message.color(NamedTextColor.GRAY)));
}
public static CompletableFuture<Void> countdown(Audience audience, int from) {
final CompletableFuture<Void> completableFuture = new CompletableFuture<>();
final AtomicInteger countdown = new AtomicInteger(from);
MinecraftServer.getSchedulerManager().submitTask(() -> {
final int count = countdown.getAndDecrement();
if (count <= 0) {
completableFuture.complete(null);
return TaskSchedule.stop();
}
audience.showTitle(Title.title(Component.text(count, NamedTextColor.GREEN), Component.empty()));
audience.playSound(Sound.sound(SoundEvent.BLOCK_NOTE_BLOCK_PLING, Sound.Source.BLOCK, 1, 1), Sound.Emitter.self());
return TaskSchedule.seconds(1);
});
return completableFuture;
}
}

156
arena/Metrics.java Normal file
View file

@ -0,0 +1,156 @@
package net.minestom.arena;
import com.sun.management.OperatingSystemMXBean;
import io.prometheus.client.*;
import io.prometheus.client.exporter.HTTPServer;
import io.prometheus.client.hotspot.GarbageCollectorExports;
import io.prometheus.client.hotspot.MemoryPoolsExports;
import net.minestom.arena.config.ConfigHandler;
import net.minestom.arena.utils.NetworkUsage;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.event.entity.EntitySpawnEvent;
import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent;
import net.minestom.server.event.player.PlayerDisconnectEvent;
import net.minestom.server.event.player.PlayerLoginEvent;
import net.minestom.server.event.player.PlayerPacketEvent;
import net.minestom.server.event.player.PlayerPacketOutEvent;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class Metrics {
public static final Gauge ENTITIES = Gauge.build().name("entities")
.help("Total entities alive (excluding players)").register();
public static final Gauge GAMES_IN_PROGRESS = Gauge.build().name("games_in_progress")
.labelNames("type").help("Games currently running").register();
public static final Counter GAMES_PLAYED = Counter.build().name("games_played")
.labelNames("type").help("Number of games played").register();
public static final Summary TICK_TIME = Summary.build().name("tick_time")
.help("ms per tick").quantile(0, 1).quantile(.5, .01).quantile(1, 0)
.maxAgeSeconds(5).unit("ms").register();
public static final Summary ACQUISITION_TIME = Summary.build().name("acquisition_time")
.help("ms per acquisition").quantile(0, 1).quantile(.5, .01).quantile(1, 0)
.maxAgeSeconds(5).unit("ms").register();
public static final Counter EXCEPTIONS = Counter.build().name("exceptions")
.help("Number of exceptions").labelNames("simple_name").register();
private static final Counter PACKETS = Counter.build().name("packets").help("Number of packets by direction")
.labelNames("direction").register();
private static final Gauge ONLINE_PLAYERS = Gauge.build().name("online_players")
.help("Number of currently online players").register();
private static final Info GENERIC_INFO = Info.build().name("generic").help("Generic system information")
.register();
private static final OperatingSystemMXBean systemMXBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
public static void init() {
try {
final String unknown = "unknown";
GENERIC_INFO.info(
"java_version", System.getProperty("java.version", unknown),
"java_vendor", System.getProperty("java.vendor", unknown),
"os_arch", System.getProperty("os.arch", unknown),
"os_name", System.getProperty("os.name", unknown),
"os_version", System.getProperty("os.version", unknown),
"available_processors", ""+systemMXBean.getAvailableProcessors()
);
// Packets & players
MinecraftServer.getGlobalEventHandler()
.addListener(PlayerPacketEvent.class, e -> Metrics.PACKETS.labels("in").inc())
.addListener(PlayerPacketOutEvent.class, e -> Metrics.PACKETS.labels("out").inc())
.addListener(PlayerLoginEvent.class, e -> Metrics.ONLINE_PLAYERS.inc())
.addListener(PlayerDisconnectEvent.class, e -> Metrics.ONLINE_PLAYERS.dec())
.addListener(EntitySpawnEvent.class, e -> {
if (!(e.getEntity() instanceof Player)) Metrics.ENTITIES.inc();
}).addListener(RemoveEntityFromInstanceEvent.class, e -> {
if (!(e.getEntity() instanceof Player)) Metrics.ENTITIES.dec();
});
// Network usage
if (NetworkUsage.checkEnabledOrExtract()) {
NetworkUsage.resetCounters();
NetworkCounter.build().name("network_io").help("Network usage").unit("bytes").labelNames("direction")
.register();
}
CPUGauge.build().name("cpu").help("CPU Usage").register();
new HTTPServer(ConfigHandler.CONFIG.prometheus().port());
new MemoryPoolsExports().register();
new GarbageCollectorExports().register();
} catch (IOException e) {
MinecraftServer.getExceptionManager().handleException(e);
}
}
private static class NetworkCounter extends SimpleCollector<Counter.Child> {
private final double created = System.currentTimeMillis()/1000f;
private final static List<String> outLabels = List.of("out");
private final static List<String> inLabels = List.of("in");
protected NetworkCounter(Builder b) {
super(b);
}
public static class Builder extends SimpleCollector.Builder<NetworkCounter.Builder, NetworkCounter> {
@Override
public NetworkCounter create() {
return new NetworkCounter(this);
}
}
public static Builder build() {
return new Builder();
}
@Override
protected Counter.Child newChild() {
return null;
}
@Override
public List<MetricFamilySamples> collect() {
List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>();
samples.add(new MetricFamilySamples.Sample(fullname + "_total", labelNames, outLabels, NetworkUsage.getBytesSent()));
samples.add(new MetricFamilySamples.Sample(fullname + "_created", labelNames, outLabels, created));
samples.add(new MetricFamilySamples.Sample(fullname + "_total", labelNames, inLabels, NetworkUsage.getBytesReceived()));
samples.add(new MetricFamilySamples.Sample(fullname + "_created", labelNames, inLabels, created));
return familySamplesList(Type.COUNTER, samples);
}
}
private static class CPUGauge extends SimpleCollector<Gauge.Child> {
protected CPUGauge(Builder b) {
super(b);
}
public static class Builder extends SimpleCollector.Builder<CPUGauge.Builder, CPUGauge> {
@Override
public CPUGauge create() {
return new CPUGauge(this);
}
}
public static Builder build() {
return new Builder();
}
@Override
protected Gauge.Child newChild() {
return new Gauge.Child();
}
@Override
public List<MetricFamilySamples> collect() {
List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>(1);
samples.add(new MetricFamilySamples.Sample(fullname, labelNames, Collections.emptyList(), systemMXBean.getProcessCpuLoad()));
return familySamplesList(Type.GAUGE, samples);
}
}
}

51
arena/ServerList.java Normal file
View file

@ -0,0 +1,51 @@
package net.minestom.arena;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.minestom.arena.config.ConfigHandler;
import net.minestom.arena.config.ConfigurationReloadedEvent;
import net.minestom.server.MinecraftServer;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.ping.ResponseData;
import java.io.InputStream;
import java.util.Base64;
import java.util.List;
final class ServerList {
private static final String FAVICON = favicon();
private static Component motd = motd();
public static void hook(EventNode<Event> eventNode) {
eventNode.addListener(ServerListPingEvent.class, event -> {
final ResponseData responseData = event.getResponseData();
responseData.setDescription(motd);
if (FAVICON != null)
responseData.setFavicon(FAVICON);
responseData.setMaxPlayer(100);
responseData.addEntries(MinecraftServer.getConnectionManager().getOnlinePlayers());
}).addListener(ConfigurationReloadedEvent.class, e -> motd = motd());
}
private static String favicon() {
String favicon = null;
try (InputStream stream = Main.class.getResourceAsStream("/favicon.png")) {
if (stream != null)
favicon = "data:image/png;base64," + Base64.getEncoder().encodeToString(stream.readAllBytes());
} catch (Exception e) {
e.printStackTrace();
}
return favicon;
}
private static Component motd() {
final MiniMessage miniMessage = MiniMessage.miniMessage();
final List<String> motd = ConfigHandler.CONFIG.server().motd();
return motd.stream()
.map(miniMessage::deserialize)
.reduce(Component.empty(), (a, b) -> a.append(b).appendNewline());
}
}

53
arena/SimpleCommands.java Normal file
View file

@ -0,0 +1,53 @@
package net.minestom.arena;
import net.minestom.arena.config.ConfigHandler;
import net.minestom.arena.game.ArenaManager;
import net.minestom.arena.group.GroupManager;
import net.minestom.arena.lobby.Lobby;
import net.minestom.arena.utils.CommandUtils;
import net.minestom.server.command.CommandManager;
import net.minestom.server.command.ConsoleSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.entity.Player;
import java.util.List;
/**
* Place for commands with little to no complexity
*/
final class SimpleCommands {
private SimpleCommands() {}
public static void register(CommandManager manager) {
for (Command command : commands())
manager.register(command);
}
private static List<Command> commands() {
final Command stop = new Command("stop");
stop.setCondition((sender, commandString) -> sender instanceof ConsoleSender ||
(sender instanceof Player player && player.getPermissionLevel() == 4));
stop.setDefaultExecutor((sender, context) -> ArenaManager.stopServer());
final Command ping = new Command("ping", "latency");
ping.setDefaultExecutor((sender, context) -> {
final Player player = (Player) sender;
Messenger.info(player, "Your ping is " + player.getLatency() + "ms");
});
final Command leave = new Command("leave", "l");
leave.setCondition(CommandUtils::arenaOnly);
leave.setDefaultExecutor((sender, context) -> {
final Player player = (Player) sender;
player.setInstance(Lobby.INSTANCE);
player.setHealth(player.getMaxHealth());
GroupManager.removePlayer(player);
});
final Command reload = new Command("reload");
reload.setCondition(CommandUtils::consoleOnly);
reload.setDefaultExecutor(((sender, context) -> ConfigHandler.loadConfig()));
return List.of(stop, ping, leave, reload);
}
}

24
arena/config/Config.java Normal file
View file

@ -0,0 +1,24 @@
package net.minestom.arena.config;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.List;
public record Config(Server server, Proxy proxy, Permissions permissions, Prometheus prometheus) {
public record Server(@Default("0.0.0.0") String host, @Default("25565") int port, @Default("true") boolean mojangAuth, @Default("[\"Line1\",\"Line2\"]") List<String> motd) {
public SocketAddress address() {
return new InetSocketAddress(host, port);
}
}
public record Proxy(@Default("false") boolean enabled, @Default("forwarding-secret") String secret) {
@Override
public String toString() {
return "Proxy[enabled="+enabled+", secret=<hidden>]";
}
}
public record Permissions(@Default("[]") List<String> operators) {}
public record Prometheus(@Default("false") boolean enabled, @Default("9090") int port) {}
}

View file

@ -0,0 +1,174 @@
package net.minestom.arena.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import net.minestom.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Parameter;
import java.lang.reflect.RecordComponent;
import java.util.*;
public final class ConfigHandler {
public volatile static Config CONFIG;
private static boolean reload = false;
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigHandler.class);
private static final Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapterFactory(new RecordTypeAdapterFactory())
.create();
private static final File configFile = new File("config.json");
static {
loadConfig();
}
public synchronized static void loadConfig() {
Config old = CONFIG;
if (configFile.exists()) {
try (JsonReader reader = new JsonReader(new FileReader(configFile))) {
CONFIG = gson.fromJson(reader, Config.class);
} catch (IOException exception) {
LOGGER.error("Failed to load configuration file, using defaults.", exception);
loadDefaults();
}
} else {
loadDefaults();
try {
final FileWriter writer = new FileWriter(configFile);
gson.toJson(CONFIG, writer);
writer.flush();
writer.close();
} catch (IOException exception) {
LOGGER.error("Failed to write default configuration.", exception);
}
}
if (reload) {
MinecraftServer.getGlobalEventHandler().call(new ConfigurationReloadedEvent(old, CONFIG));
LOGGER.info("Configuration reloaded!");
} else {
reload = true;
}
}
private synchronized static void loadDefaults() {
CONFIG = gson.fromJson("{}", Config.class);
}
private ConfigHandler() {}
private static class RecordTypeAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final Class<? super T> clazz = type.getRawType();
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
if (!clazz.isRecord())
return null;
return new TypeAdapter<>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
@SuppressWarnings("unchecked")
@Override
public T read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
} else {
final RecordComponent[] recordComponents = clazz.getRecordComponents();
final Map<String, TypeToken<?>> typeMap = new HashMap<>();
final Map<String, Object> argsMap = new HashMap<>();
for (RecordComponent component : recordComponents)
typeMap.put(component.getName(), TypeToken.get(component.getGenericType()));
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
argsMap.put(name, gson.getAdapter(typeMap.get(name)).read(reader));
}
reader.endObject();
Arrays.stream(recordComponents).filter(x -> !argsMap.containsKey(x.getName())).forEach(x -> {
final String name = x.getName();
final Class<?> argClazz = x.getType();
final Default def = x.getAnnotation(Default.class);
if (def == null) {
argsMap.put(name, instantiateWithDefaults(argClazz));
return;
}
try {
if (argClazz == String.class) {
argsMap.put(name, def.value());
} else {
argsMap.put(name, gson.getAdapter(typeMap.get(name)).fromJson(def.value()));
}
} catch (IOException ignored) {}
});
final List<Object> args = new ArrayList<>();
final List<Class<?>> argTypes = new ArrayList<>();
for (RecordComponent component : recordComponents) {
args.add(argsMap.get(component.getName()));
argTypes.add(component.getType());
}
try {
Constructor<? super T> constructor = clazz.getDeclaredConstructor(argTypes.toArray(Class<?>[]::new));
constructor.setAccessible(true);
return (T) constructor.newInstance(args.toArray(Object[]::new));
} catch (ReflectiveOperationException e) {
return null;
}
}
}
private Object instantiateWithDefaults(Class<?> clazz) {
final List<Object> args = new ArrayList<>();
final Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
for (Parameter param : constructor.getParameters()) {
final Class<?> paramClazz = param.getType();
final Default def = param.getAnnotation(Default.class);
if (def == null) {
args.add(instantiateWithDefaults(paramClazz));
continue;
}
try {
if (paramClazz == String.class) {
args.add(def.value());
} else {
args.add(gson.getAdapter(TypeToken.get(param.getType())).fromJson(def.value()));
}
} catch (IOException ignored) {
args.add(null);
}
}
try {
return constructor.newInstance(args.toArray(Object[]::new));
} catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
return null;
}
}
};
}
}
}

View file

@ -0,0 +1,6 @@
package net.minestom.arena.config;
import net.minestom.server.event.Event;
public record ConfigurationReloadedEvent(Config previousConfig, Config currentConfig) implements Event {
}

View file

@ -0,0 +1,9 @@
package net.minestom.arena.config;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Default {
String value();
}

View file

@ -0,0 +1,55 @@
package net.minestom.arena.feature;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityProjectile;
import net.minestom.server.entity.Player;
import net.minestom.server.event.EventListener;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.item.ItemUpdateStateEvent;
import net.minestom.server.event.player.PlayerItemAnimationEvent;
import net.minestom.server.event.trait.InstanceEvent;
import net.minestom.server.item.Material;
import net.minestom.server.tag.Tag;
import net.minestom.server.utils.MathUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.function.BiFunction;
/**
* @param projectileGenerator Uses the return value as the entity to shoot (in lambda arg 1 is shooter, arg 2 is power)
*/
record BowFeature(@NotNull BiFunction<Entity, Double, EntityProjectile> projectileGenerator) implements Feature {
private static final Tag<Long> CHARGE_SINCE_TAG = Tag.Long("bow_charge_since").defaultValue(Long.MAX_VALUE);
@Override
public void hook(@NotNull EventNode<InstanceEvent> node) {
node.addListener(EventListener.builder(PlayerItemAnimationEvent.class)
.handler(event -> event.getPlayer().setTag(CHARGE_SINCE_TAG, System.currentTimeMillis()))
.filter(event -> event.getItemAnimationType() == PlayerItemAnimationEvent.ItemAnimationType.BOW)
.build()
).addListener(EventListener.builder(ItemUpdateStateEvent.class)
.handler(event -> {
final Player player = event.getPlayer();
final double chargedFor = (System.currentTimeMillis() - player.getTag(CHARGE_SINCE_TAG)) / 1000D;
final double power = MathUtils.clamp((chargedFor * chargedFor + 2 * chargedFor) / 2D, 0, 1);
if (power > 0.2) {
final EntityProjectile projectile = projectileGenerator.apply(player, power);
final Pos position = player.getPosition().add(0, player.getEyeHeight(), 0);
projectile.setInstance(Objects.requireNonNull(player.getInstance()), position);
Vec direction = projectile.getPosition().direction();
projectile.shoot(position.add(direction).sub(0, 0.2, 0), power * 3, 1.0);
}
// Restore arrow
player.getInventory().update();
})
.filter(event -> event.getItemStack().material() == Material.BOW)
.build());
}
}

View file

@ -0,0 +1,122 @@
package net.minestom.arena.feature;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityProjectile;
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.damage.DamageType;
import net.minestom.server.entity.hologram.Hologram;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent;
import net.minestom.server.event.trait.InstanceEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.tag.Tag;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.ToDoubleBiFunction;
import java.util.function.ToLongFunction;
/**
* @param playerCombat Allow player combat
* @param damageFunction Uses the return value as damage to apply (in lambda arg 1 is attacker, arg 2 is victim)
* @param invulnerabilityFunction Uses the return value as time an entity is invulnerable after getting attacked (in lambda arg 1 is victim)
*/
record CombatFeature(boolean playerCombat, ToDoubleBiFunction<Entity, Entity> damageFunction, ToLongFunction<Entity> invulnerabilityFunction) implements Feature {
private static final Tag<Long> INVULNERABLE_UNTIL_TAG = Tag.Long("invulnerable_until").defaultValue(0L);
@Override
public void hook(@NotNull EventNode<InstanceEvent> node) {
node.addListener(ProjectileCollideWithEntityEvent.class, event -> {
if (!(event.getTarget() instanceof LivingEntity target)) return;
if (!(event.getEntity() instanceof EntityProjectile projectile)) return;
// PVP is disabled and two players have attempted to hit each other
if (!playerCombat && target instanceof Player && projectile.getShooter() instanceof Player) return;
// Don't apply damage if entity is invulnerable
final long now = System.currentTimeMillis();
final long invulnerableUntil = target.getTag(INVULNERABLE_UNTIL_TAG);
if (invulnerableUntil > now) return;
float damage = (float) damageFunction.applyAsDouble(projectile, target);
target.damage(DamageType.fromProjectile(projectile.getShooter(), projectile), damage);
target.setTag(INVULNERABLE_UNTIL_TAG, now + invulnerabilityFunction.applyAsLong(target));
takeKnockbackFromArrow(target, projectile);
if (damage > 0) spawnHologram(target, damage);
projectile.remove();
}).addListener(EntityAttackEvent.class, event -> {
if (!(event.getTarget() instanceof LivingEntity target)) return;
// PVP is disabled and two players have attempted to hit each other
if (!playerCombat && target instanceof Player && event.getEntity() instanceof Player) return;
// Can't have dead sources attacking things
if (((LivingEntity) event.getEntity()).isDead()) return;
// Don't apply damage if entity is invulnerable
final long now = System.currentTimeMillis();
final long invulnerableUntil = target.getTag(INVULNERABLE_UNTIL_TAG);
if (invulnerableUntil > now) return;
float damage = (float) damageFunction.applyAsDouble(event.getEntity(), target);
target.damage(DamageType.fromEntity(event.getEntity()), damage);
target.setTag(INVULNERABLE_UNTIL_TAG, now + invulnerabilityFunction.applyAsLong(target));
takeKnockback(target, event.getEntity());
if (damage > 0) spawnHologram(target, damage);
});
}
private static void takeKnockback(Entity target, Entity source) {
target.takeKnockback(
0.3f,
Math.sin(source.getPosition().yaw() * (Math.PI / 180)),
-Math.cos(source.getPosition().yaw() * (Math.PI / 180))
);
}
private static void takeKnockbackFromArrow(Entity target, EntityProjectile source) {
if (source.getShooter() == null) return;
takeKnockback(target, source.getShooter());
}
private static void spawnHologram(Entity target, float damage) {
damage = MathUtils.round(damage, 2);
new DamageHologram(
target.getInstance(),
target.getPosition().add(0, target.getEyeHeight(), 0),
Component.text(damage, NamedTextColor.RED)
);
}
private static final class DamageHologram extends Hologram {
private DamageHologram(Instance instance, Pos spawnPosition, Component text) {
super(instance, spawnPosition, text, true, true);
getEntity().getEntityMeta().setHasNoGravity(false);
Random random = ThreadLocalRandom.current();
getEntity().setVelocity(getPosition()
.direction()
.withX(random.nextDouble(2))
.withY(3)
.withZ(random.nextDouble(2))
.normalize().mul(3));
getEntity().scheduleRemove(Duration.of(15, TimeUnit.SERVER_TICK));
}
}
}

View file

@ -0,0 +1,30 @@
package net.minestom.arena.feature;
import net.minestom.server.entity.ItemEntity;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.item.ItemDropEvent;
import net.minestom.server.event.trait.InstanceEvent;
import net.minestom.server.item.ItemStack;
import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.NotNull;
import java.util.function.Predicate;
record DropFeature(Predicate<ItemStack> allowPredicate) implements Feature {
@Override
public void hook(@NotNull EventNode<InstanceEvent> node) {
node.addListener(ItemDropEvent.class, event -> {
ItemStack item = event.getItemStack();
if (!allowPredicate.test(item)) {
event.setCancelled(true);
return;
}
ItemEntity itemEntity = new ItemEntity(item);
itemEntity.setPickupDelay(40, TimeUnit.SERVER_TICK);
itemEntity.setInstance(event.getInstance(), event.getPlayer().getPosition().add(0, 1.5, 0));
itemEntity.setVelocity(event.getPlayer().getPosition().direction().mul(6));
});
}
}

View file

@ -0,0 +1,10 @@
package net.minestom.arena.feature;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.trait.InstanceEvent;
import org.jetbrains.annotations.NotNull;
@FunctionalInterface
public interface Feature {
void hook(@NotNull EventNode<InstanceEvent> node);
}

View file

@ -0,0 +1,27 @@
package net.minestom.arena.feature;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityProjectile;
import net.minestom.server.entity.Player;
import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.function.*;
public final class Features {
public static @NotNull Feature bow(BiFunction<Entity, Double, EntityProjectile> projectileGenerator) {
return new BowFeature(projectileGenerator);
}
public static @NotNull Feature combat(boolean combat, ToDoubleBiFunction<Entity, Entity> damageFunction, ToLongFunction<Entity> invulnerabilityFunction) {
return new CombatFeature(combat, damageFunction, invulnerabilityFunction);
}
public static @NotNull Feature drop(Predicate<ItemStack> allowPredicate) {
return new DropFeature(allowPredicate);
}
public static @NotNull Feature functionalItem(Predicate<ItemStack> trigger, Consumer<Player> consumer, long cooldown) {
return new FunctionalItemFeature(trigger, consumer, cooldown);
}
}

View file

@ -0,0 +1,38 @@
package net.minestom.arena.feature;
import net.minestom.server.entity.Player;
import net.minestom.server.event.EventListener;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.player.PlayerHandAnimationEvent;
import net.minestom.server.event.trait.InstanceEvent;
import net.minestom.server.item.ItemStack;
import net.minestom.server.tag.Tag;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Predicate;
record FunctionalItemFeature(Predicate<ItemStack> trigger, Consumer<Player> consumer, long cooldown) implements Feature {
@Override
public void hook(@NotNull EventNode<InstanceEvent> node) {
final UUID random = UUID.randomUUID();
final Tag<Long> lastUseTag = Tag.Long("item_" + random + "_last_use").defaultValue(0L);
node.addListener(EventListener.builder(PlayerHandAnimationEvent.class)
.handler(event -> {
final Player player = event.getPlayer();
final long lastUse = player.getTag(lastUseTag);
final long now = System.currentTimeMillis();
if (now - lastUse >= cooldown) {
player.setTag(lastUseTag, now);
consumer.accept(player);
}
})
.filter(event -> trigger.test(event.getPlayer().getItemInHand(event.getHand())))
.filter(event -> event.getPlayer().getOpenInventory() == null)
.build()
);
}
}

View file

@ -0,0 +1,209 @@
package net.minestom.arena.game;
import net.minestom.arena.group.Group;
import net.minestom.arena.utils.ConcurrentUtils;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
public abstract class AbstractArena implements Arena {
private final CompletableFuture<Void> gameFuture = new CompletableFuture<>();
private final AtomicReference<GameState> state = new AtomicReference<>(GameState.CREATED);
private Instant startDate;
private Instant endDate;
private final static Duration END_TIMEOUT = Duration.ofMinutes(10);
///////////////////////////////////////////////////////////////////////////
// Getter methods
///////////////////////////////////////////////////////////////////////////
/**
* Used to get a future that represents this game life
*
* @return a future that is completed when the game state is either {@link GameState#ENDED} or {@link GameState#KILLED}
*/
public final CompletableFuture<Void> gameFuture() {
return this.gameFuture;
}
public final Instant startDate() {
return startDate;
}
public final Instant stopDate() {
return endDate;
}
public final GameState state() {
return this.state.get();
}
public abstract @NotNull Group group();
///////////////////////////////////////////////////////////////////////////
// Life cycle methods
///////////////////////////////////////////////////////////////////////////
/**
* Used to prepare the game for players e.g. generate the world, summon entities, register listeners, etc.
* Players SHOULD NOT be altered in this state
*
* @return a future that completes when the game can be started with {@link #start()}
*/
public abstract @NotNull CompletableFuture<Void> init();
/**
* Used to start the game, here you can change the players' instance, etc.
*
* @return a future that completes when the actual gameplay begins
*/
protected abstract CompletableFuture<Void> onStart();
/**
* Used to start the game, the start sequence is the following (note that a shutdown will interrupt this flow):
* <ol>
* <li>Set state to {@link GameState#INITIALIZING}</li>
* <li>Execute {@link #init()}</li>
* <li>Set state to {@link GameState#STARTING}</li>
* <li>Execute {@link #onStart()}</li>
* <li>Set state to {@link GameState#STARTED}</li>
* </ol>
*
* @throws RuntimeException if called when the state isn't {@link GameState#CREATED}
*/
public final void start() {
if (!this.state.compareAndSet(GameState.CREATED, GameState.INITIALIZING)) {
throw new RuntimeException("Cannot start a AbstractArena twice!");
}
init().thenRun(() -> {
if (!this.state.compareAndSet(GameState.INITIALIZING, GameState.STARTING)) {
// A shutdown has been initiated during initialization, don't start the game
return;
}
onStart().thenRun(() -> {
if (!this.state.compareAndSet(GameState.STARTING, GameState.STARTED)) {
// A shutdown has been initiated during game start, don't change state
return;
}
this.startDate = Instant.now();
});
});
}
/**
* Used to reset the players after the game
*
* @return a future that completes when all players have been reset, this SHOULD NOT wait on gameplay
*/
protected abstract CompletableFuture<Void> onEnd();
/**
* Used to end the game normally, only the first call will execute {@link #onEnd()}
* multiple calls to this method will be ignored
*
* @return {@link #gameFuture()}
*/
public final CompletableFuture<Void> end() {
if (!tryAdvance(GameState.ENDING)) {
return gameFuture();
}
onEnd().thenRun(() -> {
if (!tryAdvance(GameState.ENDED)) {
// AbstractArena was killed, don't alter the state
return;
}
this.endDate = Instant.now();
this.gameFuture.complete(null);
});
return gameFuture();
}
/**
* Used to prepare the game for ending within the specified timeout
*
* @param shutdownTimeout duration in which the game should end
* @return a future which is completed when the internal state of the game allows the call of {@link #end()}
*/
protected abstract CompletableFuture<Void> onShutdown(Duration shutdownTimeout);
/**
* Used to shut down the game gracefully, shutdown process id the following:
* <ol>
* <li>Call {@link #onShutdown(Duration)} with the timeout</li>
* <li>Wait for the returned future to complete or the timeout to be reached</li>
* <li>If <b>(A)</b> the timeout wasn't reached continue with the normal ending procedure by calling {@link #end()}
* or if it was reached, but <b>(B)</b> the game already ended then return otherwise <b>(C)</b> kill the game</li>
* </ol>
*
* @return {@link #gameFuture()}
*/
public final CompletableFuture<Void> shutdown() {
if (!tryAdvance(GameState.SHUTTING_DOWN)) {
return gameFuture();
}
ConcurrentUtils.thenRunOrTimeout(onShutdown(END_TIMEOUT), END_TIMEOUT, (timeoutReached) -> {
if (timeoutReached) {
if (!tryAdvance(GameState.KILLED)) {
// The game ended already, we can safely return
return;
}
// Kill game
this.endDate = Instant.now();
this.gameFuture.complete(null);
this.kill();
} else {
// Execute normal end procedure
end();
}
});
return gameFuture();
}
/**
* Called when the game didn't finish in time after {@link #shutdown()} has been called
*/
protected void kill() {}
///////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////
/**
* Advance game state
*
* @return true if the game state advanced, false otherwise
*/
private boolean tryAdvance(GameState newState) {
return ConcurrentUtils.testAndSet(this.state, GameState::isBefore, newState);
}
public enum GameState {
CREATED(0), INITIALIZING(1), STARTING(2), STARTED(3), ENDING(4), SHUTTING_DOWN(4), ENDED(5), KILLED(5);
private final int sequence;
GameState(int sequence) {
this.sequence = sequence;
}
public boolean isBefore(GameState state) {
return this.sequence < state.sequence;
}
public boolean isOrBefore(GameState state) {
return this.sequence <= state.sequence;
}
public boolean isAfter(GameState state) {
return this.sequence > state.sequence;
}
public boolean isOrAfter(GameState state) {
return this.sequence >= state.sequence;
}
}
}

21
arena/game/Arena.java Normal file
View file

@ -0,0 +1,21 @@
package net.minestom.arena.game;
import net.minestom.arena.group.Group;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public interface Arena {
@NotNull Group group();
@NotNull CompletableFuture<Void> init();
void start();
void stop();
@ApiStatus.NonExtendable
default void unregister() {
ArenaManager.unregister(this);
}
}

View file

@ -0,0 +1,161 @@
package net.minestom.arena.game;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.arena.Items;
import net.minestom.arena.lobby.Lobby;
import net.minestom.arena.Messenger;
import net.minestom.arena.group.Group;
import net.minestom.arena.utils.CommandUtils;
import net.minestom.arena.utils.ItemUtils;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.ArgumentEnum;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.entity.Player;
import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.InventoryType;
import net.minestom.server.inventory.click.ClickType;
import net.minestom.server.item.Enchantment;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.tag.Tag;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class ArenaCommand extends Command {
public ArenaCommand() {
super("arena", "play", "join", "game");
setCondition(CommandUtils::lobbyOnly);
setDefaultExecutor((sender, context) -> open((Player) sender));
addSyntax((sender, context) ->
play((Player) sender, context.get("type"), Set.of()),
ArgumentType.Enum("type", ArenaType.class).setFormat(ArgumentEnum.Format.LOWER_CASED));
}
public static void open(@NotNull Player player) {
player.openInventory(new ArenaInventory());
}
private static void play(@NotNull Player player, @NotNull ArenaType type, @NotNull Set<ArenaOption> options) {
if (player.getInstance() != Lobby.INSTANCE) {
Messenger.warn(player, "You are not in the lobby! Join the lobby first.");
return;
}
final Group group = Group.findGroup(player);
if (group.leader() != player) {
Messenger.warn(player, "You are not the leader of your group!");
return;
}
Arena arena = type.createInstance(group, options);
ArenaManager.register(arena);
arena.init().thenRun(() -> group.members().forEach(Player::refreshCommands));
}
private static final class ArenaInventory extends Inventory {
private static final Tag<Integer> ARENA_TAG = Tag.Integer("arena").defaultValue(-1);
private static final ItemStack HEADER = ItemUtils.stripItalics(ItemStack.builder(Material.IRON_BARS)
.displayName(Component.text("Arena", NamedTextColor.RED))
.lore(Component.text("Select an arena to play in", NamedTextColor.GRAY))
.build());
ArenaInventory() {
super(InventoryType.CHEST_4_ROW, Component.text("Arena"));
setItemStack(4, HEADER);
setItemStack(31, Items.CLOSE);
final ArenaType[] arenaTypes = ArenaType.values();
int index = 13 - arenaTypes.length / 2;
for (ArenaType arenaType : ArenaType.values())
setItemStack(index++, ItemUtils.stripItalics(arenaType.item()
.withLore(List.of(Component.text(
"Left click to play or right click to configure",
NamedTextColor.GRAY
)))
.withTag(ARENA_TAG, arenaType.ordinal())));
addInventoryCondition((player, slot, clickType, result) -> {
result.setCancel(true);
if (slot == 31) { // Close button
player.closeInventory();
return;
}
final int arena = result.getClickedItem().getTag(ARENA_TAG);
if (arena == -1) return;
final ArenaType type = ArenaType.values()[arena];
if (clickType == ClickType.RIGHT_CLICK) {
player.openInventory(new ArenaOptionInventory(this, type));
} else{
player.closeInventory();
play(player, type, Set.of());
}
});
}
}
private static final class ArenaOptionInventory extends Inventory {
private static final ItemStack PLAY_ITEM = ItemUtils.stripItalics(ItemStack.builder(Material.NOTE_BLOCK)
.displayName(Component.text("Play", NamedTextColor.GREEN))
.lore(Component.text("Play this arena", NamedTextColor.GRAY))
.build());
private static final Tag<Integer> OPTION_TAG = Tag.Integer("option").defaultValue(-1);
private final ArenaType type;
private final List<ArenaOption> availableOptions;
private final Set<ArenaOption> selectedOptions = new HashSet<>();
ArenaOptionInventory(@NotNull Inventory parent, @NotNull ArenaType type) {
super(InventoryType.CHEST_4_ROW, Component.text("Arena Options"));
this.type = type;
availableOptions = type.availableOptions();
draw();
addInventoryCondition((player, slot, c, result) -> {
result.setCancel(true);
if (slot == 30) { // Play button
player.closeInventory();
play(player, type, selectedOptions);
return;
}
if (slot == 32) { // Back button
player.openInventory(parent);
return;
}
final int index = result.getClickedItem().getTag(OPTION_TAG);
if (index == -1) return;
final ArenaOption option = availableOptions.get(index);
if (!selectedOptions.add(option)) {
selectedOptions.remove(option);
}
draw();
});
}
private void draw() {
setItemStack(4, type.item());
setItemStack(30, PLAY_ITEM);
setItemStack(32, Items.BACK);
final int start = 13 - availableOptions.size() / 2;
int index = 0;
for (ArenaOption option : availableOptions)
setItemStack(start + index, option.item().withTag(OPTION_TAG, index++).withMeta(builder -> {
if (selectedOptions.contains(option)) builder.enchantment(Enchantment.PROTECTION, (short) 1);
}));
}
}
}

View file

@ -0,0 +1,53 @@
package net.minestom.arena.game;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.arena.Metrics;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnmodifiableView;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
public final class ArenaManager {
private static final List<Arena> ARENAS = new CopyOnWriteArrayList<>();
private static volatile boolean stopQueued;
static void register(@NotNull Arena arena) {
Metrics.GAMES_IN_PROGRESS.labels(ArenaType.getMetricsDisplayName(arena)).inc();
ARENAS.add(arena);
}
static void unregister(@NotNull Arena arena) {
Metrics.GAMES_IN_PROGRESS.labels(ArenaType.getMetricsDisplayName(arena)).dec();
Metrics.GAMES_PLAYED.labels(ArenaType.getMetricsDisplayName(arena)).inc();
ARENAS.remove(arena);
if (ARENAS.size() == 0 && stopQueued)
stopServerNow();
}
public static @NotNull @UnmodifiableView List<Arena> list() {
return Collections.unmodifiableList(ARENAS);
}
public static void stopServer() {
Collection<Arena> arenas = list();
stopQueued = true;
for (Arena arena : arenas) arena.stop();
if (arenas.size() == 0) stopServerNow();
}
private static void stopServerNow() {
for (Player player : MinecraftServer.getConnectionManager().getOnlinePlayers())
player.kick(Component.text("Server is shutting down", NamedTextColor.RED));
CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS)
.execute(MinecraftServer::stopCleanly);
}
}

View file

@ -0,0 +1,21 @@
package net.minestom.arena.game;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.minestom.arena.utils.ItemUtils;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import org.jetbrains.annotations.NotNull;
public record ArenaOption(@NotNull String name, @NotNull String description,
@NotNull TextColor color, @NotNull Material material) {
public @NotNull ItemStack item() {
return ItemUtils.stripItalics(ItemStack.builder(material)
.displayName(Component.text(name, color))
.lore(Component.text(description, NamedTextColor.GRAY))
.meta(ItemUtils::hideFlags)
.build());
}
}

68
arena/game/ArenaType.java Normal file
View file

@ -0,0 +1,68 @@
package net.minestom.arena.game;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.minestom.arena.game.mob.MobArena;
import net.minestom.arena.group.Group;
import net.minestom.arena.utils.ItemUtils;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.BiFunction;
enum ArenaType {
MOB("Mob Arena", NamedTextColor.GREEN, Material.ZOMBIE_HEAD, MobArena::new, MobArena.class, MobArena.OPTIONS);
private final ItemStack item;
private final BiFunction<Group, Set<ArenaOption>, Arena> supplier;
private final List<ArenaOption> availableOptions;
private final Class<? extends Arena> clazz;
private final String name;
private static final Map<Class<? extends Arena>, ArenaType> classToType = new HashMap<>();
static {
for (ArenaType value : ArenaType.values()) {
classToType.put(value.clazz, value);
}
}
ArenaType(@NotNull String name, @NotNull TextColor color, @NotNull Material material,
@NotNull BiFunction<Group, Set<ArenaOption>, Arena> supplier, @NotNull Class<? extends Arena> clazz,
@NotNull List<ArenaOption> availableOptions) {
item = ItemUtils.stripItalics(ItemStack.builder(material)
.displayName(Component.text(name, color))
.meta(ItemUtils::hideFlags)
.build());
this.supplier = supplier;
this.name = name;
this.clazz = clazz;
this.availableOptions = List.copyOf(availableOptions);
}
public ItemStack item() {
return item;
}
public List<ArenaOption> availableOptions() {
return availableOptions;
}
public Arena createInstance(Group group, Set<ArenaOption> options) {
return supplier.apply(group, Set.copyOf(options));
}
public static @Nullable ArenaType typeOf(Arena arena) {
return classToType.get(arena.getClass());
}
public static String getMetricsDisplayName(Arena arena) {
final ArenaType type = ArenaType.typeOf(arena);
return type == null ? "unknown" : type.name;
}
}

122
arena/game/Generator.java Normal file
View file

@ -0,0 +1,122 @@
package net.minestom.arena.game;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.*;
public sealed interface Generator<T, G extends GenerationContext> permits GeneratorImpl {
@Contract("_ -> new")
static <T, G extends GenerationContext> @NotNull Builder<T, G> builder(@NotNull Function<G, T> function) {
return new GeneratorImpl.Builder<>(function);
}
static <T, G extends GenerationContext> @NotNull List<T> generateAll(
@NotNull List<Generator<? extends T, G>> generators, int amount, Supplier<G> contextSupplier) {
final Map<Generator<? extends T, G>, G> contextMap = new HashMap<>();
final List<T> result = new ArrayList<>();
for (Generator<? extends T, G> generator : generators)
contextMap.put(generator, contextSupplier.get());
while (result.size() < amount) {
final Generator<? extends T, G> generator = generators.get(ThreadLocalRandom.current().nextInt(generators.size()));
final G context = contextMap.get(generator);
final Optional<? extends T> generated = generator.generate(context);
if (generated.isPresent()) {
result.add(generated.get());
context.incrementGenerated();
}
}
return result;
}
@NotNull Optional<T> generate(@NotNull G context);
sealed interface Builder<T, G extends GenerationContext> permits GeneratorImpl.Builder {
@Contract("_ -> this")
@NotNull Builder<T, G> chance(double chance);
@Contract("_ -> this")
@NotNull Builder<T, G> condition(@NotNull Condition<G> condition);
@Contract("_ -> this")
@NotNull Builder<T, G> controller(@NotNull Controller<G> controller);
@Contract("_ -> this")
@NotNull Builder<T, G> preference(@NotNull Preference<G> preference);
@Contract("_ -> this")
default @NotNull Builder<T, G> preference(@NotNull ToDoubleFunction<G> isPreferred) {
return preference(isPreferred, 1);
}
@Contract("_, _ -> this")
default @NotNull Builder<T, G> preference(@NotNull ToDoubleFunction<G> isPreferred, double weight) {
return preference(new Preference<>() {
@Override
public double isPreferred(@NotNull G context) {
return isPreferred.applyAsDouble(context);
}
@Override
public double weight() {
return weight;
}
});
}
@NotNull Generator<T, G> build();
}
@FunctionalInterface
interface Condition<G extends GenerationContext> {
boolean isMet(@NotNull G context);
}
@FunctionalInterface
interface Controller<G extends GenerationContext> {
@NotNull Control control(@NotNull G context);
enum Control {
ALLOW,
DISALLOW,
OK
}
static <G extends GenerationContext> @NotNull Controller<G> minCount(int count) {
return context -> context.generated() <= count
? Control.ALLOW
: Control.OK;
}
static <G extends GenerationContext> @NotNull Controller<G> minCount(ToIntFunction<G> count) {
return context -> context.generated() <= count.applyAsInt(context)
? Control.ALLOW
: Control.OK;
}
static <G extends GenerationContext> @NotNull Controller<G> maxCount(int count) {
return context -> context.generated() >= count
? Control.DISALLOW
: Control.OK;
}
static <G extends GenerationContext> @NotNull Controller<G> maxCount(ToIntFunction<G> count) {
return context -> context.generated() >= count.applyAsInt(context)
? Control.DISALLOW
: Control.OK;
}
}
interface Preference<G extends GenerationContext> {
double isPreferred(@NotNull G context);
double weight();
}
}

View file

@ -0,0 +1,101 @@
package net.minestom.arena.game;
import net.minestom.server.utils.MathUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Function;
import java.util.function.Predicate;
record GeneratorImpl<T, G extends GenerationContext>(Function<G, T> function,
Predicate<G> shouldGenerate) implements Generator<T, G> {
@Override
public @NotNull Optional<T> generate(@NotNull G context) {
return shouldGenerate.test(context)
? Optional.of(function.apply(context))
: Optional.empty();
}
static final class Builder<T, G extends GenerationContext> implements Generator.Builder<T, G> {
final Function<G, T> function;
final List<Condition<G>> conditions = new ArrayList<>();
final List<Controller<G>> controllers = new ArrayList<>();
final List<Preference<G>> preferences = new ArrayList<>();
double chance = 1;
Builder(@NotNull Function<G, T> function) {
this.function = function;
}
@Override
public @NotNull Generator.Builder<T, G> chance(double chance) {
this.chance = chance;
return this;
}
@Override
public @NotNull Generator.Builder<T, G> condition(@NotNull Condition<G> condition) {
conditions.add(condition);
return this;
}
@Override
public @NotNull Generator.Builder<T, G> controller(@NotNull Controller<G> controller) {
controllers.add(controller);
return this;
}
@Override
public @NotNull Generator.Builder<T, G> preference(@NotNull Preference<G> preference) {
preferences.add(preference);
return this;
}
@Override
public @NotNull Generator<T, G> build() {
return new GeneratorImpl<>(function, context -> {
final Random random = ThreadLocalRandom.current();
// Chance
if (random.nextDouble() > chance)
return false;
// Conditions
for (Condition<G> condition : conditions) {
if (!condition.isMet(context))
return false;
}
// Controllers
for (Controller<G> controller : controllers) {
switch (controller.control(context)) {
case ALLOW -> {
return true;
}
case DISALLOW -> {
return false;
}
}
}
// Preferences
double score = 0;
for (Preference<G> preference : preferences) {
score += MathUtils.clamp(preference.isPreferred(context), 0, 1) * preference.weight();
}
final double chance = score / preferences.stream()
.mapToDouble(Preference::weight)
.sum();
// NaN if no preferences
return random.nextDouble() <= chance || Double.isNaN(chance);
});
}
}
}

View file

@ -0,0 +1,53 @@
package net.minestom.arena.game;
import net.minestom.arena.lobby.LobbySidebarDisplay;
import net.minestom.arena.feature.Feature;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent;
import net.minestom.server.instance.Instance;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public interface SingleInstanceArena extends Arena {
@NotNull Instance instance();
@NotNull Pos spawnPosition(@NotNull Player player);
@NotNull List<Feature> features();
@Override
default @NotNull CompletableFuture<Void> init() {
Instance instance = instance();
// Register this arena
MinecraftServer.getInstanceManager().registerInstance(instance);
instance.eventNode().addListener(RemoveEntityFromInstanceEvent.class, event -> {
// We don't care about entities, only players.
if (!(event.getEntity() instanceof Player)) return;
// Ensure there is only this player in the instance
if (instance.getPlayers().size() > 1) return;
// All players have left. We can remove this instance once the player is removed.
MinecraftServer.getSchedulerManager().scheduleNextTick(() -> {
MinecraftServer.getInstanceManager().unregisterInstance(instance);
stop();
});
group().setDisplay(new LobbySidebarDisplay(group()));
});
for (Feature feature : features()) {
feature.hook(instance.eventNode());
}
CompletableFuture<?>[] futures =
group().members().stream()
.map(player -> player.setInstance(instance, spawnPosition(player)))
.toArray(CompletableFuture<?>[]::new);
return CompletableFuture.allOf(futures).thenRun(this::start);
}
}

View file

@ -0,0 +1,28 @@
package net.minestom.arena.game.mob;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.minestom.arena.utils.ItemUtils;
import net.minestom.server.entity.Player;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
record ArenaClass(String name, String description, String icon, TextColor color, Material material, Kit kit, int cost) {
public void apply(Player player) {
kit.apply(player);
}
public ItemStack itemStack() {
return ItemUtils.stripItalics(ItemStack.builder(material)
.displayName(Component.text(icon + " " + name, color))
.lore(
Component.text(description, NamedTextColor.GRAY),
Component.empty(),
Component.text("Switch to this class for " + cost + " coins", NamedTextColor.GOLD)
)
.meta(ItemUtils::hideFlags)
.build()
);
}
}

View file

@ -0,0 +1,17 @@
package net.minestom.arena.game.mob;
import net.minestom.server.entity.EntityType;
import org.jetbrains.annotations.NotNull;
class ArenaMinion extends ArenaMob {
private final ArenaMob owner;
public ArenaMinion(@NotNull EntityType entityType, @NotNull ArenaMob owner) {
super(entityType, owner.context);
this.owner = owner;
}
public ArenaMob owner() {
return owner;
}
}

View file

@ -0,0 +1,41 @@
package net.minestom.arena.game.mob;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.minestom.arena.utils.ItemUtils;
import net.minestom.server.entity.Player;
import net.minestom.server.item.Enchantment;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.IntFunction;
record ArenaUpgrade(String name, String description, TextColor color, Material material,
@Nullable BiConsumer<Player, Integer> apply, @Nullable Consumer<Player> remove,
@NotNull IntFunction<String> effect, int cost, float costMultiplier, int maxLevel) {
public ItemStack itemStack(int level) {
return ItemUtils.stripItalics(ItemStack.builder(material)
.displayName(Component.text(name, color))
.lore(
Component.text(description, NamedTextColor.GRAY),
Component.empty(),
Component.text("Buy this team upgrade for " + cost(level) + " coins", NamedTextColor.GOLD),
Component.text(effect.apply(level), NamedTextColor.YELLOW)
)
.meta(ItemUtils::hideFlags)
.meta(builder -> {
if (level >= maxLevel) builder.enchantment(Enchantment.PROTECTION, (short) 1);
})
.build()
);
}
public int cost(int level) {
return (int) (cost * Math.pow(costMultiplier, level));
}
}

View file

@ -0,0 +1,122 @@
package net.minestom.arena.game.mob;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.*;
import net.minestom.server.entity.ai.GoalSelector;
import net.minestom.server.entity.ai.target.ClosestEntityTarget;
import net.minestom.server.utils.time.Cooldown;
import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.List;
import java.util.function.Function;
final class BlazeMob extends ArenaMob {
private static final int MAX_HEIGHT = 5;
public BlazeMob(MobGenerationContext context) {
super(EntityType.BLAZE, context);
// Custom attack goal, blaze is stationary and shouldn't follow their targets
BlazeAttackGoal rangedAttackGoal = new BlazeAttackGoal(
this, Duration.of(10, TimeUnit.SERVER_TICK),
16, 1, 0.5, entity -> {
EntityProjectile projectile = new FireballProjectile(entity);
projectile.scheduleRemove(Duration.of(30, TimeUnit.SERVER_TICK));
return projectile;
});
addAIGroup(
List.of(rangedAttackGoal),
List.of(new ClosestEntityTarget(this, 32, entity -> entity instanceof Player))
);
}
@Override
public @NotNull Vec getVelocity() {
return super.getVelocity()
.withY(y -> getPosition().y() > MobArena.HEIGHT + MAX_HEIGHT // If above max height
? y / 5 // Stop flying up
: Math.abs(y)); // Fly up
}
private static final class FireballProjectile extends EntityProjectile {
public FireballProjectile(@NotNull Entity shooter) {
super(shooter, EntityType.SMALL_FIREBALL);
setNoGravity(true);
}
@Override
public void tick(long time) {
super.tick(time);
if (isOnGround()) remove();
}
}
private static final class BlazeAttackGoal extends GoalSelector {
private long lastShot;
private final Duration delay;
private final int attackRangeSquared;
private final double power;
private final double spread;
private final Function<Entity, EntityProjectile> projectileGenerator;
private boolean stop;
private Entity cachedTarget;
public BlazeAttackGoal(@NotNull EntityCreature entityCreature, Duration delay, int attackRange, double power, double spread, Function<Entity, EntityProjectile> projectileGenerator) {
super(entityCreature);
this.delay = delay;
this.attackRangeSquared = attackRange * attackRange;
this.power = power;
this.spread = spread;
this.projectileGenerator = projectileGenerator;
}
@Override
public boolean shouldStart() {
this.cachedTarget = findTarget();
return this.cachedTarget != null;
}
@Override
public void start() {}
@Override
public void tick(long time) {
Entity target;
if (this.cachedTarget != null) {
target = this.cachedTarget;
this.cachedTarget = null;
} else {
target = findTarget();
}
if (target == null) {
this.stop = true;
return;
}
double distanceSquared = this.entityCreature.getDistanceSquared(target);
if (distanceSquared <= this.attackRangeSquared) {
if (!Cooldown.hasCooldown(time, this.lastShot, this.delay)) {
if (entityCreature.hasLineOfSight(target)) {
final var to = target.getPosition().add(0D, target.getEyeHeight(), 0D);
EntityProjectile projectile = projectileGenerator.apply(entityCreature);
projectile.setInstance(entityCreature.getInstance(), entityCreature.getPosition().add(0D, entityCreature.getEyeHeight(), 0D));
projectile.shoot(to, power, spread);
this.lastShot = time;
}
}
}
entityCreature.lookAt(target);
}
@Override
public boolean shouldEnd() {
return stop;
}
@Override
public void end() {}
}
}

View file

@ -0,0 +1,85 @@
package net.minestom.arena.game.mob;
import net.minestom.server.attribute.Attribute;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.ai.GoalSelector;
import net.minestom.server.entity.ai.goal.MeleeAttackGoal;
import net.minestom.server.entity.ai.target.ClosestEntityTarget;
import net.minestom.server.utils.time.Cooldown;
import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.List;
final class EndermanMob extends ArenaMob {
public EndermanMob(MobGenerationContext context) {
super(EntityType.ENDERMAN, context);
addAIGroup(
List.of(
new TeleportGoal(this, Duration.ofSeconds(10), 8),
new MeleeAttackGoal(this, 1.2, 20, TimeUnit.SERVER_TICK)
),
List.of(new ClosestEntityTarget(this, 32, entity -> entity instanceof Player))
);
getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(getAttributeValue(Attribute.MOVEMENT_SPEED) * 2);
}
private static final class TeleportGoal extends GoalSelector {
private final Duration cooldown;
private final int distanceSquared;
private long lastTeleport;
private Entity target;
public TeleportGoal(@NotNull EntityCreature entityCreature, @NotNull Duration cooldown, int distance) {
super(entityCreature);
this.cooldown = cooldown;
distanceSquared = distance * distance;
}
@Override
public boolean shouldStart() {
Entity target = entityCreature.getTarget();
if (target == null) target = findTarget();
if (target == null) return false;
if (Cooldown.hasCooldown(System.currentTimeMillis(), lastTeleport, cooldown)) return false;
final boolean result = target.getPosition().distanceSquared(entityCreature.getPosition()) >= distanceSquared;
if (result) this.target = target;
return result;
}
@Override
public void start() {
if (target == null) return;
entityCreature.setTarget(target);
}
@Override
public void tick(long time) {
if (!Cooldown.hasCooldown(time, lastTeleport, cooldown)) {
final Pos targetPos = entityCreature.getTarget() != null
? entityCreature.getTarget().getPosition() : null;
lastTeleport = time;
if (targetPos != null)
entityCreature.teleport(targetPos.sub(targetPos.direction()));
}
}
@Override
public boolean shouldEnd() {
final Entity target = entityCreature.getTarget();
return target == null || target.isRemoved() ||
Cooldown.hasCooldown(System.currentTimeMillis(), lastTeleport, cooldown) ||
target.getPosition().distanceSquared(entityCreature.getPosition()) < distanceSquared;
}
@Override
public void end() {}
}
}

View file

@ -0,0 +1,111 @@
package net.minestom.arena.game.mob;
import net.minestom.server.attribute.Attribute;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.ai.GoalSelector;
import net.minestom.server.entity.ai.goal.MeleeAttackGoal;
import net.minestom.server.entity.ai.target.ClosestEntityTarget;
import net.minestom.server.entity.metadata.monster.raider.EvokerMeta;
import net.minestom.server.entity.metadata.monster.raider.SpellcasterIllagerMeta;
import net.minestom.server.particle.Particle;
import net.minestom.server.particle.ParticleCreator;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.time.Cooldown;
import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Consumer;
final class EvokerMob extends ArenaMob {
public EvokerMob(MobGenerationContext context) {
super(EntityType.EVOKER, context);
addAIGroup(
List.of(new ActionGoal(this, Duration.ofSeconds(10), target -> {
lookAt(target);
((EvokerMeta) getEntityMeta()).setSpell(SpellcasterIllagerMeta.Spell.SUMMON_VEX);
scheduler().scheduleTask(() -> {
final Random random = ThreadLocalRandom.current();
for (int i = 0; i < random.nextInt(1, 3); i++) {
ArenaMob silverfish = new ArenaMinion(EntityType.SILVERFISH, this);
silverfish.addAIGroup(
List.of(new MeleeAttackGoal(silverfish, 1.2, 20, TimeUnit.SERVER_TICK)),
List.of(new ClosestEntityTarget(silverfish, 32, entity -> entity instanceof Player))
);
silverfish.getAttribute(Attribute.MAX_HEALTH).setBaseValue(silverfish.getMaxHealth() / 4);
silverfish.heal();
final Pos pos = position.add(
random.nextFloat(-2, 2), 0,
random.nextFloat(-2, 2)
);
silverfish.setInstance(instance, pos);
instance.sendGroupedPacket(ParticleCreator.createParticlePacket(
Particle.POOF, true, pos.x(), pos.y(), pos.z(),
0.2f, 0.2f, 0.2f, 0.1f, 10, null
));
}
((EvokerMeta) getEntityMeta()).setSpell(SpellcasterIllagerMeta.Spell.NONE);
}, TaskSchedule.seconds(2), TaskSchedule.stop());
})),
List.of(new ClosestEntityTarget(this, 32, entity -> entity instanceof Player))
);
}
private static final class ActionGoal extends GoalSelector {
private final Duration cooldown;
private final Consumer<Entity> consumer;
private long lastSummon;
private Entity target;
public ActionGoal(@NotNull EntityCreature entityCreature, @NotNull Duration cooldown, Consumer<Entity> consumer) {
super(entityCreature);
this.cooldown = cooldown;
this.consumer = consumer;
}
@Override
public boolean shouldStart() {
Entity target = entityCreature.getTarget();
if (target == null || target.getInstance() != entityCreature.getInstance()) target = findTarget();
if (target == null) return false;
if (Cooldown.hasCooldown(System.currentTimeMillis(), lastSummon, cooldown)) return false;
this.target = target;
return true;
}
@Override
public void start() {
if (target == null) return;
entityCreature.setTarget(target);
}
@Override
public void tick(long time) {
if (!Cooldown.hasCooldown(time, lastSummon, cooldown) && entityCreature.getTarget() != null) {
lastSummon = time;
consumer.accept(entityCreature.getTarget());
}
}
@Override
public boolean shouldEnd() {
final Entity target = entityCreature.getTarget();
return target == null || target.isRemoved() ||
Cooldown.hasCooldown(System.currentTimeMillis(), lastSummon, cooldown);
}
@Override
public void end() {}
}
}

41
arena/game/mob/Kit.java Normal file
View file

@ -0,0 +1,41 @@
package net.minestom.arena.game.mob;
import net.minestom.server.entity.EquipmentSlot;
import net.minestom.server.entity.Player;
import net.minestom.server.inventory.PlayerInventory;
import net.minestom.server.inventory.TransactionOption;
import net.minestom.server.item.ItemStack;
import net.minestom.server.tag.Tag;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
record Kit(@NotNull List<ItemStack> inventory, Map<EquipmentSlot, ItemStack> equipments) {
public static final Tag<Boolean> KIT_ITEM_TAG = Tag.Boolean("kit_item").defaultValue(false);
Kit {
inventory = List.copyOf(inventory);
equipments = Map.copyOf(equipments);
}
public void apply(Player player) {
final PlayerInventory playerInventory = player.getInventory();
// Clear previous kit items
for (ItemStack item : playerInventory.getItemStacks()) {
if (item.getTag(KIT_ITEM_TAG))
player.getInventory().takeItemStack(item, TransactionOption.ALL);
}
// Equipment
for (EquipmentSlot slot : EquipmentSlot.armors()) {
final ItemStack item = equipments.get(slot);
if (item != null) player.setEquipment(slot, item.withTag(KIT_ITEM_TAG, true));
else player.setEquipment(slot, ItemStack.AIR);
}
// Misc
for (ItemStack item : inventory) {
playerInventory.addItemStack(item.withTag(KIT_ITEM_TAG, true));
}
}
}

View file

@ -0,0 +1,76 @@
package net.minestom.arena.game.mob;
import de.articdive.jnoise.core.api.pipeline.NoiseSource;
import de.articdive.jnoise.generators.noisegen.opensimplex.FastSimplexNoiseGenerator;
import de.articdive.jnoise.generators.noisegen.perlin.PerlinNoiseGenerator;
import de.articdive.jnoise.modules.octavation.OctavationModule;
import de.articdive.jnoise.pipeline.JNoise;
import net.minestom.arena.Metrics;
import net.minestom.arena.utils.FullbrightDimension;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.block.Block;
import net.minestom.server.utils.MathUtils;
import java.util.UUID;
final class MobArenaInstance extends InstanceContainer {
private final JNoise noise = JNoise.newBuilder()
.fastSimplex(FastSimplexNoiseGenerator.newBuilder().build())
.scale(0.03)//0.0025
.octavation(OctavationModule.newBuilder().setOctaves(6).setNoiseSource(PerlinNoiseGenerator.newBuilder().build()).build())
.build();
MobArenaInstance() {
super(UUID.randomUUID(), FullbrightDimension.INSTANCE);
getWorldBorder().setDiameter(100);
setGenerator(unit -> {
final Point start = unit.absoluteStart();
for (int x = 0; x < unit.size().x(); x++) {
for (int z = 0; z < unit.size().z(); z++) {
Point bottom = start.add(x, 0, z);
// Ensure flat terrain in the fighting area
final double modifier = MathUtils.clamp((bottom.distance(Pos.ZERO.withY(bottom.y())) - 75) / 50, 0, 1);
double y = noise.evaluateNoise(bottom.x(), bottom.z()) * modifier;
y = (y > 0 ? y * 4 : y) * 8 + MobArena.HEIGHT;
unit.modifier().fill(bottom, bottom.add(1, 0, 1).withY(y), Block.SAND);
}
}
});
int x = MobArena.SPAWN_RADIUS;
int y = 0;
int xChange = 1 - (MobArena.SPAWN_RADIUS << 1);
int yChange = 0;
int radiusError = 0;
while (x >= y) {
for (int i = -x; i <= x; i++) {
setBlock(i, 15, y, Block.RED_SAND);
setBlock(i, 15, -y, Block.RED_SAND);
}
for (int i = -y; i <= y; i++) {
setBlock(i, 15, x, Block.RED_SAND);
setBlock(i, 15, -x, Block.RED_SAND);
}
y++;
radiusError += yChange;
yChange += 2;
if (((radiusError << 1) + xChange) > 0) {
x--;
radiusError += xChange;
xChange += 2;
}
}
}
@Override
protected void setRegistered(boolean registered) {
super.setRegistered(registered);
if (!registered) {
Metrics.ENTITIES.dec(getEntities().size());
}
}
}

View file

@ -0,0 +1,46 @@
package net.minestom.arena.game.mob;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.minestom.arena.Icons;
import net.minestom.arena.Messenger;
import net.minestom.arena.group.Group;
import net.minestom.arena.group.displays.GroupSidebarDisplay;
import net.minestom.server.entity.Player;
import net.minestom.server.scoreboard.Sidebar;
final class MobArenaSidebarDisplay extends GroupSidebarDisplay {
private final MobArena arena;
public MobArenaSidebarDisplay(MobArena arena) {
super(arena.group());
this.arena = arena;
}
// TODO: Probably a better way to make the whole component gray
@Override
protected Sidebar.ScoreboardLine createPlayerLine(Player player, Group group) {
final ArenaClass arenaClass = arena.playerClass(player);
final boolean dead = !arena.instance().getPlayers().contains(player);
Component icon = Component.text(arenaClass.icon(), arenaClass.color());
if (!arena.stageInProgress()) {
icon = arena.hasContinued(player)
? Component.text(Icons.CHECKMARK, dead ? NamedTextColor.GRAY : NamedTextColor.GREEN)
: Component.text(Icons.CROSS, dead ? NamedTextColor.GRAY : NamedTextColor.RED);
}
Component line = icon.append(Component.text(" "))
.append(player.getName().color(dead ? NamedTextColor.GRAY : Messenger.ORANGE_COLOR));
// Strikethrough if player is dead
if (dead) line = line.decorate(TextDecoration.STRIKETHROUGH);
return new Sidebar.ScoreboardLine(
player.getUuid().toString(),
line,
0
);
}
}

View file

@ -0,0 +1,101 @@
package net.minestom.arena.game.mob;
import net.minestom.arena.Items;
import net.minestom.arena.game.Arena;
import net.minestom.arena.game.ArenaManager;
import net.minestom.arena.utils.CommandUtils;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.ArgumentLiteral;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.command.builder.arguments.number.ArgumentNumber;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.damage.DamageType;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.utils.MathUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class MobTestCommand extends Command {
public MobTestCommand() {
super("test");
setCondition((sender, commandString) -> CommandUtils.arenaOnly(sender, commandString) &&
sender instanceof Player player && player.getPermissionLevel() == 4);
final ArgumentLiteral coins = ArgumentType.Literal("coins");
final ArgumentLiteral stage = ArgumentType.Literal("stage");
final ArgumentLiteral clear = ArgumentType.Literal("clear");
final ArgumentLiteral clazz = ArgumentType.Literal("class");
final ArgumentLiteral spawn = ArgumentType.Literal("spawn");
final ArgumentLiteral immortal = ArgumentType.Literal("immortal");
final ArgumentLiteral strong = ArgumentType.Literal("strong");
final ArgumentLiteral damageme = ArgumentType.Literal("damageme");
final ArgumentNumber<Integer> coinsAmount = ArgumentType.Integer("amount")
.between(0, 1000);
final ArgumentNumber<Integer> classId = ArgumentType.Integer("id")
.between(0, MobArena.CLASSES.size() - 1);
final ArgumentNumber<Integer> mobType = ArgumentType.Integer("type")
.between(0, MobArena.MOB_GENERATORS.size() - 1);
addSyntax((sender, context) -> ((Player) sender).getInventory().addItemStack(Items.COIN
.withAmount(context.get(coinsAmount))), coins, coinsAmount);
addSyntax((sender, context) -> ((Player) sender).getInventory().addItemStack(Items.COIN
.withAmount(10)), coins);
addSyntax((sender, context) -> arena(sender)
.ifPresent(MobArena::nextStage), stage);
addSyntax((sender, context) -> arena(sender).ifPresent(arena -> {
for (Entity entity : arena.instance().getEntities()) {
if (entity instanceof ArenaMob arenaMob)
arenaMob.kill();
}
}), clear);
addSyntax((sender, context) -> arena(sender).ifPresent(arena ->
arena.setPlayerClass(
(Player) sender,
MobArena.CLASSES.get(MathUtils.clamp(
context.get(classId),
0, MobArena.CLASSES.size() - 1
))
)
), clazz, classId);
addSyntax((sender, context) -> arena(sender).ifPresent(arena ->
MobArena.MOB_GENERATORS.get(MathUtils.clamp(
context.get(mobType),
0, MobArena.MOB_GENERATORS.size() - 1
)).generate(new MobGenerationContext(arena)).ifPresent(entity ->
entity.setInstance(arena.instance(), ((Player) sender).getPosition()))), spawn, mobType);
addSyntax((sender, context) -> {
final Player player = (Player) sender;
player.setInvulnerable(!player.isInvulnerable());
}, immortal);
addSyntax((sender, context) ->
((Player) sender).getInventory().addItemStack(ItemStack.builder(Material.COOKED_CHICKEN)
.set(MobArena.MELEE_TAG, 10000).build()
), strong);
addSyntax((sender, context) -> ((Player) sender).damage(DamageType.VOID, 10), damageme);
}
private static @NotNull Optional<MobArena> arena(CommandSender sender) {
if (!(sender instanceof Player player)) return Optional.empty();
for (Arena arena : ArenaManager.list()) {
if (!(arena instanceof MobArena mobArena)) continue;
if (arena.group().members().contains(player))
return Optional.of(mobArena);
}
return Optional.empty();
}
}

View file

@ -0,0 +1,177 @@
package net.minestom.arena.game.mob;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.arena.Items;
import net.minestom.arena.Messenger;
import net.minestom.arena.utils.ItemUtils;
import net.minestom.server.entity.Player;
import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.InventoryType;
import net.minestom.server.inventory.TransactionOption;
import net.minestom.server.item.Enchantment;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.sound.SoundEvent;
final class NextStageInventory extends Inventory {
private static final ItemStack HEADER = ItemUtils.stripItalics(ItemStack.builder(Material.PAPER)
.displayName(Component.text("Next Stage", NamedTextColor.GOLD))
.lore(Component.text("Buy a different class, team upgrades or just continue to the next stage", NamedTextColor.GRAY))
.build());
private static final ItemStack CLASS_SELECTION = ItemUtils.stripItalics(ItemStack.builder(Material.SHIELD)
.displayName(Component.text("Class Selection", NamedTextColor.GREEN))
.lore(Component.text("Buy a different class", NamedTextColor.GRAY))
.build());
private static final ItemStack TEAM_UPGRADES = ItemUtils.stripItalics(ItemStack.builder(Material.ANVIL)
.displayName(Component.text("Team Upgrades", NamedTextColor.LIGHT_PURPLE))
.lore(Component.text("Buy upgrades for the whole team", NamedTextColor.GRAY))
.build());
private final Player player;
private final MobArena arena;
NextStageInventory(Player player, MobArena arena) {
super(InventoryType.CHEST_4_ROW, Component.text("Next Stage"));
this.player = player;
this.arena = arena;
setItemStack(4, HEADER);
setItemStack(12, CLASS_SELECTION);
setItemStack(14, TEAM_UPGRADES);
setItemStack(30, Items.CLOSE);
setItemStack(32, Items.CONTINUE);
addInventoryCondition((p, s, c, result) -> result.setCancel(true));
addInventoryCondition((p, slot, c, r) -> {
switch (slot) {
case 12 -> player.openInventory(new ClassSelectionInventory(this));
case 14 -> player.openInventory(new TeamUpgradeInventory(this));
case 30 -> player.closeInventory();
case 32 -> {
player.closeInventory();
arena.continueToNextStage(player);
}
}
});
}
private final class ClassSelectionInventory extends Inventory {
ClassSelectionInventory(Inventory parent) {
super(InventoryType.CHEST_4_ROW, Component.text("Class Selection"));
setItemStack(4, HEADER);
draw();
setItemStack(31, Items.BACK);
addInventoryCondition((p, s, c, result) -> result.setCancel(true));
addInventoryCondition((p, slot, c, r) -> {
if (slot == 31) player.openInventory(parent);
else {
final int length = MobArena.CLASSES.size();
for (int i = 0; i < length; i++) {
ArenaClass arenaClass = MobArena.CLASSES.get(i);
if (slot == 13 - length / 2 + i) {
switchClass(arenaClass);
return;
}
}
}
});
}
private void draw() {
final int length = MobArena.CLASSES.size();
for (int i = 0; i < length; i++) {
ArenaClass arenaClass = MobArena.CLASSES.get(i);
setItemStack(13 - length / 2 + i, arenaClass.itemStack()
.withMeta(builder -> {
if (arena.playerClass(player).equals(arenaClass))
builder.enchantment(Enchantment.PROTECTION, (short) 1);
}));
}
}
private void switchClass(ArenaClass arenaClass) {
if (arena.playerClass(player).equals(arenaClass)) {
Messenger.warn(player, "You can't switch to your selected class");
return;
}
if (player.getInventory().takeItemStack(Items.COIN.withAmount(arenaClass.cost()), TransactionOption.ALL_OR_NOTHING)) {
Messenger.info(arena.group(), player.getUsername() + " switched their class to " + arenaClass.name());
arena.setPlayerClass(player, arenaClass);
draw();
player.playSound(Sound.sound(SoundEvent.ENTITY_VILLAGER_YES, Sound.Source.NEUTRAL, 1, 1), Sound.Emitter.self());
} else {
Messenger.warn(player, "You can't afford that");
player.playSound(Sound.sound(SoundEvent.ENTITY_VILLAGER_NO, Sound.Source.NEUTRAL, 1, 1), Sound.Emitter.self());
}
}
}
private final class TeamUpgradeInventory extends Inventory {
TeamUpgradeInventory(Inventory parent) {
super(InventoryType.CHEST_4_ROW, Component.text("Team Upgrades"));
setItemStack(4, HEADER);
draw();
setItemStack(31, Items.BACK);
addInventoryCondition((p, s, c, result) -> result.setCancel(true));
addInventoryCondition((p, slot, c, r) -> {
if (slot == 31) player.openInventory(parent);
else {
final int length = MobArena.UPGRADES.size();
for (int i = 0; i < length; i++) {
ArenaUpgrade upgrade = MobArena.UPGRADES.get(i);
if (slot == 13 - length / 2 + i) {
buyUpgrade(upgrade);
return;
}
}
}
});
}
private void draw() {
final int length = MobArena.UPGRADES.size();
for (int i = 0; i < length; i++) {
final ArenaUpgrade upgrade = MobArena.UPGRADES.get(i);
final int level = arena.getUpgrade(upgrade);
setItemStack(13 - length / 2 + i, upgrade.itemStack(level));
}
}
private void buyUpgrade(ArenaUpgrade upgrade) {
final int level = arena.getUpgrade(upgrade);
if (level >= upgrade.maxLevel()) {
Messenger.warn(player, "Maximum upgrade level has been reached");
player.playSound(Sound.sound(SoundEvent.ENTITY_VILLAGER_NO, Sound.Source.NEUTRAL, 1, 1), Sound.Emitter.self());
return;
}
if (player.getInventory().takeItemStack(Items.COIN.withAmount(upgrade.cost(level)), TransactionOption.ALL_OR_NOTHING)) {
Messenger.info(arena.group(), player.getUsername() + " bought the " + upgrade.name() + " upgrade");
arena.addUpgrade(upgrade);
draw();
player.playSound(Sound.sound(SoundEvent.ENTITY_VILLAGER_YES, Sound.Source.NEUTRAL, 1, 1), Sound.Emitter.self());
} else {
Messenger.warn(player, "You can't afford that");
player.playSound(Sound.sound(SoundEvent.ENTITY_VILLAGER_NO, Sound.Source.NEUTRAL, 1, 1), Sound.Emitter.self());
}
}
}
}

View file

@ -0,0 +1,10 @@
package net.minestom.arena.game.mob;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
final class NextStageNPC extends Entity {
public NextStageNPC() {
super(EntityType.VILLAGER);
}
}

View file

@ -0,0 +1,65 @@
package net.minestom.arena.game.mob;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityProjectile;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.ai.goal.RangedAttackGoal;
import net.minestom.server.entity.ai.target.ClosestEntityTarget;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.List;
final class SkeletonMob extends ArenaMob {
public SkeletonMob(MobGenerationContext context) {
super(EntityType.SKELETON, context);
setItemInMainHand(ItemStack.of(Material.BOW));
RangedAttackGoal rangedAttackGoal = new RangedAttackGoal(
this, Duration.of(40, TimeUnit.SERVER_TICK),
16, 8, true, 1, 0.1);
rangedAttackGoal.setProjectileGenerator(entity -> {
HomingArrow projectile = new HomingArrow(entity, EntityType.PLAYER);
projectile.scheduleRemove(Duration.of(100, TimeUnit.SERVER_TICK));
return projectile;
});
addAIGroup(
List.of(rangedAttackGoal),
List.of(new ClosestEntityTarget(this, 32, entity -> entity instanceof Player))
);
}
private static final class HomingArrow extends EntityProjectile {
private final EntityType target;
public HomingArrow(@Nullable Entity shooter, EntityType target) {
super(shooter, EntityType.ARROW);
this.target = target;
}
@Override
public void tick(long time) {
super.tick(time);
if (instance == null) return;
if (isOnGround()) return;
for (Entity entity : instance.getNearbyEntities(position, 5.0)) {
if (entity.getEntityType() != target) continue;
final Vec target = position.withLookAt(entity.getPosition()).direction();
final Vec newVelocity = velocity.add(target);
setVelocity(newVelocity);
return;
}
}
}
}

View file

@ -0,0 +1,110 @@
package net.minestom.arena.game.mob;
import net.kyori.adventure.sound.Sound;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.*;
import net.minestom.server.entity.ai.goal.RangedAttackGoal;
import net.minestom.server.entity.ai.target.ClosestEntityTarget;
import net.minestom.server.entity.metadata.other.ArmorStandMeta;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.network.packet.server.LazyPacket;
import net.minestom.server.network.packet.server.play.EntityEquipmentPacket;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
final class SpiderMob extends ArenaMob {
public SpiderMob(MobGenerationContext context) {
super(EntityType.SPIDER, context);
RangedAttackGoal attackGoal = new RangedAttackGoal(
this, Duration.of(10, TimeUnit.SECOND),
16, 12, false, 1.3, 0);
attackGoal.setProjectileGenerator(WebProjectile::new);
addAIGroup(
List.of(attackGoal),
List.of(new ClosestEntityTarget(this, 32, entity -> entity instanceof Player))
);
}
private static class WebProjectile extends EntityProjectile {
public WebProjectile(@NotNull Entity shooter) {
super(shooter, EntityType.ARMOR_STAND);
ArmorStandMeta meta = (ArmorStandMeta) getEntityMeta();
meta.setHasNoBasePlate(true);
meta.setHasArms(true);
meta.setSmall(true);
meta.setRightArmRotation(new Vec(135, 90, 0));
meta.setInvisible(true);
getViewersAsAudience().playSound(Sound.sound(SoundEvent.ENTITY_SPIDER_STEP, Sound.Source.HOSTILE, 1, 1), shooter);
eventNode().addListener(ProjectileCollideWithEntityEvent.class, event -> {
final Entity target = event.getTarget();
if (!(target instanceof Player)) event.setCancelled(true);
else collide(event.getEntity(), target.getPosition());
});
eventNode().addListener(ProjectileCollideWithBlockEvent.class, event -> collide(event.getEntity(), event.getCollisionPosition()));
}
private @NotNull EntityEquipmentPacket getEquipmentsPacket() {
return new EntityEquipmentPacket(this.getEntityId(), Map.of(
EquipmentSlot.MAIN_HAND, ItemStack.of(Material.COBWEB),
EquipmentSlot.OFF_HAND, ItemStack.AIR,
EquipmentSlot.BOOTS, ItemStack.AIR,
EquipmentSlot.LEGGINGS, ItemStack.AIR,
EquipmentSlot.CHESTPLATE, ItemStack.AIR,
EquipmentSlot.HELMET, ItemStack.AIR));
}
@Override
public void updateNewViewer(@NotNull Player player) {
super.updateNewViewer(player);
player.sendPacket(new LazyPacket(this::getEquipmentsPacket));
}
private static void collide(Entity projectile, Pos pos) {
final Instance instance = projectile.getInstance();
if (instance == null) return;
final Random random = ThreadLocalRandom.current();
final List<Pos> cobwebs = new ArrayList<>();
for (int i = 0; i < 8; i++) {
Pos spawnAt = pos.add(
random.nextInt(-1, 1),
random.nextInt(0, 2),
random.nextInt(-1, 1)
);
if (instance.getBlock(spawnAt).isAir()) {
instance.setBlock(spawnAt, Block.COBWEB);
cobwebs.add(spawnAt);
}
}
projectile.remove();
instance.scheduler().buildTask(() -> {
if (!instance.isRegistered()) return;
for (Pos cobweb : cobwebs) {
instance.setBlock(cobweb, Block.AIR);
}
}).delay(5, TimeUnit.SECOND).schedule();
}
}
}

29
arena/group/Group.java Normal file
View file

@ -0,0 +1,29 @@
package net.minestom.arena.group;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience;
import net.minestom.arena.group.displays.GroupDisplay;
import net.minestom.server.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Set;
public sealed interface Group extends ForwardingAudience permits GroupImpl {
static Group findGroup(@NotNull Player player) {
return GroupManager.getGroup(player);
}
@NotNull Player leader();
@NotNull List<@NotNull Player> members();
@NotNull GroupDisplay display();
void setDisplay(@NotNull GroupDisplay display);
@Override
default @NotNull Iterable<? extends Audience> audiences() {
return members();
}
}

View file

@ -0,0 +1,168 @@
package net.minestom.arena.group;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.arena.utils.CommandUtils;
import net.minestom.arena.Messenger;
import net.minestom.server.command.builder.Command;
import net.minestom.server.entity.Player;
import net.minestom.server.utils.entity.EntityFinder;
import java.util.StringJoiner;
import static net.minestom.server.command.builder.arguments.ArgumentType.Entity;
import static net.minestom.server.command.builder.arguments.ArgumentType.Literal;
public final class GroupCommand extends Command {
public GroupCommand() {
super("group");
setCondition(CommandUtils::lobbyOnly);
addSyntax((sender, context) -> {
Player player = (Player) sender;
GroupImpl group = GroupManager.getMemberGroup(player);
if (group == null || group.members().size() == 1) {
Messenger.info(sender, Component.text("You aren't in a group. Begin by inviting another player (click).")
.clickEvent(ClickEvent.suggestCommand("/group invite "))
.hoverEvent(HoverEvent.showText(Component.text("/group invite <player>", NamedTextColor.GREEN)))
);
} else {
boolean isLeader = group.leader().equals(player);
TextComponent.Builder builder = Component.text()
.append(Component.text((isLeader ? "Your" : group.leader().getUsername() + "'s") + " group"))
.append(Component.newline())
.append(Component.text("Members (" + group.members().size() + "): "));
StringJoiner joiner = new StringJoiner(", ");
for (Player member : group.members()) {
joiner.add(member.getUsername());
}
builder.append(Component.text(joiner.toString()));
Messenger.info(sender, builder.build());
}
});
addSyntax((sender, context) ->
GroupManager.removePlayer((Player) sender), Literal("leave"));
addSyntax((sender, context) -> {
final EntityFinder finder = context.get("player");
final Player player = finder.findFirstPlayer(sender);
Player inviter = (Player) sender;
if (player != null) {
GroupImpl group = GroupManager.getMemberGroup(inviter);
if (group == null) {
group = GroupManager.getGroup(inviter);
Messenger.info(sender, "Group created");
} else if (group.members().contains(player)) {
Messenger.warn(sender, player.getName().append(Component.text(" is already in this group.")));
}
if (!group.members().contains(player) && !group.getPendingInvites().contains(player)) {
Component invite = group.getInviteMessage();
group.addPendingInvite(player);
Messenger.info(player, invite);
Messenger.info(inviter, Component.text("Invite sent to ").append(player.getName()));
}
} else {
Messenger.warn(sender, "Player not found");
}
}, Literal("invite"), Entity("player").onlyPlayers(true).singleEntity(true));
addSyntax((sender, context) -> {
final EntityFinder finder = context.get("player");
final Player newLeader = finder.findFirstPlayer(sender);
Player player = (Player) sender;
if (newLeader != null) {
GroupImpl group = GroupManager.getMemberGroup(player);
if (group == null) {
GroupManager.getGroup(newLeader);
Messenger.info(sender, "Group created");
} else if (group.leader() != player) {
Messenger.warn(sender, "You are not the leader of this group");
} else if (group.leader() == newLeader) {
Messenger.warn(sender, "You are already the leader");
} else {
group.setLeader(newLeader);
}
} else {
Messenger.warn(sender, "Player not found");
}
// TODO: only show players in the group
}, Literal("leader"), Entity("player").onlyPlayers(true).singleEntity(true));
addSyntax((sender, context) -> {
final EntityFinder finder = context.get("player");
final Player toKick = finder.findFirstPlayer(sender);
Player player = (Player) sender;
if (toKick != null) {
GroupImpl group = GroupManager.getMemberGroup(player);
if (group == null) {
Messenger.warn(sender, "You are not in a group");
} else if (group.leader() == player) {
if (toKick == player) {
GroupManager.removePlayer(player);
} else {
if (group.removeMember(toKick)) {
group.members().forEach(p -> {
if (p == player) {
Messenger.info(p, Component.text("You kicked ")
.append(toKick.getName()).append(Component.text(" from the group")));
} else {
Messenger.info(p, toKick.getName().append(Component.text(" was kicked from your group")));
}
});
Messenger.info(toKick, Component.text("You have been kicked rom ")
.append(player.getName())
.append(Component.text("'s group")));
} else {
Messenger.warn(sender, "They are not in your group");
}
}
} else {
Messenger.warn(player, "You are not the leader of this group");
}
} else {
Messenger.warn(sender, "Player not found");
}
// TODO: only show players in the group
}, Literal("kick"), Entity("player").onlyPlayers(true).singleEntity(true));
addSyntax((sender, context) -> {
final EntityFinder finder = context.get("player");
final Player player = finder.findFirstPlayer(sender);
if (player != null) {
GroupImpl group = GroupManager.getGroup(player);
Player invitee = (Player) sender;
boolean wasInvited = group.getPendingInvites().contains(invitee);
if (wasInvited) {
GroupManager.removePlayer(invitee); // Remove from old group
group.addMember(invitee);
Component accepted = group.getAcceptedMessage();
Messenger.info(invitee, accepted);
} else if (group.members().contains(invitee)) {
Messenger.warn(invitee, "You are already in this group");
} else {
Messenger.warn(invitee, Component.text("You have not been invited to ")
.append(group.leader().getName()).append(Component.text("'s group")));
}
} else {
Messenger.warn(sender, "Group not found");
}
}, Literal("accept"), Entity("player").onlyPlayers(true).singleEntity(true));
}
}

View file

@ -0,0 +1,12 @@
package net.minestom.arena.group;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.player.PlayerDisconnectEvent;
import org.jetbrains.annotations.NotNull;
public class GroupEvent {
public static void hook(@NotNull EventNode<Event> eventHandler) {
eventHandler.addListener(PlayerDisconnectEvent.class, event -> GroupManager.removePlayer(event.getPlayer()));
}
}

View file

@ -0,0 +1,99 @@
package net.minestom.arena.group;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.arena.Messenger;
import net.minestom.arena.group.displays.GroupDisplay;
import net.minestom.server.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
final class GroupImpl implements Group {
private final List<Player> players = new ArrayList<>();
private final Set<Player> pendingInvites = Collections.newSetFromMap(new WeakHashMap<>());
private Player leader;
private GroupDisplay display;
@Override
public @NotNull Player leader() {
return leader;
}
GroupImpl(@NotNull Player leader) {
this.leader = leader;
players.add(leader);
}
@Override
public @NotNull List<Player> members() {
return List.copyOf(players);
}
@Override
public @NotNull GroupDisplay display() {
return display;
}
@Override
public void setDisplay(@NotNull GroupDisplay display) {
if (this.display != null) this.display.clean();
this.display = display;
display.update();
}
public void addPendingInvite(@NotNull Player player) {
pendingInvites.add(player);
}
public @NotNull Set<Player> getPendingInvites() {
return pendingInvites;
}
public void addMember(@NotNull Player player) {
if (players.add(player)) {
pendingInvites.remove(player);
players.forEach(p -> {
if (p != player) {
Messenger.info(p, player.getName().append(Component.text(" has joined your group")));
}
});
display.update();
}
}
public boolean removeMember(@NotNull Player player) {
if (players.remove(player)) {
players.forEach(p -> Messenger.info(p, player.getName().append(Component.text(" has left your group"))));
display.update();
return true;
}
return false;
}
public @NotNull Component getInviteMessage() {
return leader.getName()
.append(Component.text(" Has invited you to join their group. "))
.append(Component.text("[Accept]").color(NamedTextColor.GREEN).clickEvent(
ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/group accept " + leader.getUsername())
));
}
public Component getAcceptedMessage() {
return Component.text("You have been added to ")
.append(leader.getName())
.append(Component.text("'s group"));
}
public void setLeader(@NotNull Player player) {
this.leader = player;
players.forEach(p -> Messenger.info(p, player.getName().append(Component.text(" has become the group leader"))));
display.update();
}
}

View file

@ -0,0 +1,63 @@
package net.minestom.arena.group;
import net.minestom.arena.lobby.LobbySidebarDisplay;
import net.minestom.arena.Messenger;
import net.minestom.server.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public final class GroupManager {
private static final Map<Player, GroupImpl> groups = new HashMap<>();
public static @NotNull GroupImpl getGroup(@NotNull Player player) {
GroupImpl group = groups.get(player);
if (group == null) group = createGroup(player);
return group;
}
public static @NotNull GroupImpl createGroup(@NotNull Player player) {
GroupImpl group = new GroupImpl(player);
group.setDisplay(new LobbySidebarDisplay(group));
groups.put(player, group);
return group;
}
public static void transferOwnership(@NotNull GroupImpl group, @NotNull Player newLeader) {
groups.remove(group.leader());
group.setLeader(newLeader);
Messenger.info(group, "Group ownership has been transferred to " + newLeader.getUsername());
groups.put(newLeader, group);
}
public static void removePlayer(@NotNull Player player) {
GroupImpl foundGroup = getMemberGroup(player);
if (foundGroup == null) return;
foundGroup.removeMember(player);
// If the leader is removed, change the leader
if (groups.containsKey(player)) {
Optional<Player> newLeader = groups.get(player).members().stream().findFirst();
if (newLeader.isPresent()) {
transferOwnership(groups.get(player), newLeader.get());
Messenger.info(player, "You have left your group and ownership has been transferred");
} else {
Messenger.info(player, "Your group has been disbanded");
groups.remove(player);
}
} else {
Messenger.info(player, "You have left your group");
}
}
public static GroupImpl getMemberGroup(@NotNull Player player) {
for (GroupImpl group : groups.values())
if (group.members().contains(player))
return group;
return null;
}
}

View file

@ -0,0 +1,6 @@
package net.minestom.arena.group.displays;
public interface GroupDisplay {
void update();
default void clean() {}
}

View file

@ -0,0 +1,88 @@
package net.minestom.arena.group.displays;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.arena.Messenger;
import net.minestom.arena.group.Group;
import net.minestom.server.entity.Player;
import net.minestom.server.scoreboard.Sidebar;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public abstract class GroupSidebarDisplay implements GroupDisplay {
private static final int MAX_SCOREBOARD_LINES = 15;
private final Sidebar sidebar = new Sidebar(Component.text("Group"));
private final Group group;
public GroupSidebarDisplay(Group group) {
this.group = group;
}
private List<Sidebar.ScoreboardLine> createLines() {
List<Sidebar.ScoreboardLine> lines = new ArrayList<>();
List<Player> groupMembers = group.members();
// separate check is required to prevent "1 more..." from occurring when the player could just be displayed.
if (groupMembers.size() <= MAX_SCOREBOARD_LINES) {
for (Player player : groupMembers) {
lines.add(createPlayerLine(player, group));
}
} else {
for (int i = 0; i < groupMembers.size() && i < 14; i++) {
lines.add(createPlayerLine(groupMembers.get(i), group));
}
lines.add(new Sidebar.ScoreboardLine(
"more",
Component.text(groupMembers.size() - 14 + " more...", NamedTextColor.DARK_GREEN),
-1
));
}
lines.addAll(createAdditionalLines());
return lines;
}
protected abstract Sidebar.ScoreboardLine createPlayerLine(Player player, Group group);
protected List<Sidebar.ScoreboardLine> createAdditionalLines() {
return List.of();
}
@Override
public final void update() {
Set<Sidebar.ScoreboardLine> lines = sidebar.getLines();
for (Sidebar.ScoreboardLine line : lines) {
sidebar.removeLine(line.getId());
}
Set<Player> toUpdate = new HashSet<>(group.members());
toUpdate.retainAll(sidebar.getPlayers());
Set<Player> toRemove = new HashSet<>(sidebar.getPlayers());
toRemove.removeAll(toUpdate);
for (Player player : toRemove) {
sidebar.removeViewer(player);
}
for (Sidebar.ScoreboardLine line : createLines())
sidebar.createLine(line);
Set<Player> toAdd = new HashSet<>(group.members());
toAdd.removeAll(sidebar.getPlayers());
for (Player player : toAdd) {
sidebar.addViewer(player);
}
}
@Override
public void clean() {
for (Player viewer : sidebar.getViewers()) {
sidebar.removeViewer(viewer);
}
}
}

55
arena/lobby/Lobby.java Normal file
View file

@ -0,0 +1,55 @@
package net.minestom.arena.lobby;
import net.minestom.arena.group.Group;
import net.minestom.arena.utils.FullbrightDimension;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.event.instance.AddEntityToInstanceEvent;
import net.minestom.server.event.item.ItemDropEvent;
import net.minestom.server.event.player.PlayerEntityInteractEvent;
import net.minestom.server.instance.AnvilLoader;
import net.minestom.server.instance.Instance;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import java.nio.file.Path;
public final class Lobby {
public static final Instance INSTANCE;
static {
final Instance instance = MinecraftServer.getInstanceManager().createInstanceContainer(
FullbrightDimension.INSTANCE, new AnvilLoader(Path.of("lobby")));
Map.create(instance, new Pos(2, 18, 9));
instance.setTimeRate(0);
for (NPC npc : NPC.spawnNPCs(instance)) {
instance.eventNode().addListener(EntityAttackEvent.class, npc::handle)
.addListener(PlayerEntityInteractEvent.class, npc::handle);
}
instance.eventNode().addListener(AddEntityToInstanceEvent.class, event -> {
if (!(event.getEntity() instanceof Player player)) return;
if (player.getInstance() != null) player.scheduler().scheduleNextTick(() -> onArenaFinish(player));
else onFirstSpawn(player);
}).addListener(ItemDropEvent.class, event -> event.setCancelled(true));
INSTANCE = instance;
}
private static void onFirstSpawn(Player player) {
player.sendPackets(Map.packets());
final Group group = Group.findGroup(player);
group.setDisplay(new LobbySidebarDisplay(group));
}
private static void onArenaFinish(Player player) {
player.refreshCommands();
player.getInventory().clear();
player.teleport(new Pos(0.5, 16, 0.5));
player.tagHandler().updateContent(NBTCompound.EMPTY);
}
}

View file

@ -0,0 +1,33 @@
package net.minestom.arena.lobby;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.arena.Icons;
import net.minestom.arena.Messenger;
import net.minestom.arena.group.Group;
import net.minestom.arena.group.displays.GroupSidebarDisplay;
import net.minestom.server.entity.Player;
import net.minestom.server.scoreboard.Sidebar;
public final class LobbySidebarDisplay extends GroupSidebarDisplay {
public LobbySidebarDisplay(Group group) {
super(group);
}
@Override
protected Sidebar.ScoreboardLine createPlayerLine(Player player, Group group) {
if (player.equals(group.leader())) {
return new Sidebar.ScoreboardLine(
player.getUuid().toString(),
Component.text(Icons.STAR + " ").color(NamedTextColor.WHITE).append(player.getName().color(Messenger.ORANGE_COLOR)),
1
);
} else {
return new Sidebar.ScoreboardLine(
player.getUuid().toString(),
player.getName(),
0
);
}
}
}

82
arena/lobby/Map.java Normal file
View file

@ -0,0 +1,82 @@
package net.minestom.arena.lobby;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.metadata.other.ItemFrameMeta;
import net.minestom.server.instance.Instance;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.item.metadata.MapMeta;
import net.minestom.server.map.framebuffers.LargeGraphics2DFramebuffer;
import net.minestom.server.network.packet.server.SendablePacket;
import org.jetbrains.annotations.NotNull;
import javax.imageio.ImageIO;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
final class Map {
private static SendablePacket[] packets = null;
private Map() {}
public static SendablePacket[] packets() {
if (packets != null) return packets;
try {
final LargeGraphics2DFramebuffer framebuffer = new LargeGraphics2DFramebuffer(5 * 128, 3 * 128);
final InputStream imageStream = Lobby.class.getResourceAsStream("/minestom.png");
assert imageStream != null;
BufferedImage image = ImageIO.read(imageStream);
framebuffer.getRenderer().drawRenderedImage(image, AffineTransform.getScaleInstance(1.0, 1.0));
packets = mapPackets(framebuffer);
return packets;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Creates the maps on the board in the lobby
*/
public static void create(@NotNull Instance instance, Point maximum) {
final int maxX = maximum.blockX();
final int maxY = maximum.blockY();
final int z = maximum.blockZ();
for (int i = 0; i < 15; i++) {
final int x = maxX - i % 5;
final int y = maxY - i / 5;
final int id = i;
final Entity itemFrame = new Entity(EntityType.ITEM_FRAME);
final ItemFrameMeta meta = (ItemFrameMeta) itemFrame.getEntityMeta();
itemFrame.setInstance(instance, new Pos(x, y, z, 180, 0));
meta.setNotifyAboutChanges(false);
meta.setOrientation(ItemFrameMeta.Orientation.NORTH);
meta.setInvisible(true);
meta.setItem(ItemStack.builder(Material.FILLED_MAP)
.meta(MapMeta.class, builder -> builder.mapId(id))
.build());
meta.setNotifyAboutChanges(true);
}
}
/**
* Creates packets for maps that will display an image on the board in the lobby
*/
private static SendablePacket[] mapPackets(@NotNull LargeGraphics2DFramebuffer framebuffer) {
final SendablePacket[] packets = new SendablePacket[15];
for (int i = 0; i < 15; i++) {
final int x = i % 5;
final int y = i / 5;
packets[i] = framebuffer.createSubView(x * 128, y * 128).preparePacket(i);
}
return packets;
}
}

167
arena/lobby/NPC.java Normal file
View file

@ -0,0 +1,167 @@
package net.minestom.arena.lobby;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.minestom.arena.Messenger;
import net.minestom.arena.game.ArenaCommand;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.*;
import net.minestom.server.entity.ai.GoalSelector;
import net.minestom.server.entity.ai.target.ClosestEntityTarget;
import net.minestom.server.entity.metadata.PlayerMeta;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.event.player.PlayerEntityInteractEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket;
import net.minestom.server.sound.SoundEvent;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
// https://gist.github.com/iam4722202468/36630043ca89e786bb6318e296f822f8
final class NPC extends EntityCreature {
private final String name;
private final PlayerSkin skin;
private final Consumer<Player> onClick;
NPC(@NotNull String name, @NotNull PlayerSkin skin, @NotNull Instance instance,
@NotNull Point spawn, @NotNull Consumer<Player> onClick) {
super(EntityType.PLAYER);
this.name = name;
this.skin = skin;
this.onClick = onClick;
final PlayerMeta meta = (PlayerMeta) getEntityMeta();
meta.setNotifyAboutChanges(false);
meta.setCapeEnabled(false);
meta.setJacketEnabled(true);
meta.setLeftSleeveEnabled(true);
meta.setRightSleeveEnabled(true);
meta.setLeftLegEnabled(true);
meta.setRightLegEnabled(true);
meta.setHatEnabled(true);
meta.setNotifyAboutChanges(true);
addAIGroup(
List.of(new LookAtPlayerGoal(this)),
List.of(new ClosestEntityTarget(this, 15, entity -> entity instanceof Player))
);
setInstance(instance, spawn);
}
public void handle(@NotNull EntityAttackEvent event) {
if (event.getTarget() != this) return;
if (!(event.getEntity() instanceof Player player)) return;
player.playSound(Sound.sound()
.type(SoundEvent.BLOCK_NOTE_BLOCK_PLING)
.pitch(2)
.build(), event.getTarget());
onClick.accept(player);
}
public void handle(@NotNull PlayerEntityInteractEvent event) {
if (event.getTarget() != this) return;
if (event.getHand() != Player.Hand.MAIN) return; // Prevent duplicating event
event.getEntity().playSound(Sound.sound()
.type(SoundEvent.BLOCK_NOTE_BLOCK_PLING)
.pitch(2)
.build(), event.getTarget());
onClick.accept(event.getEntity());
}
@Override
public void updateNewViewer(@NotNull Player player) {
// Required to spawn player
final List<PlayerInfoUpdatePacket.Property> properties = List.of(
new PlayerInfoUpdatePacket.Property("textures", skin.textures(), skin.signature())
);
player.sendPacket(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.ADD_PLAYER,
new PlayerInfoUpdatePacket.Entry(
getUuid(), name, properties, false, 0, GameMode.SURVIVAL, null,
null)
)
);
super.updateNewViewer(player);
}
private static final class LookAtPlayerGoal extends GoalSelector {
private Entity target;
public LookAtPlayerGoal(EntityCreature entityCreature) {
super(entityCreature);
}
@Override
public boolean shouldStart() {
target = findTarget();
return target != null;
}
@Override
public void start() {}
@Override
public void tick(long time) {
if (entityCreature.getDistanceSquared(target) > 225 ||
entityCreature.getInstance() != target.getInstance()) {
target = null;
return;
}
entityCreature.lookAt(target);
}
@Override
public boolean shouldEnd() {
return target == null;
}
@Override
public void end() {}
}
public static List<NPC> spawnNPCs(@NotNull Instance instance) {
try {
final java.util.Map<String, PlayerSkin> skins = new HashMap<>();
final Gson gson = new Gson();
final JsonObject root = gson.fromJson(new String(Lobby.class.getResourceAsStream("/skins.json")
.readAllBytes()), JsonObject.class);
for (JsonElement skin : root.getAsJsonArray("skins")) {
final JsonObject object = skin.getAsJsonObject();
final String owner = object.get("owner").getAsString();
final String value = object.get("value").getAsString();
final String signature = object.get("signature").getAsString();
skins.put(owner, new PlayerSkin(value, signature));
}
return List.of(
new NPC("Discord", skins.get("Discord"), instance, new Pos(8.5, 15, 8.5),
player -> Messenger.info(player, Component.text("Click here to join the Discord server")
.clickEvent(ClickEvent.openUrl("https://discord.gg/minestom")))),
new NPC("Website", skins.get("Website"), instance, new Pos(-7.5, 15, 8.5),
player -> Messenger.info(player, Component.text("Click here to go to the Minestom website")
.clickEvent(ClickEvent.openUrl("https://minestom.net")))),
new NPC("GitHub", skins.get("GitHub"), instance, new Pos(8.5, 15, -7.5),
player -> Messenger.info(player, Component.text("Click here to go to the Arena GitHub repository")
.clickEvent(ClickEvent.openUrl("https://github.com/Minestom/Arena")))),
new NPC("Play", skins.get("Play"), instance, new Pos(-7.5, 15, -7.5), ArenaCommand::open)
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,25 @@
package net.minestom.arena.utils;
import net.minestom.arena.lobby.Lobby;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.ConsoleSender;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.Instance;
public final class CommandUtils {
public static boolean lobbyOnly(CommandSender sender, String commandString) {
if (!(sender instanceof Player player)) return false;
final Instance instance = player.getInstance();
return instance == null || instance == Lobby.INSTANCE;
}
public static boolean arenaOnly(CommandSender sender, String commandString) {
if (!(sender instanceof Player player)) return false;
final Instance instance = player.getInstance();
return instance != null && instance != Lobby.INSTANCE;
}
public static boolean consoleOnly(CommandSender sender, String commandString) {
return sender instanceof ConsoleSender;
}
}

View file

@ -0,0 +1,69 @@
package net.minestom.arena.utils;
import it.unimi.dsi.fastutil.booleans.BooleanConsumer;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiPredicate;
public final class ConcurrentUtils {
private ConcurrentUtils() {
//no instance
}
/**
* Used to add timeout for CompletableFutures
*
* @param future the future which has to complete
* @param timeout duration to wait for the future to complete
* @param action Action to run after the future completes or the timeout is reached.<br>
* Parameter means:
* <ul>
* <li><b>true</b> - the timeout is reached</li>
* <li><b>false</b> - future completed before timeout</li>
* </ul>
* @return the new CompletionStage
*/
public static CompletableFuture<Void> thenRunOrTimeout(CompletableFuture<?> future, Duration timeout, BooleanConsumer action) {
final CompletableFuture<Boolean> f = new CompletableFuture<>();
CompletableFuture.delayedExecutor(timeout.toNanos(), TimeUnit.NANOSECONDS).execute(() -> f.complete(true));
future.thenRun(() -> f.complete(false));
return f.thenAccept(action);
}
/**
* Create a future from a CountDownLatch
*
* @return a future that completes when the countdown reaches zero
*/
public static CompletableFuture<Void> futureFromCountdown(CountDownLatch countDownLatch) {
final CompletableFuture<Void> future = new CompletableFuture<>();
CompletableFuture.runAsync(() -> {
try {
countDownLatch.await();
future.complete(null);
} catch (InterruptedException e) {
future.completeExceptionally(e);
}
});
return future;
}
public static <V> boolean testAndSet(AtomicReference<V> reference, BiPredicate<V, V> predicate, V testValue, V newValue) {
for (;;) {
V prev = reference.get();
if (predicate.test(prev, testValue)) {
if (reference.compareAndSet(prev, newValue)) return true;
} else {
return false;
}
}
}
public static <V> boolean testAndSet(AtomicReference<V> reference, BiPredicate<V, V> predicate, V newValue) {
return testAndSet(reference, predicate, newValue, newValue);
}
}

View file

@ -0,0 +1,15 @@
package net.minestom.arena.utils;
import net.minestom.server.MinecraftServer;
import net.minestom.server.utils.NamespaceID;
import net.minestom.server.world.DimensionType;
public class FullbrightDimension {
public static final DimensionType INSTANCE = DimensionType.builder(NamespaceID.from("minestom:full_bright"))
.ambientLight(2.0f)
.build();
static {
MinecraftServer.getDimensionTypeManager().addDimension(INSTANCE);
}
}

View file

@ -0,0 +1,38 @@
package net.minestom.arena.utils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextDecoration;
import net.minestom.server.item.ItemHideFlag;
import net.minestom.server.item.ItemMeta;
import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.Contract;
public final class ItemUtils {
private ItemUtils() {
}
@Contract("null -> null; !null -> !null")
public static Component stripItalics(Component component) {
if (component == null) return null;
if (component.decoration(TextDecoration.ITALIC) == TextDecoration.State.NOT_SET) {
component = component.decoration(TextDecoration.ITALIC, false);
}
return component;
}
@Contract("null -> null; !null -> !null")
public static ItemStack stripItalics(ItemStack itemStack) {
if (itemStack == null) return null;
return itemStack.withDisplayName(ItemUtils::stripItalics)
.withLore(lore -> lore.stream()
.map(ItemUtils::stripItalics)
.toList());
}
public static ItemMeta.Builder hideFlags(ItemMeta.Builder builder) {
return builder.hideFlag(ItemHideFlag.values());
}
}

20
build.gradle Normal file
View file

@ -0,0 +1,20 @@
plugins {
id 'java'
}
group = 'me.zen'
version = '0.0.1'
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
dependencies {
implementation("com.github.Minestom:Minestom:aad7bdab0f")
implementation("de.articdive:jnoise-pipeline:4.0.0")
implementation("io.prometheus:simpleclient:0.16.0")
implementation("io.prometheus:simpleclient_hotspot:0.16.0")
implementation("io.prometheus:simpleclient_httpserver:0.16.0")
implementation("net.kyori:adventure-text-minimessage:4.12.0")
}

22
config.json Normal file
View file

@ -0,0 +1,22 @@
{
"server": {
"host": "0.0.0.0",
"port": 25565,
"mojangAuth": true,
"motd": [
"Line1",
"Line2"
]
},
"proxy": {
"enabled": false,
"secret": "forwarding-secret"
},
"permissions": {
"operators": []
},
"prometheus": {
"enabled": false,
"port": 9090
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Wed Jan 24 07:27:22 PST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
gradlew vendored Normal file
View file

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View file

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

2
settings.gradle Normal file
View file

@ -0,0 +1,2 @@
rootProject.name = 'MinestomServ'

View file

@ -0,0 +1,175 @@
package me.zen;
import me.zen.instance.LobbyInstance;
import me.zen.util.MessageHelper;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import net.minestom.server.MinecraftServer;
import net.minestom.server.advancements.FrameType;
import net.minestom.server.advancements.notifications.Notification;
import net.minestom.server.advancements.notifications.NotificationCenter;
import net.minestom.server.adventure.audience.Audiences;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.ItemEntity;
import net.minestom.server.entity.Player;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.item.ItemDropEvent;
import net.minestom.server.event.item.PickupItemEvent;
import net.minestom.server.event.player.*;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.event.server.ServerTickMonitorEvent;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.monitoring.TickMonitor;
import net.minestom.server.ping.ResponseData;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.identity.NamedAndIdentified;
import net.minestom.server.utils.time.TimeUnit;
import java.time.Duration;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import static net.minestom.server.MinecraftServer.LOGGER;
public class Initialization {
private static final EventNode<Event> NODE = EventNode.all("node")
.addListener(AsyncPlayerConfigurationEvent.class, event -> {
final Player player = event.getPlayer();
// Check if there is any instance if not kick the player
if ( MinecraftServer.getInstanceManager().getInstances().isEmpty() ) {
player.kick(Component.text("No instances found", MessageHelper.RED_COLOR));
return;
}
event.setSpawningInstance(LobbyInstance.INSTANCE);
player.setRespawnPoint(new Pos(0.5, 18, 0.5));
// Permission
player.setPermissionLevel(4);
}).addListener(PlayerSpawnEvent.class, event -> {
final Player player = event.getPlayer();
ItemStack itemStack = ItemStack.builder(Material.STONE)
.amount(64)
.build();
player.getInventory().addItemStack(itemStack);
if (event.isFirstSpawn()) {
Notification notification = new Notification(
Component.text("Welcome to the server!", MessageHelper.BLUE_COLOR),
FrameType.TASK,
Material.IRON_SWORD
);
NotificationCenter.send(notification, event.getPlayer());
// Join message
Audiences.all().sendMessage(Component.text("[", MessageHelper.GRAY_COLOR)
.append(Component.text("+", MessageHelper.BLUE_COLOR))
.append(Component.text("] ", MessageHelper.GRAY_COLOR))
.append(Component.text(player.getUsername(), MessageHelper.BLUE_ISH_COLOR))
.append(Component.text(" joined the server", MessageHelper.GRAY_COLOR))
);
}
}).addListener(PlayerDisconnectEvent.class, event -> {
final Player player = event.getPlayer();
// Leave message
Audiences.all().sendMessage(Component.text("[", MessageHelper.GRAY_COLOR)
.append(Component.text("-", MessageHelper.RED_ISH_COLOR))
.append(Component.text("] ", MessageHelper.GRAY_COLOR))
.append(Component.text(player.getUsername(), MessageHelper.RED_COLOR))
.append(Component.text(" left the server", MessageHelper.GRAY_COLOR))
);
}).addListener(PlayerBlockInteractEvent.class, event -> {
var block = event.getBlock();
var rawOpenProp = block.getProperty("open");
if (rawOpenProp == null) return;
block = block.withProperty("open", String.valueOf(!Boolean.parseBoolean(rawOpenProp)));
event.getInstance().setBlock(event.getBlockPosition(), block);
}).addListener(PickupItemEvent.class, event -> {
final Entity entity = event.getLivingEntity();
if (entity instanceof Player) {
// Cancel event if player does not have enough inventory space
final ItemStack itemStack = event.getItemEntity().getItemStack();
event.setCancelled(!((Player) entity).getInventory().addItemStack(itemStack));
}
}).addListener(ItemDropEvent.class, event -> {
final Player player = event.getPlayer();
ItemStack droppedItem = event.getItemStack();
Pos playerPos = player.getPosition();
ItemEntity itemEntity = new ItemEntity(droppedItem);
itemEntity.setPickupDelay(Duration.of(500, TimeUnit.MILLISECOND));
itemEntity.setInstance(player.getInstance(), playerPos.withY(y -> y + 1.5));
Vec velocity = playerPos.direction().mul(6);
itemEntity.setVelocity(velocity);
});
// This method is called from Main.java
public static void init() {
MinecraftServer.getInstanceManager().registerInstance(LobbyInstance.INSTANCE);
var eventHandler = MinecraftServer.getGlobalEventHandler();
eventHandler.addChild(NODE);
// Monitor
AtomicReference<TickMonitor> lastTick = new AtomicReference<>();
eventHandler.addListener(ServerTickMonitorEvent.class, event -> {
final TickMonitor monitor = event.getTickMonitor();
Metrics.TICK_TIME.observe(monitor.getTickTime());
Metrics.ACQUISITION_TIME.observe(monitor.getAcquisitionTime());
lastTick.set(monitor);
});
MinecraftServer.getExceptionManager().setExceptionHandler(e -> {
LOGGER.error("Global exception handler", e);
Metrics.EXCEPTIONS.labels(e.getClass().getSimpleName()).inc();
});
// Playerlist in server list
eventHandler.addListener(ServerListPingEvent.class, event -> {
ResponseData responseData = event.getResponseData();
if (event.getConnection() != null) {
responseData.addEntry(NamedAndIdentified.named(Component.text("Hello! this is ZenZoya's Testing server.", MessageHelper.GRAY_COLOR)));
}
responseData.setDescription(Component.text("Minestom Server", MessageHelper.BLUE_COLOR)
.append(Component.text(" - ", MessageHelper.GRAY_COLOR))
.append(Component.text("Testing server", MessageHelper.BLUE_ISH_COLOR))
);
});
// Tablist header and footer
MinecraftServer.getSchedulerManager().scheduleTask(() -> {
Collection<Player> players = MinecraftServer.getConnectionManager().getOnlinePlayers();
if (players.isEmpty()) return;
final Runtime runtime = Runtime.getRuntime();
final TickMonitor tickMonitor = lastTick.get();
final long ramUsage = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024;
final Component header = Component.newline()
.append(Component.text("Vortres Network", MessageHelper.BLUE_COLOR))
.append(Component.newline())
.append(Component.text("Players: ", MessageHelper.GRAY_COLOR)).append(Component.text(players.size(), MessageHelper.BLUE_ISH_COLOR))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("RAM USAGE: ", MessageHelper.GRAY_COLOR).append(Component.text(ramUsage + "MB", MessageHelper.BLUE_ISH_COLOR))
.append(Component.newline())
.append(Component.text("TICK TIME: ", MessageHelper.GRAY_COLOR).append(Component.text(MathUtils.round(tickMonitor.getTickTime(), 2) + "ms", MessageHelper.BLUE_ISH_COLOR))))
.append(Component.newline());
final Component footer = Component.newline()
.append(Component.text("Project: minestom.net", TextColor.color(0x8C8C8C))
.append(Component.newline()));
Audiences.players().sendPlayerListHeaderAndFooter(header, footer);
}, TaskSchedule.tick(2), TaskSchedule.tick(2));
}
}

View file

@ -0,0 +1,82 @@
package me.zen;
import me.zen.block.TestBlockHandler;
import me.zen.block.placement.DripstonePlacementRule;
import me.zen.commands.*;
import me.zen.features.Features;
import me.zen.instance.LobbyInstance;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.minestom.server.MinecraftServer;
import net.minestom.server.adventure.audience.Audiences;
import net.minestom.server.command.CommandManager;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.event.player.AsyncPlayerConfigurationEvent;
import net.minestom.server.event.player.PlayerSpawnEvent;
import net.minestom.server.event.GlobalEventHandler;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.event.server.ServerTickMonitorEvent;
import net.minestom.server.extras.lan.OpenToLAN;
import net.minestom.server.extras.lan.OpenToLANConfig;
import net.minestom.server.instance.block.BlockManager;
import net.minestom.server.monitoring.TickMonitor;
import net.minestom.server.ping.ResponseData;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.identity.NamedAndIdentified;
import net.minestom.server.utils.time.TimeUnit;
import java.time.Duration;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicReference;
import static me.zen.config.ConfigHandler.CONFIG;
import static net.minestom.server.MinecraftServer.LOGGER;
import static net.minestom.server.MinecraftServer.getGlobalEventHandler;
public class Main {
public static void main(String[] args) {
// Server Init
MinecraftServer.setCompressionThreshold(128);
MinecraftServer minecraftServer = MinecraftServer.init();
MinecraftServer.getBenchmarkManager().enable(Duration.of(10, TimeUnit.SECOND));
MinecraftServer.setBrandName("Vortres");
if (CONFIG.prometheus().enabled()) Metrics.init();
// Events
BlockManager blockManager = MinecraftServer.getBlockManager();
blockManager.registerBlockPlacementRule(new DripstonePlacementRule());
blockManager.registerHandler(TestBlockHandler.INSTANCE.getNamespaceId(), () -> TestBlockHandler.INSTANCE);
// Command Manager
CommandManager commandManager = MinecraftServer.getCommandManager();
commandManager.register(new HealthCommand());
commandManager.register(new ShutdownCommand());
commandManager.register(new TeleportCommand());
commandManager.register(new PlayersCommand());
commandManager.register(new FindCommand());
commandManager.register(new GiveCommand());
commandManager.register(new SaveCommand());
commandManager.register(new GamemodeCommand());
commandManager.register(new SpawnCommand());
commandManager.register(new SetBlockCommand());
commandManager.register(new TestMessageHelper());
commandManager.register(new SummonCommand());
commandManager.register(new WorldCommand());
commandManager.setUnknownCommandCallback((sender, command) -> sender.sendMessage(Component.text("> Unknown command", TextColor.color(255, 50, 50))));
// Initialization
Initialization.init();
// Start Server
minecraftServer.start("0.0.0.0", 25565);
OpenToLAN.open(new OpenToLANConfig().eventCallDelay(Duration.of(1, TimeUnit.DAY)));
Runtime.getRuntime().addShutdownHook(new Thread(MinecraftServer::stopCleanly));
}
}

View file

@ -0,0 +1,153 @@
package me.zen;
import com.sun.management.OperatingSystemMXBean;
import io.prometheus.client.*;
import io.prometheus.client.exporter.HTTPServer;
import io.prometheus.client.hotspot.GarbageCollectorExports;
import io.prometheus.client.hotspot.MemoryPoolsExports;
import me.zen.config.ConfigHandler;
import me.zen.util.NetworkUsage;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.event.entity.EntitySpawnEvent;
import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent;
import net.minestom.server.event.player.*;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class Metrics {
public static final Gauge ENTITIES = Gauge.build().name("entities")
.help("Total entities alive (excluding players)").register();
public static final Gauge GAMES_IN_PROGRESS = Gauge.build().name("games_in_progress")
.labelNames("type").help("Games currently running").register();
public static final Counter GAMES_PLAYED = Counter.build().name("games_played")
.labelNames("type").help("Number of games played").register();
public static final Summary TICK_TIME = Summary.build().name("tick_time")
.help("ms per tick").quantile(0, 1).quantile(.5, .01).quantile(1, 0)
.maxAgeSeconds(5).unit("ms").register();
public static final Summary ACQUISITION_TIME = Summary.build().name("acquisition_time")
.help("ms per acquisition").quantile(0, 1).quantile(.5, .01).quantile(1, 0)
.maxAgeSeconds(5).unit("ms").register();
public static final Counter EXCEPTIONS = Counter.build().name("exceptions")
.help("Number of exceptions").labelNames("simple_name").register();
private static final Counter PACKETS = Counter.build().name("packets").help("Number of packets by direction")
.labelNames("direction").register();
private static final Gauge ONLINE_PLAYERS = Gauge.build().name("online_players")
.help("Number of currently online players").register();
private static final Info GENERIC_INFO = Info.build().name("generic").help("Generic system information")
.register();
private static final OperatingSystemMXBean systemMXBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
public static void init() {
try {
final String unknown = "unknown";
GENERIC_INFO.info(
"java_version", System.getProperty("java.version", unknown),
"java_vendor", System.getProperty("java.vendor", unknown),
"os_arch", System.getProperty("os.arch", unknown),
"os_name", System.getProperty("os.name", unknown),
"os_version", System.getProperty("os.version", unknown),
"available_processors", "" + systemMXBean.getAvailableProcessors()
);
// Packets & players
MinecraftServer.getGlobalEventHandler()
.addListener(PlayerPacketEvent.class, e -> Metrics.PACKETS.labels("in").inc())
.addListener(PlayerPacketOutEvent.class, e -> Metrics.PACKETS.labels("out").inc())
.addListener(PlayerSpawnEvent.class, e -> Metrics.ONLINE_PLAYERS.inc())
.addListener(PlayerDisconnectEvent.class, e -> Metrics.ONLINE_PLAYERS.dec())
.addListener(EntitySpawnEvent.class, e -> {
if (!(e.getEntity() instanceof Player)) Metrics.ENTITIES.inc();
}).addListener(RemoveEntityFromInstanceEvent.class, e -> {
if (!(e.getEntity() instanceof Player)) Metrics.ENTITIES.dec();
});
// Network usage
if (NetworkUsage.checkEnabledOrExtract()) {
NetworkUsage.resetCounters();
NetworkCounter.build().name("network_io").help("Network usage").unit("bytes").labelNames("direction")
.register();
}
CPUGauge.build().name("cpu").help("CPU Usage").register();
new HTTPServer(ConfigHandler.CONFIG.prometheus().port());
new MemoryPoolsExports().register();
new GarbageCollectorExports().register();
} catch (IOException e) {
MinecraftServer.getExceptionManager().handleException(e);
}
}
private static class NetworkCounter extends SimpleCollector<Counter.Child> {
private final double created = System.currentTimeMillis()/1000f;
private final static List<String> outLabels = List.of("out");
private final static List<String> inLabels = List.of("in");
protected NetworkCounter(Builder b) {
super(b);
}
public static class Builder extends SimpleCollector.Builder<NetworkCounter.Builder, NetworkCounter> {
@Override
public NetworkCounter create() {
return new NetworkCounter(this);
}
}
public static Builder build() {
return new Builder();
}
@Override
protected Counter.Child newChild() {
return null;
}
@Override
public List<MetricFamilySamples> collect() {
List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>();
samples.add(new MetricFamilySamples.Sample(fullname + "_total", labelNames, outLabels, NetworkUsage.getBytesSent()));
samples.add(new MetricFamilySamples.Sample(fullname + "_created", labelNames, outLabels, created));
samples.add(new MetricFamilySamples.Sample(fullname + "_total", labelNames, inLabels, NetworkUsage.getBytesReceived()));
samples.add(new MetricFamilySamples.Sample(fullname + "_created", labelNames, inLabels, created));
return familySamplesList(Type.COUNTER, samples);
}
}
private static class CPUGauge extends SimpleCollector<Gauge.Child> {
protected CPUGauge(Builder b) {
super(b);
}
public static class Builder extends SimpleCollector.Builder<CPUGauge.Builder, CPUGauge> {
@Override
public CPUGauge create() {
return new CPUGauge(this);
}
}
public static Builder build() {
return new Builder();
}
@Override
protected Gauge.Child newChild() {
return new Gauge.Child();
}
@Override
public List<MetricFamilySamples> collect() {
List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>(1);
samples.add(new MetricFamilySamples.Sample(fullname, labelNames, Collections.emptyList(), systemMXBean.getProcessCpuLoad()));
return familySamplesList(Type.GAUGE, samples);
}
}
}

View file

@ -0,0 +1,70 @@
package me.zen.block;
import net.minestom.server.instance.block.BlockHandler;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagReadable;
import net.minestom.server.tag.TagSerializer;
import net.minestom.server.tag.TagWritable;
import net.minestom.server.utils.NamespaceID;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jglrxavpok.hephaistos.nbt.NBT;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import org.jglrxavpok.hephaistos.nbt.NBTList;
import org.jglrxavpok.hephaistos.nbt.NBTType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class CampfireHandler implements BlockHandler {
public static final Tag<List<ItemStack>> ITEMS = Tag.View(new TagSerializer<>() {
private final Tag<NBT> internal = Tag.NBT("Items");
@Override
public @Nullable List<ItemStack> read(@NotNull TagReadable reader) {
NBTList<NBTCompound> item = (NBTList<NBTCompound>) reader.getTag(internal);
if (item == null)
return null;
List<ItemStack> result = new ArrayList<>();
item.forEach(nbtCompound -> {
int amount = nbtCompound.getAsByte("Count");
String id = nbtCompound.getString("id");
Material material = Material.fromNamespaceId(id);
result.add(ItemStack.of(material, amount));
});
return result;
}
@Override
public void write(@NotNull TagWritable writer, @Nullable List<ItemStack> value) {
if (value == null) {
writer.removeTag(internal);
return;
}
writer.setTag(internal, NBT.List(
NBTType.TAG_Compound,
value.stream()
.map(item -> NBT.Compound(nbt -> {
nbt.setByte("Count", (byte) item.amount());
nbt.setByte("Slot", (byte) 1);
nbt.setString("id", item.material().name());
}))
.toList()
));
}
});
@Override
public @NotNull Collection<Tag<?>> getBlockEntityTags() {
return List.of(ITEMS);
}
@Override
public @NotNull NamespaceID getNamespaceId() {
return NamespaceID.from("minestom:test");
}
}

View file

@ -0,0 +1,24 @@
package me.zen.block;
import net.minestom.server.instance.block.BlockHandler;
import net.minestom.server.utils.NamespaceID;
import org.jetbrains.annotations.NotNull;
public class TestBlockHandler implements BlockHandler {
public static final BlockHandler INSTANCE = new TestBlockHandler();
@Override
public @NotNull NamespaceID getNamespaceId() {
return NamespaceID.from("minestom", "test");
}
@Override
public void onPlace(@NotNull Placement placement) {
System.out.println(placement);
}
@Override
public void onDestroy(@NotNull Destroy destroy) {
System.out.println(destroy);
}
}

View file

@ -0,0 +1,76 @@
package me.zen.block.placement;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockFace;
import net.minestom.server.instance.block.rule.BlockPlacementRule;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.Objects;
public class DripstonePlacementRule extends BlockPlacementRule {
private static final String PROP_VERTICAL_DIRECTION = "vertical_direction"; // Tip, frustum, middle(0 or more), base
private static final String PROP_THICKNESS = "thickness";
public DripstonePlacementRule() {
super(Block.POINTED_DRIPSTONE);
}
@Override
public @Nullable Block blockPlace(@NotNull PlacementState placementState) {
var blockFace = Objects.requireNonNullElse(placementState.blockFace(), BlockFace.TOP);
var direction = switch (blockFace) {
case TOP -> "up";
case BOTTOM -> "down";
default -> Objects.requireNonNullElse(placementState.cursorPosition(), Vec.ZERO).y() < 0.5 ? "up" : "down";
};
var thickness = getThickness(placementState.instance(), placementState.placePosition(), direction.equals("up"));
return block.withProperties(Map.of(
PROP_VERTICAL_DIRECTION, direction,
PROP_THICKNESS, thickness
));
}
@Override
public @NotNull Block blockUpdate(@NotNull UpdateState updateState) {
var direction = updateState.currentBlock().getProperty(PROP_VERTICAL_DIRECTION).equals("up");
var newThickness = getThickness(updateState.instance(), updateState.blockPosition(), direction);
return updateState.currentBlock().withProperty(PROP_THICKNESS, newThickness);
}
private @NotNull String getThickness(@NotNull Block.Getter instance, @NotNull Point blockPosition, boolean direction) {
var abovePosition = blockPosition.add(0, direction ? 1 : -1, 0);
var aboveBlock = instance.getBlock(abovePosition, Block.Getter.Condition.TYPE);
// If there is no dripstone above, it is always a tip
if (aboveBlock.id() != Block.POINTED_DRIPSTONE.id())
return "tip";
// If there is an opposite facing dripstone above, it is always a merged tip
if ((direction ? "down" : "up").equals(aboveBlock.getProperty(PROP_VERTICAL_DIRECTION)))
return "tip_merge";
// If the dripstone above this is a tip, it is a frustum
var aboveThickness = aboveBlock.getProperty(PROP_THICKNESS);
if ("tip".equals(aboveThickness) || "tip_merge".equals(aboveThickness))
return "frustum";
// At this point we know that there is a dripstone above, and that the dripstone is facing the same direction.
var belowPosition = blockPosition.add(0, direction ? -1 : 1, 0);
var belowBlock = instance.getBlock(belowPosition, Block.Getter.Condition.TYPE);
// If there is no dripstone below, it is always a base
if (belowBlock.id() != Block.POINTED_DRIPSTONE.id())
return "base";
// Otherwise it is a middle
return "middle";
}
@Override
public int maxUpdateDistance() {
return 2;
}
}

View file

@ -0,0 +1,100 @@
package me.zen.commands;
import net.kyori.adventure.text.Component;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.metadata.display.AbstractDisplayMeta;
import net.minestom.server.entity.metadata.display.BlockDisplayMeta;
import net.minestom.server.entity.metadata.display.ItemDisplayMeta;
import net.minestom.server.entity.metadata.display.TextDisplayMeta;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.NotNull;
public class DisplayCommand extends Command {
public DisplayCommand() {
super("display");
var follow = ArgumentType.Literal("follow");
addSyntax(this::spawnItem, ArgumentType.Literal("item"));
addSyntax(this::spawnBlock, ArgumentType.Literal("block"));
addSyntax(this::spawnText, ArgumentType.Literal("text"));
addSyntax(this::spawnItem, ArgumentType.Literal("item"), follow);
addSyntax(this::spawnBlock, ArgumentType.Literal("block"), follow);
addSyntax(this::spawnText, ArgumentType.Literal("text"), follow);
}
public void spawnItem(@NotNull CommandSender sender, @NotNull CommandContext context) {
if (!(sender instanceof Player player))
return;
var entity = new Entity(EntityType.ITEM_DISPLAY);
var meta = (ItemDisplayMeta) entity.getEntityMeta();
meta.setTransformationInterpolationDuration(20);
meta.setItemStack(ItemStack.of(Material.STICK));
entity.setInstance(player.getInstance(), player.getPosition());
if (context.has("follow")) {
startSmoothFollow(entity, player);
}
}
public void spawnBlock(@NotNull CommandSender sender, @NotNull CommandContext context) {
if (!(sender instanceof Player player))
return;
var entity = new Entity(EntityType.BLOCK_DISPLAY);
var meta = (BlockDisplayMeta) entity.getEntityMeta();
meta.setTransformationInterpolationDuration(20);
meta.setBlockState(Block.ORANGE_CANDLE_CAKE.stateId());
entity.setInstance(player.getInstance(), player.getPosition()).join();
if (context.has("follow")) {
startSmoothFollow(entity, player);
}
}
public void spawnText(@NotNull CommandSender sender, @NotNull CommandContext context) {
if (!(sender instanceof Player player))
return;
var entity = new Entity(EntityType.TEXT_DISPLAY);
var meta = (TextDisplayMeta) entity.getEntityMeta();
meta.setTransformationInterpolationDuration(20);
meta.setBillboardRenderConstraints(AbstractDisplayMeta.BillboardConstraints.CENTER);
meta.setText(Component.text("Hello, world!"));
entity.setInstance(player.getInstance(), player.getPosition());
if (context.has("follow")) {
startSmoothFollow(entity, player);
}
}
private void startSmoothFollow(@NotNull Entity entity, @NotNull Player player) {
// entity.setCustomName(Component.text("MY CUSTOM NAME"));
// entity.setCustomNameVisible(true);
MinecraftServer.getSchedulerManager().buildTask(() -> {
var meta = (AbstractDisplayMeta) entity.getEntityMeta();
meta.setNotifyAboutChanges(true);
meta.setTransformationInterpolationStartDelta(1);
meta.setTransformationInterpolationDuration(20);
// meta.setPosRotInterpolationDuration(20);
// entity.teleport(player.getPosition());
// meta.setScale(new Vec(5, 5, 5));
meta.setTranslation(player.getPosition().sub(entity.getPosition()));
meta.setNotifyAboutChanges(true);
}).delay(20, TimeUnit.SERVER_TICK).repeat(20, TimeUnit.SERVER_TICK).schedule();
}
}

View file

@ -0,0 +1,53 @@
package me.zen.commands;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Player;
import java.util.Collection;
import static net.minestom.server.command.builder.arguments.ArgumentType.Float;
import static net.minestom.server.command.builder.arguments.ArgumentType.Literal;
public class FindCommand extends Command {
public FindCommand() {
super("find");
this.addSyntax(
this::executorEntity,
Literal("entity"),
Float("range")
);
}
private void executorEntity(CommandSender sender, CommandContext context) {
Player player = (Player) sender;
float range = context.get("range");
Collection<Entity> entities = player.getInstance().getNearbyEntities(player.getPosition(), range);
player.sendMessage(Component.text("Search result", TextColor.color(0x7575FF))
.append(Component.text(":", TextColor.color(0x5F5F5F)))
);
for (Entity entity : entities) {
player.sendMessage(Component.text(" - ", TextColor.color(0x9595FF))
.append(Component.text(String.valueOf(entity.getEntityType()), TextColor.color(0x8C8C8C)))
.append(Component.text(":", TextColor.color(0x5F5F5F)))
);
player.sendMessage(Component.text(" - Meta: ", TextColor.color(0x5F5F5F))
.append(Component.text(String.valueOf(" " + entity.getEntityMeta()), TextColor.color(0x8C8C8C)))
.append(Component.text(""))
);
player.sendMessage(Component.text(" - Position: ", TextColor.color(0x5F5F5F))
.append(Component.text(String.valueOf(" " + entity.getPosition()), TextColor.color(0x8C8C8C)))
.append(Component.text(""))
);
player.sendMessage("");
}
}
}

View file

@ -0,0 +1,131 @@
package me.zen.commands;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.ArgumentEnum;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.command.builder.arguments.minecraft.ArgumentEntity;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.utils.entity.EntityFinder;
import java.util.List;
import java.util.Locale;
/**
* Command that make a player change gamemode, made in
* the style of the vanilla /gamemode command.
*
* @see <a href="https://minecraft.fandom.com/wiki/Commands/gamemode">...</a>
*/
public class GamemodeCommand extends Command {
public GamemodeCommand() {
super("gamemode", "gm");
//GameMode parameter
ArgumentEnum<GameMode> gamemode = ArgumentType.Enum("gamemode", GameMode.class).setFormat(ArgumentEnum.Format.LOWER_CASED);
gamemode.setCallback((sender, exception) -> {
sender.sendMessage(
Component.text("Invalid gamemode ", NamedTextColor.RED)
.append(Component.text(exception.getInput(), NamedTextColor.WHITE))
.append(Component.text("!")), MessageType.SYSTEM);
});
ArgumentEntity player = ArgumentType.Entity("targets").onlyPlayers(true);
//Upon invalid usage, print the correct usage of the command to the sender
setDefaultExecutor((sender, context) -> {
String commandName = context.getCommandName();
sender.sendMessage(Component.text("Usage: /" + commandName + " <gamemode> [targets]", NamedTextColor.RED), MessageType.SYSTEM);
});
//Command Syntax for /gamemode <gamemode>
addSyntax((sender, context) -> {
//Limit execution to players only
if (!(sender instanceof Player p)) {
sender.sendMessage(Component.text("Please run this command in-game.", NamedTextColor.RED));
return;
}
//Check permission, this could be replaced with hasPermission
if (p.getPermissionLevel() < 2) {
sender.sendMessage(Component.text("You don't have permission to use this command.", NamedTextColor.RED));
return;
}
GameMode mode = context.get(gamemode);
//Set the gamemode for the sender
executeSelf(p, mode);
}, gamemode);
//Command Syntax for /gamemode <gamemode> [targets]
addSyntax((sender, context) -> {
//Check permission for players only
//This allows the console to use this syntax too
if (sender instanceof Player p && p.getPermissionLevel() < 2) {
sender.sendMessage(Component.text("You don't have permission to use this command.", NamedTextColor.RED));
return;
}
EntityFinder finder = context.get(player);
GameMode mode = context.get(gamemode);
//Set the gamemode for the targets
executeOthers(sender, mode, finder.find(sender));
}, gamemode, player);
}
/**
* Sets the gamemode for the specified entities, and
* notifies them (and the sender) in the chat.
*/
private void executeOthers(CommandSender sender, GameMode mode, List<Entity> entities) {
if (entities.size() == 0) {
//If there are no players that could be modified, display an error message
if (sender instanceof Player)
sender.sendMessage(Component.translatable("argument.entity.notfound.player", NamedTextColor.RED), MessageType.SYSTEM);
else sender.sendMessage(Component.text("No player was found", NamedTextColor.RED), MessageType.SYSTEM);
} else for (Entity entity : entities) {
if (entity instanceof Player p) {
if (p == sender) {
//If the player is the same as the sender, call
//executeSelf to display one message instead of two
executeSelf((Player) sender, mode);
} else {
p.setGameMode(mode);
String gamemodeString = "gameMode." + mode.name().toLowerCase(Locale.ROOT);
Component gamemodeComponent = Component.translatable(gamemodeString);
Component playerName = p.getDisplayName() == null ? p.getName() : p.getDisplayName();
//Send a message to the changed player and the sender
p.sendMessage(Component.translatable("gameMode.changed", gamemodeComponent), MessageType.SYSTEM);
sender.sendMessage(Component.translatable("commands.gamemode.success.other", playerName, gamemodeComponent), MessageType.SYSTEM);
}
}
}
}
/**
* Sets the gamemode for the executing Player, and
* notifies them in the chat.
*/
private void executeSelf(Player sender, GameMode mode) {
sender.setGameMode(mode);
//The translation keys 'gameMode.survival', 'gameMode.creative', etc.
//correspond to the translated game mode names.
String gamemodeString = "gameMode." + mode.name().toLowerCase(Locale.ROOT);
Component gamemodeComponent = Component.translatable(gamemodeString);
//Send the translated message to the player.
sender.sendMessage(Component.translatable("commands.gamemode.success.self", gamemodeComponent), MessageType.SYSTEM);
}
}

View file

@ -0,0 +1,57 @@
package me.zen.commands;
import net.kyori.adventure.text.Component;
import net.minestom.server.command.builder.Command;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Player;
import net.minestom.server.inventory.PlayerInventory;
import net.minestom.server.inventory.TransactionOption;
import net.minestom.server.item.ItemStack;
import net.minestom.server.utils.entity.EntityFinder;
import java.util.ArrayList;
import java.util.List;
import static net.minestom.server.command.builder.arguments.ArgumentType.Integer;
import static net.minestom.server.command.builder.arguments.ArgumentType.*;
public class GiveCommand extends Command {
public GiveCommand() {
super("give");
setDefaultExecutor((sender, context) ->
sender.sendMessage(Component.text("Usage: /give <target> <item> [<count>]")));
addSyntax((sender, context) -> {
final EntityFinder entityFinder = context.get("target");
int count = context.get("count");
count = Math.min(count, PlayerInventory.INVENTORY_SIZE * 64);
ItemStack itemStack = context.get("item");
List<ItemStack> itemStacks;
if (count <= 64) {
itemStack = itemStack.withAmount(count);
itemStacks = List.of(itemStack);
} else {
itemStacks = new ArrayList<>();
while (count > 64) {
itemStacks.add(itemStack.withAmount(64));
count -= 64;
}
itemStacks.add(itemStack.withAmount(count));
}
final List<Entity> targets = entityFinder.find(sender);
for (Entity target : targets) {
if (target instanceof Player) {
Player player = (Player) target;
player.getInventory().addItemStacks(itemStacks, TransactionOption.ALL);
}
}
sender.sendMessage(Component.text("Items have been given successfully!"));
}, Entity("target").onlyPlayers(true), ItemStack("item"), Integer("count").setDefaultValue(() -> 1));
}
}

View file

@ -0,0 +1,76 @@
package me.zen.commands;
import net.kyori.adventure.text.Component;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.command.builder.arguments.number.ArgumentNumber;
import net.minestom.server.command.builder.condition.Conditions;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.entity.Player;
public class HealthCommand extends Command {
public HealthCommand() {
super("health");
setCondition(Conditions::playerOnly);
setDefaultExecutor(this::defaultExecutor);
var modeArg = ArgumentType.Word("mode").from("set", "add");
var valueArg = ArgumentType.Integer("value").between(0, 100);
setArgumentCallback(this::onModeError, modeArg);
setArgumentCallback(this::onValueError, valueArg);
addSyntax(this::sendSuggestionMessage, modeArg);
addSyntax(this::onHealthCommand, modeArg, valueArg);
}
private void defaultExecutor(CommandSender sender, CommandContext context) {
sender.sendMessage(Component.text("Correct usage: health set|add <number>"));
}
private void onModeError(CommandSender sender, ArgumentSyntaxException exception) {
sender.sendMessage(Component.text("SYNTAX ERROR: '" + exception.getInput() + "' should be replaced by 'set' or 'add'"));
}
private void onValueError(CommandSender sender, ArgumentSyntaxException exception) {
final int error = exception.getErrorCode();
final String input = exception.getInput();
switch (error) {
case ArgumentNumber.NOT_NUMBER_ERROR:
sender.sendMessage(Component.text("SYNTAX ERROR: '" + input + "' isn't a number!"));
break;
case ArgumentNumber.TOO_LOW_ERROR:
case ArgumentNumber.TOO_HIGH_ERROR:
sender.sendMessage(Component.text("SYNTAX ERROR: " + input + " is not between 0 and 100"));
break;
}
}
private void sendSuggestionMessage(CommandSender sender, CommandContext context) {
sender.sendMessage(Component.text("/health " + context.get("mode") + " [Integer]"));
}
private void onHealthCommand(CommandSender sender, CommandContext context) {
final Player player = (Player) sender;
final String mode = context.get("mode");
final int value = context.get("value");
switch (mode.toLowerCase()) {
case "set":
player.setHealth(value);
break;
case "add":
player.setHealth(player.getHealth() + value);
break;
}
player.sendMessage(Component.text("You have now " + player.getHealth() + " health"));
}
}

View file

@ -0,0 +1,23 @@
package me.zen.commands;
import net.kyori.adventure.text.Component;
import net.minestom.server.advancements.FrameType;
import net.minestom.server.advancements.notifications.Notification;
import net.minestom.server.advancements.notifications.NotificationCenter;
import net.minestom.server.command.builder.Command;
import net.minestom.server.entity.Player;
import net.minestom.server.item.Material;
import org.jetbrains.annotations.NotNull;
public class NotificationCommand extends Command {
public NotificationCommand() {
super("notification");
setDefaultExecutor((sender, context) -> {
var player = (Player) sender;
var notification = new Notification(Component.text("Hello World!"), FrameType.GOAL, Material.DIAMOND_AXE);
NotificationCenter.send(notification, player);
});
}
}

View file

@ -0,0 +1,35 @@
package me.zen.commands;
import net.kyori.adventure.text.Component;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.entity.Player;
import net.minestom.server.network.ConnectionState;
import java.util.Collection;
import java.util.List;
public class PlayersCommand extends Command {
public PlayersCommand() {
super("players");
setDefaultExecutor(this::usage);
}
private void usage(CommandSender sender, CommandContext context) {
final var players = List.copyOf(MinecraftServer.getConnectionManager().getOnlinePlayers());
final int playerCount = players.size();
sender.sendMessage(Component.text("Total players: " + playerCount));
final int limit = 15;
for (int i = 0; i < Math.min(limit, playerCount); i++) {
final var player = players.get(i);
sender.sendMessage(Component.text(player.getUsername()));
}
if (playerCount > limit) sender.sendMessage(Component.text("..."));
}
}

View file

@ -0,0 +1,18 @@
package me.zen.commands;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.ArgumentLiteral;
import net.minestom.server.command.builder.arguments.ArgumentLoop;
public class RedirectTestCommand extends Command {
public RedirectTestCommand() {
super("redirect");
final ArgumentLiteral a = new ArgumentLiteral("a");
final ArgumentLiteral b = new ArgumentLiteral("b");
final ArgumentLiteral c = new ArgumentLiteral("c");
final ArgumentLiteral d = new ArgumentLiteral("d");
addSyntax(((sender, context) -> {}), new ArgumentLoop<>("test", a,b,c,d));
}
}

View file

@ -0,0 +1,34 @@
package me.zen.commands;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.command.builder.arguments.minecraft.ArgumentEntity;
import net.minestom.server.command.builder.condition.Conditions;
import net.minestom.server.entity.Entity;
import net.minestom.server.utils.entity.EntityFinder;
public class RemoveCommand extends Command {
public RemoveCommand() {
super("remove");
addSubcommand(new RemoveEntities());
}
static class RemoveEntities extends Command {
private final ArgumentEntity entity;
public RemoveEntities() {
super("entities");
setCondition(Conditions::playerOnly);
entity = ArgumentType.Entity("entity");
addSyntax(this::remove, entity);
}
private void remove(CommandSender commandSender, CommandContext commandContext) {
final EntityFinder entityFinder = commandContext.get(entity);
entityFinder.find(commandSender).forEach(Entity::remove);
}
}
}

View file

@ -0,0 +1,43 @@
package me.zen.commands;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* A simple instance save command.
*/
public class SaveCommand extends Command {
public SaveCommand() {
super("save");
addSyntax(this::execute);
}
private void execute(@NotNull CommandSender commandSender, @NotNull CommandContext commandContext) {
commandSender.sendMessage(Component.text("» ", TextColor.color(0x858585), TextDecoration.BOLD)
.append(Component.text("Saving instance...", TextColor.color(0x9595FF))));
saveInstance();
commandSender.sendMessage(Component.text("» ", TextColor.color(0x858585), TextDecoration.BOLD)
.append(Component.text("Instance saved!", TextColor.color(0x9595FF))));
}
static void saveInstance() {
for(var instance : MinecraftServer.getInstanceManager().getInstances()) {
CompletableFuture<Void> instanceSave = instance.saveChunksToStorage().thenCompose(v -> instance.saveChunksToStorage());
try {
instanceSave.get();
} catch (InterruptedException | ExecutionException e) {
MinecraftServer.getExceptionManager().handleException(e);
}
}
}
}

View file

@ -0,0 +1,30 @@
package me.zen.commands;
import me.zen.block.TestBlockHandler;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.minecraft.ArgumentBlockState;
import net.minestom.server.command.builder.arguments.relative.ArgumentRelativeBlockPosition;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.block.Block;
import static net.minestom.server.command.builder.arguments.ArgumentType.BlockState;
import static net.minestom.server.command.builder.arguments.ArgumentType.RelativeBlockPosition;
public class SetBlockCommand extends Command {
public SetBlockCommand() {
super("setblock");
final ArgumentRelativeBlockPosition position = RelativeBlockPosition("position");
final ArgumentBlockState block = BlockState("block");
addSyntax((sender, context) -> {
final Player player = (Player) sender;
Block blockToPlace = context.get(block);
if (blockToPlace.stateId() == Block.GOLD_BLOCK.stateId())
blockToPlace = blockToPlace.withHandler(TestBlockHandler.INSTANCE);
player.getInstance().setBlock(context.get(position).from(player), blockToPlace);
}, position, block);
}
}

View file

@ -0,0 +1,44 @@
package me.zen.commands;
import me.zen.util.MessageHelper;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.format.TextDecorationAndState;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* A simple shutdown command.
*/
public class ShutdownCommand extends Command {
public ShutdownCommand() {
super("shutdown", "stop");
addSyntax(this::execute);
}
private void execute(@NotNull CommandSender commandSender, @NotNull CommandContext commandContext) {
// Save Instance
SaveCommand.saveInstance();
// Kick players
CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS).execute(() -> {
for (Player player : MinecraftServer.getConnectionManager().getOnlinePlayers())
player.kick(Component.text("Server is shutting down", MessageHelper.RED_COLOR));
}
);
// Stop server
CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS).execute(MinecraftServer::stopCleanly);
}
}

View file

@ -0,0 +1,99 @@
package me.zen.commands;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.command.builder.condition.Conditions;
import net.minestom.server.entity.Player;
import net.minestom.server.scoreboard.Sidebar;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class SidebarCommand extends Command {
private final Sidebar sidebar = new Sidebar(Component.text("DEMO").decorate(TextDecoration.BOLD));
private int currentLine = 0;
public SidebarCommand() {
super("sidebar");
addLine("BLANK ", Sidebar.NumberFormat.blank());
addLine("STYLE ", Sidebar.NumberFormat.styled(Component.empty().decorate(TextDecoration.STRIKETHROUGH).color(NamedTextColor.GRAY)));
addLine("FIXED ", Sidebar.NumberFormat.fixed(Component.text("FIXED").color(NamedTextColor.GRAY)));
addLine("NULL ", null);
setDefaultExecutor((source, args) -> source.sendMessage(Component.text("Unknown syntax (note: title must be quoted)")));
setCondition(Conditions::playerOnly);
var option = ArgumentType.Word("option").from("add-line", "remove-line", "set-title", "toggle", "update-content", "update-score");
var content = ArgumentType.String("content").setDefaultValue("");
var targetLine = ArgumentType.Integer("target line").setDefaultValue(-1);
addSyntax(this::handleSidebar, option);
addSyntax(this::handleSidebar, option, content);
addSyntax(this::handleSidebar, option, content, targetLine);
}
private void handleSidebar(CommandSender source, CommandContext context) {
Player player = (Player) source;
String option = context.get("option");
String content = context.get("content");
int targetLine = context.get("target line");
if (targetLine == -1) targetLine = currentLine;
switch (option) {
case "add-line":
addLine(content, null);
break;
case "remove-line":
removeLine();
break;
case "set-title":
setTitle(content);
break;
case "toggle":
toggleSidebar(player);
break;
case "update-content":
updateLineContent(content, String.valueOf(targetLine));
break;
case "update-score":
updateLineScore(Integer.parseInt(content), String.valueOf(targetLine));
break;
}
}
private void addLine(@NotNull String content, @Nullable Sidebar.NumberFormat numberFormat) {
if (currentLine < 16) {
sidebar.createLine(new Sidebar.ScoreboardLine(String.valueOf(currentLine), Component.text(content).color(NamedTextColor.WHITE), currentLine, numberFormat));
currentLine++;
}
}
private void removeLine() {
if (currentLine > 0) {
sidebar.removeLine(String.valueOf(currentLine));
currentLine--;
}
}
private void setTitle(@NotNull String title) {
sidebar.setTitle(Component.text(title).decorate(TextDecoration.BOLD));
}
private void toggleSidebar(Player player) {
if (sidebar.getViewers().contains(player)) sidebar.removeViewer(player);
else sidebar.addViewer(player);
}
private void updateLineContent(@NotNull String content, @NotNull String lineId) {
sidebar.updateLineContent(lineId, Component.text(content).color(NamedTextColor.WHITE));
}
private void updateLineScore(int score, @NotNull String lineId) {
sidebar.updateLineScore(lineId, score);
}
}

View file

@ -0,0 +1,24 @@
package me.zen.commands;
import me.zen.instance.LobbyInstance;
import me.zen.util.MessageHelper;
import net.minestom.server.command.builder.Command;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
public class SpawnCommand extends Command {
public SpawnCommand() {
super("spawn");
setDefaultExecutor((sender, context) -> {
if (!(sender instanceof Player player)) return;
if (player.getInstance() == LobbyInstance.INSTANCE) {
player.teleport(new Pos(0.5, 16, 0.5));
} else {
player.setInstance(LobbyInstance.INSTANCE);
player.teleport(new Pos(0.5, 16, 0.5));
}
MessageHelper.fancyTitle(player, "Lobby");
});
}
}

View file

@ -0,0 +1,22 @@
package me.zen.commands;
import me.zen.entity.ZombieEntity;
import net.minestom.server.command.builder.Command;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.*;
import net.minestom.server.instance.Instance;
public class SummonCommand extends Command {
public SummonCommand() {
super("summon");
setDefaultExecutor((sender, context) -> {
if (sender instanceof Player player) {
final Instance instance = player.getInstance();
final Pos position = player.getPosition();
Entity entity = new ZombieEntity();
entity.setInstance(instance, position);
}
});
}
}

View file

@ -0,0 +1,44 @@
package me.zen.commands;
import net.kyori.adventure.text.Component;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
import net.minestom.server.utils.location.RelativeVec;
public class TeleportCommand extends Command {
public TeleportCommand() {
super("tp");
setDefaultExecutor((source, context) -> source.sendMessage(Component.text("Usage: /tp x y z")));
var posArg = ArgumentType.RelativeVec3("pos");
var playerArg = ArgumentType.Word("player");
addSyntax(this::onPlayerTeleport, playerArg);
addSyntax(this::onPositionTeleport, posArg);
}
private void onPlayerTeleport(CommandSender sender, CommandContext context) {
final String playerName = context.get("player");
Player pl = MinecraftServer.getConnectionManager().getOnlinePlayerByUsername(playerName);
if (sender instanceof Player player) {
player.teleport(pl.getPosition());
}
sender.sendMessage(Component.text("Teleported to player " + playerName));
}
private void onPositionTeleport(CommandSender sender, CommandContext context) {
final Player player = (Player) sender;
final RelativeVec relativeVec = context.get("pos");
final Pos position = player.getPosition().withCoord(relativeVec.from(player));
player.teleport(position);
player.sendMessage(Component.text("You have been teleported to " + position));
}
}

View file

@ -0,0 +1,25 @@
package me.zen.commands;
import me.zen.util.MessageHelper;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import org.jetbrains.annotations.NotNull;
public class TestMessageHelper extends Command {
public TestMessageHelper() {
super("testmsg");
addSyntax(this::execute);
}
private void execute(@NotNull CommandSender commandSender, @NotNull CommandContext commandContext) {
MessageHelper.info(commandSender, "Test message!");
MessageHelper.warn(commandSender, "Test message!");
MessageHelper.countdown(commandSender, 5).thenAccept(v -> {
MessageHelper.fancyTitle(commandSender, "Hello :>");
});
}
}

View file

@ -0,0 +1,29 @@
package me.zen.commands;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.title.Title;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.command.builder.condition.Conditions;
import net.minestom.server.entity.Player;
public class TitleCommand extends Command {
public TitleCommand() {
super("title");
setDefaultExecutor((source, args) -> source.sendMessage(Component.text("Unknown syntax (note: title must be quoted)")));
setCondition(Conditions::playerOnly);
var content = ArgumentType.String("content");
addSyntax(this::handleTitle, content);
}
private void handleTitle(CommandSender source, CommandContext context) {
Player player = (Player) source;
String titleContent = context.get("content");
player.showTitle(Title.title(Component.text(titleContent), Component.empty(), Title.DEFAULT_TIMES));
}
}

View file

@ -0,0 +1,44 @@
package me.zen.commands;
import me.zen.instance.LobbyInstance;
import me.zen.instance.VanillaInstance;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.command.builder.condition.Conditions;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
public class WorldCommand extends Command {
public WorldCommand() {
super("world");
setCondition(Conditions::playerOnly);
setDefaultExecutor((source, args) -> source.sendMessage("Usage: /world <terrain|flat|void>"));
var option = ArgumentType.Word("option").from("terrain", "flat", "void");
addSyntax(this::execute, option);
}
private void execute(CommandSender source, CommandContext context) {
Player player = (Player) source;
String option = context.get("option");
switch (option) {
case "terrain":
player.setInstance(VanillaInstance.INSTANCE);
player.setRespawnPoint(new Pos(0.5, 70, 0.5));
break;
case "flat":
player.setInstance(LobbyInstance.INSTANCE);
player.setRespawnPoint(new Pos(0.5, 70, 0.5));
break;
case "void":
player.setInstance(LobbyInstance.INSTANCE);
player.setRespawnPoint(new Pos(0.5, 70, 0.5));
break;
}
}
}

View file

@ -0,0 +1,24 @@
package me.zen.config;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.List;
public record Config(Server server, Proxy proxy, Permissions permissions, Prometheus prometheus) {
public record Server(@Default("0.0.0.0") String host, @Default("25565") int port, @Default("true") boolean mojangAuth, @Default("[\"Line1\",\"Line2\"]") List<String> motd) {
public SocketAddress address() {
return new InetSocketAddress(host, port);
}
}
public record Proxy(@Default("false") boolean enabled, @Default("forwarding-secret") String secret) {
@Override
public String toString() {
return "Proxy[enabled="+enabled+", secret=<hidden>]";
}
}
public record Permissions(@Default("[]") List<String> operators) {}
public record Prometheus(@Default("false") boolean enabled, @Default("9090") int port) {}
}

View file

@ -0,0 +1,174 @@
package me.zen.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import net.minestom.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Parameter;
import java.lang.reflect.RecordComponent;
import java.util.*;
public final class ConfigHandler {
public volatile static Config CONFIG;
private static boolean reload = false;
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigHandler.class);
private static final Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapterFactory(new RecordTypeAdapterFactory())
.create();
private static final File configFile = new File("config.json");
static {
loadConfig();
}
public synchronized static void loadConfig() {
Config old = CONFIG;
if (configFile.exists()) {
try (JsonReader reader = new JsonReader(new FileReader(configFile))) {
CONFIG = gson.fromJson(reader, Config.class);
} catch (IOException exception) {
LOGGER.error("Failed to load configuration file, using defaults.", exception);
loadDefaults();
}
} else {
loadDefaults();
try {
final FileWriter writer = new FileWriter(configFile);
gson.toJson(CONFIG, writer);
writer.flush();
writer.close();
} catch (IOException exception) {
LOGGER.error("Failed to write default configuration.", exception);
}
}
if (reload) {
MinecraftServer.getGlobalEventHandler().call(new ConfigurationReloadedEvent(old, CONFIG));
LOGGER.info("Configuration reloaded!");
} else {
reload = true;
}
}
private synchronized static void loadDefaults() {
CONFIG = gson.fromJson("{}", Config.class);
}
private ConfigHandler() {}
private static class RecordTypeAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final Class<? super T> clazz = type.getRawType();
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
if (!clazz.isRecord())
return null;
return new TypeAdapter<>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
@SuppressWarnings("unchecked")
@Override
public T read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
} else {
final RecordComponent[] recordComponents = clazz.getRecordComponents();
final Map<String, TypeToken<?>> typeMap = new HashMap<>();
final Map<String, Object> argsMap = new HashMap<>();
for (RecordComponent component : recordComponents)
typeMap.put(component.getName(), TypeToken.get(component.getGenericType()));
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
argsMap.put(name, gson.getAdapter(typeMap.get(name)).read(reader));
}
reader.endObject();
Arrays.stream(recordComponents).filter(x -> !argsMap.containsKey(x.getName())).forEach(x -> {
final String name = x.getName();
final Class<?> argClazz = x.getType();
final Default def = x.getAnnotation(Default.class);
if (def == null) {
argsMap.put(name, instantiateWithDefaults(argClazz));
return;
}
try {
if (argClazz == String.class) {
argsMap.put(name, def.value());
} else {
argsMap.put(name, gson.getAdapter(typeMap.get(name)).fromJson(def.value()));
}
} catch (IOException ignored) {}
});
final List<Object> args = new ArrayList<>();
final List<Class<?>> argTypes = new ArrayList<>();
for (RecordComponent component : recordComponents) {
args.add(argsMap.get(component.getName()));
argTypes.add(component.getType());
}
try {
Constructor<? super T> constructor = clazz.getDeclaredConstructor(argTypes.toArray(Class<?>[]::new));
constructor.setAccessible(true);
return (T) constructor.newInstance(args.toArray(Object[]::new));
} catch (ReflectiveOperationException e) {
return null;
}
}
}
private Object instantiateWithDefaults(Class<?> clazz) {
final List<Object> args = new ArrayList<>();
final Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
for (Parameter param : constructor.getParameters()) {
final Class<?> paramClazz = param.getType();
final Default def = param.getAnnotation(Default.class);
if (def == null) {
args.add(instantiateWithDefaults(paramClazz));
continue;
}
try {
if (paramClazz == String.class) {
args.add(def.value());
} else {
args.add(gson.getAdapter(TypeToken.get(param.getType())).fromJson(def.value()));
}
} catch (IOException ignored) {
args.add(null);
}
}
try {
return constructor.newInstance(args.toArray(Object[]::new));
} catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
return null;
}
}
};
}
}
}

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