Code commit
This commit is contained in:
parent
a166622f44
commit
a61f3b669b
133 changed files with 7112 additions and 0 deletions
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal 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
10
.idea/.gitignore
vendored
Normal 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
7
.idea/discord.xml
Normal 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
17
.idea/gradle.xml
Normal 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
10
.idea/misc.xml
Normal 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
124
.idea/uiDesigner.xml
Normal 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
6
.idea/vcs.xml
Normal 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
BIN
Lobby/region/r.-1.-1.mca
Normal file
Binary file not shown.
BIN
Lobby/region/r.-1.0.mca
Normal file
BIN
Lobby/region/r.-1.0.mca
Normal file
Binary file not shown.
BIN
Lobby/region/r.0.-1.mca
Normal file
BIN
Lobby/region/r.0.-1.mca
Normal file
Binary file not shown.
BIN
Lobby/region/r.0.0.mca
Normal file
BIN
Lobby/region/r.0.0.mca
Normal file
Binary file not shown.
14
arena/Icons.java
Normal file
14
arena/Icons.java
Normal 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
27
arena/Items.java
Normal 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
135
arena/Main.java
Normal 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
56
arena/Messenger.java
Normal 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
156
arena/Metrics.java
Normal 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
51
arena/ServerList.java
Normal 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
53
arena/SimpleCommands.java
Normal 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
24
arena/config/Config.java
Normal 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) {}
|
||||
}
|
174
arena/config/ConfigHandler.java
Normal file
174
arena/config/ConfigHandler.java
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
6
arena/config/ConfigurationReloadedEvent.java
Normal file
6
arena/config/ConfigurationReloadedEvent.java
Normal 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 {
|
||||
}
|
9
arena/config/Default.java
Normal file
9
arena/config/Default.java
Normal 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();
|
||||
}
|
55
arena/feature/BowFeature.java
Normal file
55
arena/feature/BowFeature.java
Normal 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());
|
||||
}
|
||||
}
|
122
arena/feature/CombatFeature.java
Normal file
122
arena/feature/CombatFeature.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
30
arena/feature/DropFeature.java
Normal file
30
arena/feature/DropFeature.java
Normal 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));
|
||||
});
|
||||
}
|
||||
}
|
10
arena/feature/Feature.java
Normal file
10
arena/feature/Feature.java
Normal 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);
|
||||
}
|
27
arena/feature/Features.java
Normal file
27
arena/feature/Features.java
Normal 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);
|
||||
}
|
||||
}
|
38
arena/feature/FunctionalItemFeature.java
Normal file
38
arena/feature/FunctionalItemFeature.java
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
209
arena/game/AbstractArena.java
Normal file
209
arena/game/AbstractArena.java
Normal 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
21
arena/game/Arena.java
Normal 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);
|
||||
}
|
||||
}
|
161
arena/game/ArenaCommand.java
Normal file
161
arena/game/ArenaCommand.java
Normal 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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
53
arena/game/ArenaManager.java
Normal file
53
arena/game/ArenaManager.java
Normal 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);
|
||||
}
|
||||
}
|
21
arena/game/ArenaOption.java
Normal file
21
arena/game/ArenaOption.java
Normal 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
68
arena/game/ArenaType.java
Normal 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
122
arena/game/Generator.java
Normal 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();
|
||||
}
|
||||
}
|
101
arena/game/GeneratorImpl.java
Normal file
101
arena/game/GeneratorImpl.java
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
53
arena/game/SingleInstanceArena.java
Normal file
53
arena/game/SingleInstanceArena.java
Normal 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);
|
||||
}
|
||||
}
|
28
arena/game/mob/ArenaClass.java
Normal file
28
arena/game/mob/ArenaClass.java
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
17
arena/game/mob/ArenaMinion.java
Normal file
17
arena/game/mob/ArenaMinion.java
Normal 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;
|
||||
}
|
||||
}
|
41
arena/game/mob/ArenaUpgrade.java
Normal file
41
arena/game/mob/ArenaUpgrade.java
Normal 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));
|
||||
}
|
||||
}
|
122
arena/game/mob/BlazeMob.java
Normal file
122
arena/game/mob/BlazeMob.java
Normal 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() {}
|
||||
}
|
||||
}
|
85
arena/game/mob/EndermanMob.java
Normal file
85
arena/game/mob/EndermanMob.java
Normal 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() {}
|
||||
}
|
||||
}
|
111
arena/game/mob/EvokerMob.java
Normal file
111
arena/game/mob/EvokerMob.java
Normal 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
41
arena/game/mob/Kit.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
76
arena/game/mob/MobArenaInstance.java
Normal file
76
arena/game/mob/MobArenaInstance.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
46
arena/game/mob/MobArenaSidebarDisplay.java
Normal file
46
arena/game/mob/MobArenaSidebarDisplay.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
101
arena/game/mob/MobTestCommand.java
Normal file
101
arena/game/mob/MobTestCommand.java
Normal 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();
|
||||
}
|
||||
}
|
177
arena/game/mob/NextStageInventory.java
Normal file
177
arena/game/mob/NextStageInventory.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
arena/game/mob/NextStageNPC.java
Normal file
10
arena/game/mob/NextStageNPC.java
Normal 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);
|
||||
}
|
||||
}
|
65
arena/game/mob/SkeletonMob.java
Normal file
65
arena/game/mob/SkeletonMob.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
110
arena/game/mob/SpiderMob.java
Normal file
110
arena/game/mob/SpiderMob.java
Normal 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
29
arena/group/Group.java
Normal 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();
|
||||
}
|
||||
}
|
168
arena/group/GroupCommand.java
Normal file
168
arena/group/GroupCommand.java
Normal 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));
|
||||
}
|
||||
}
|
12
arena/group/GroupEvent.java
Normal file
12
arena/group/GroupEvent.java
Normal 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()));
|
||||
}
|
||||
}
|
99
arena/group/GroupImpl.java
Normal file
99
arena/group/GroupImpl.java
Normal 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();
|
||||
}
|
||||
}
|
63
arena/group/GroupManager.java
Normal file
63
arena/group/GroupManager.java
Normal 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;
|
||||
}
|
||||
}
|
6
arena/group/displays/GroupDisplay.java
Normal file
6
arena/group/displays/GroupDisplay.java
Normal file
|
@ -0,0 +1,6 @@
|
|||
package net.minestom.arena.group.displays;
|
||||
|
||||
public interface GroupDisplay {
|
||||
void update();
|
||||
default void clean() {}
|
||||
}
|
88
arena/group/displays/GroupSidebarDisplay.java
Normal file
88
arena/group/displays/GroupSidebarDisplay.java
Normal 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
55
arena/lobby/Lobby.java
Normal 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);
|
||||
}
|
||||
}
|
33
arena/lobby/LobbySidebarDisplay.java
Normal file
33
arena/lobby/LobbySidebarDisplay.java
Normal 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
82
arena/lobby/Map.java
Normal 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
167
arena/lobby/NPC.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
25
arena/utils/CommandUtils.java
Normal file
25
arena/utils/CommandUtils.java
Normal 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;
|
||||
}
|
||||
}
|
69
arena/utils/ConcurrentUtils.java
Normal file
69
arena/utils/ConcurrentUtils.java
Normal 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);
|
||||
}
|
||||
}
|
15
arena/utils/FullbrightDimension.java
Normal file
15
arena/utils/FullbrightDimension.java
Normal 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);
|
||||
}
|
||||
}
|
38
arena/utils/ItemUtils.java
Normal file
38
arena/utils/ItemUtils.java
Normal 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
20
build.gradle
Normal 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
22
config.json
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
234
gradlew
vendored
Normal 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
89
gradlew.bat
vendored
Normal 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
2
settings.gradle
Normal file
|
@ -0,0 +1,2 @@
|
|||
rootProject.name = 'MinestomServ'
|
||||
|
175
src/main/java/me/zen/Initialization.java
Normal file
175
src/main/java/me/zen/Initialization.java
Normal 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));
|
||||
}
|
||||
}
|
82
src/main/java/me/zen/Main.java
Normal file
82
src/main/java/me/zen/Main.java
Normal 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));
|
||||
}
|
||||
}
|
153
src/main/java/me/zen/Metrics.java
Normal file
153
src/main/java/me/zen/Metrics.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
70
src/main/java/me/zen/block/CampfireHandler.java
Normal file
70
src/main/java/me/zen/block/CampfireHandler.java
Normal 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");
|
||||
}
|
||||
}
|
24
src/main/java/me/zen/block/TestBlockHandler.java
Normal file
24
src/main/java/me/zen/block/TestBlockHandler.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
100
src/main/java/me/zen/commands/DisplayCommand.java
Normal file
100
src/main/java/me/zen/commands/DisplayCommand.java
Normal 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();
|
||||
}
|
||||
}
|
53
src/main/java/me/zen/commands/FindCommand.java
Normal file
53
src/main/java/me/zen/commands/FindCommand.java
Normal 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("");
|
||||
}
|
||||
}
|
||||
}
|
131
src/main/java/me/zen/commands/GamemodeCommand.java
Normal file
131
src/main/java/me/zen/commands/GamemodeCommand.java
Normal 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);
|
||||
}
|
||||
}
|
57
src/main/java/me/zen/commands/GiveCommand.java
Normal file
57
src/main/java/me/zen/commands/GiveCommand.java
Normal 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));
|
||||
|
||||
}
|
||||
}
|
76
src/main/java/me/zen/commands/HealthCommand.java
Normal file
76
src/main/java/me/zen/commands/HealthCommand.java
Normal 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"));
|
||||
}
|
||||
|
||||
}
|
23
src/main/java/me/zen/commands/NotificationCommand.java
Normal file
23
src/main/java/me/zen/commands/NotificationCommand.java
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
35
src/main/java/me/zen/commands/PlayersCommand.java
Normal file
35
src/main/java/me/zen/commands/PlayersCommand.java
Normal 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("..."));
|
||||
}
|
||||
|
||||
}
|
18
src/main/java/me/zen/commands/RedirectTestCommand.java
Normal file
18
src/main/java/me/zen/commands/RedirectTestCommand.java
Normal 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));
|
||||
}
|
||||
}
|
34
src/main/java/me/zen/commands/RemoveCommand.java
Normal file
34
src/main/java/me/zen/commands/RemoveCommand.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
43
src/main/java/me/zen/commands/SaveCommand.java
Normal file
43
src/main/java/me/zen/commands/SaveCommand.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
src/main/java/me/zen/commands/SetBlockCommand.java
Normal file
30
src/main/java/me/zen/commands/SetBlockCommand.java
Normal 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);
|
||||
}
|
||||
}
|
44
src/main/java/me/zen/commands/ShutdownCommand.java
Normal file
44
src/main/java/me/zen/commands/ShutdownCommand.java
Normal 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);
|
||||
}
|
||||
}
|
99
src/main/java/me/zen/commands/SidebarCommand.java
Normal file
99
src/main/java/me/zen/commands/SidebarCommand.java
Normal 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);
|
||||
}
|
||||
}
|
24
src/main/java/me/zen/commands/SpawnCommand.java
Normal file
24
src/main/java/me/zen/commands/SpawnCommand.java
Normal 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");
|
||||
});
|
||||
}
|
||||
}
|
22
src/main/java/me/zen/commands/SummonCommand.java
Normal file
22
src/main/java/me/zen/commands/SummonCommand.java
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
44
src/main/java/me/zen/commands/TeleportCommand.java
Normal file
44
src/main/java/me/zen/commands/TeleportCommand.java
Normal 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));
|
||||
}
|
||||
}
|
25
src/main/java/me/zen/commands/TestMessageHelper.java
Normal file
25
src/main/java/me/zen/commands/TestMessageHelper.java
Normal 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 :>");
|
||||
});
|
||||
}
|
||||
}
|
29
src/main/java/me/zen/commands/TitleCommand.java
Normal file
29
src/main/java/me/zen/commands/TitleCommand.java
Normal 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));
|
||||
}
|
||||
}
|
44
src/main/java/me/zen/commands/WorldCommand.java
Normal file
44
src/main/java/me/zen/commands/WorldCommand.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
24
src/main/java/me/zen/config/Config.java
Normal file
24
src/main/java/me/zen/config/Config.java
Normal 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) {}
|
||||
}
|
174
src/main/java/me/zen/config/ConfigHandler.java
Normal file
174
src/main/java/me/zen/config/ConfigHandler.java
Normal 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
Loading…
Reference in a new issue