Start Pre-release 26.1.2
some patches still not applied, need further update
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
package fun.bm.lophine.command.counter;
|
||||
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import fun.bm.lophine.command.counter.sub.DisplayCommand;
|
||||
import fun.bm.lophine.command.counter.sub.ResetCommand;
|
||||
import fun.bm.lophine.command.counter.sub.ToggleCommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.JoinConfiguration;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.CommandContext;
|
||||
import org.leavesmc.leaves.command.RootNode;
|
||||
import org.leavesmc.leaves.util.HopperCounter;
|
||||
|
||||
public class CounterCommand extends RootNode {
|
||||
private final String PERM_BASE;
|
||||
|
||||
public CounterCommand() {
|
||||
super("counter", "lophine.commands.counter");
|
||||
this.PERM_BASE = "lophine.commands.counter";
|
||||
children(
|
||||
new ToggleCommand(this),
|
||||
new ResetCommand(this),
|
||||
new DisplayCommand(this)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException {
|
||||
context.getSender().sendMessage(Component.join(JoinConfiguration.noSeparators(),
|
||||
Component.text("Hopper Counter: ", NamedTextColor.GRAY),
|
||||
Component.text(HopperCounter.isEnabled(), HopperCounter.isEnabled() ? NamedTextColor.AQUA : NamedTextColor.GRAY)
|
||||
));
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean hasPermission(@NotNull CommandSender sender, String... subcommand) {
|
||||
return hasPermission(PERM_BASE, sender, subcommand);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package fun.bm.lophine.command.counter;
|
||||
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.LiteralNode;
|
||||
|
||||
public class CounterSubCommand extends LiteralNode {
|
||||
protected final CounterCommand parent;
|
||||
|
||||
protected CounterSubCommand(String name, CounterCommand parent) {
|
||||
super(name);
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requires(@NotNull CommandSourceStack source) {
|
||||
return hasPermission(source.getSender());
|
||||
}
|
||||
|
||||
protected boolean hasPermission(CommandSender sender) {
|
||||
return parent.hasPermission(sender, this.name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package fun.bm.lophine.command.counter.sub;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import fun.bm.lophine.command.counter.CounterCommand;
|
||||
import fun.bm.lophine.command.counter.CounterSubCommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.ArgumentNode;
|
||||
import org.leavesmc.leaves.command.CommandContext;
|
||||
import org.leavesmc.leaves.util.HopperCounter;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class DisplayCommand extends CounterSubCommand {
|
||||
public DisplayCommand(CounterCommand parent) {
|
||||
super("display", parent);
|
||||
children(
|
||||
DyeColorArg::new
|
||||
);
|
||||
}
|
||||
|
||||
public static void displayCounter(CommandContext context, @NotNull HopperCounter counter, boolean realTime) {
|
||||
for (Component component : counter.format(MinecraftServer.getServer(), realTime)) {
|
||||
context.getSender().sendMessage(component);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DyeColorArg extends ArgumentNode<String> {
|
||||
protected DyeColorArg() {
|
||||
super("color", StringArgumentType.string());
|
||||
children(
|
||||
TimeArg::new
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) {
|
||||
String color0 = context.getArgument(DyeColorArg.class);
|
||||
DyeColor color = DyeColor.byName(color0, null);
|
||||
if (color == null) return true;
|
||||
HopperCounter counter = HopperCounter.getCounter(color);
|
||||
displayCounter(context, counter, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Suggestions> getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
|
||||
String path = context.getArgumentOrDefault(DyeColorArg.class, "");
|
||||
for (DyeColor value : DyeColor.values()) {
|
||||
String color = value.getName();
|
||||
if (color.startsWith(path)) {
|
||||
builder.suggest(color);
|
||||
}
|
||||
}
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
private static class TimeArg extends ArgumentNode<String> {
|
||||
protected TimeArg() {
|
||||
super("time", StringArgumentType.string());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) {
|
||||
String color0 = context.getArgument(DyeColorArg.class);
|
||||
DyeColor color = DyeColor.byName(color0, null);
|
||||
if (color == null) return true;
|
||||
HopperCounter counter = HopperCounter.getCounter(color);
|
||||
String timeType = context.getArgument(TimeArg.class);
|
||||
switch (timeType) {
|
||||
case "realtime" -> displayCounter(context, counter, true);
|
||||
case "gametick" -> displayCounter(context, counter, false);
|
||||
default ->
|
||||
context.getSender().sendMessage(Component.text("Invalid time type: " + timeType, NamedTextColor.RED));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected CompletableFuture<Suggestions> getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
|
||||
builder.suggest("realtime");
|
||||
builder.suggest("gametick");
|
||||
return builder.buildFuture();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package fun.bm.lophine.command.counter.sub;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import fun.bm.lophine.command.counter.CounterCommand;
|
||||
import fun.bm.lophine.command.counter.CounterSubCommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.JoinConfiguration;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.ArgumentNode;
|
||||
import org.leavesmc.leaves.command.CommandContext;
|
||||
import org.leavesmc.leaves.util.HopperCounter;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class ResetCommand extends CounterSubCommand {
|
||||
public ResetCommand(CounterCommand parent) {
|
||||
super("reset", parent);
|
||||
children(
|
||||
DyeColorArg::new
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException {
|
||||
HopperCounter.resetAll(MinecraftServer.getServer(), false);
|
||||
context.getSender().sendMessage(Component.text("Restarted all counters."));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static class DyeColorArg extends ArgumentNode<String> {
|
||||
protected DyeColorArg() {
|
||||
super("color", StringArgumentType.string());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) {
|
||||
String color0 = context.getArgument(DyeColorArg.class);
|
||||
if (color0.equals("all")) {
|
||||
HopperCounter.resetAll(MinecraftServer.getServer(), false);
|
||||
context.getSender().sendMessage(Component.text("Restarted all counters."));
|
||||
return true;
|
||||
}
|
||||
DyeColor color = DyeColor.byName(color0, null);
|
||||
if (color == null) return true;
|
||||
HopperCounter counter = HopperCounter.getCounter(color);
|
||||
counter.reset(MinecraftServer.getServer());
|
||||
context.getSender().sendMessage(Component.join(JoinConfiguration.noSeparators(),
|
||||
Component.text("Restarted "),
|
||||
Component.text(color.getName(), TextColor.color(color.getTextColor())),
|
||||
Component.text(" counter.")
|
||||
));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Suggestions> getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
|
||||
String path = context.getArgumentOrDefault(DyeColorArg.class, "");
|
||||
if ("all".startsWith(path)) {
|
||||
builder.suggest("all");
|
||||
}
|
||||
for (DyeColor value : DyeColor.values()) {
|
||||
String color = value.getName();
|
||||
if (color.startsWith(path)) {
|
||||
builder.suggest(color);
|
||||
}
|
||||
}
|
||||
return builder.buildFuture();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package fun.bm.lophine.command.counter.sub;
|
||||
|
||||
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import fun.bm.lophine.command.counter.CounterCommand;
|
||||
import fun.bm.lophine.command.counter.CounterSubCommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.ArgumentNode;
|
||||
import org.leavesmc.leaves.command.CommandContext;
|
||||
import org.leavesmc.leaves.util.HopperCounter;
|
||||
|
||||
public class ToggleCommand extends CounterSubCommand {
|
||||
public ToggleCommand(CounterCommand parent) {
|
||||
super("toggle", parent);
|
||||
children(
|
||||
BooleanArg::new
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException {
|
||||
if (!HopperCounter.isEnabled()) {
|
||||
HopperCounter.setEnabled(true);
|
||||
context.getSender().sendMessage(Component.text("Hopper Counter now is enabled.", NamedTextColor.AQUA));
|
||||
} else {
|
||||
HopperCounter.setEnabled(false);
|
||||
context.getSender().sendMessage(Component.text("Hopper Counter now is disabled.", NamedTextColor.RED));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static class BooleanArg extends ArgumentNode<Boolean> {
|
||||
protected BooleanArg() {
|
||||
super("enabled", BoolArgumentType.bool());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) {
|
||||
boolean enabled = context.getArgument(BooleanArg.class);
|
||||
if (enabled == HopperCounter.isEnabled()) {
|
||||
context.getSender().sendMessage(Component.text("Hopper Counter is already " + (enabled ? "enabled" : "disabled") + ".", NamedTextColor.GRAY));
|
||||
} else {
|
||||
HopperCounter.setEnabled(enabled);
|
||||
if (enabled) {
|
||||
context.getSender().sendMessage(Component.text("Hopper Counter now is enabled.", NamedTextColor.AQUA));
|
||||
} else {
|
||||
context.getSender().sendMessage(Component.text("Hopper Counter now is disabled.", NamedTextColor.RED));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package fun.bm.lophine.config.modules.function;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import me.earthme.luminol.config.IConfigModule;
|
||||
import me.earthme.luminol.config.flags.ConfigClassInfo;
|
||||
import me.earthme.luminol.config.flags.ConfigInfo;
|
||||
import me.earthme.luminol.enums.EnumConfigCategory;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.leavesmc.leaves.protocol.CarpetServerProtocol;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@ConfigClassInfo(name = "creative_fly_no_clip", category = EnumConfigCategory.FUNCTION)
|
||||
public class CreativeFlyNoClipConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled", comments = """
|
||||
Whether to enable creative fly no clip.
|
||||
When enabled, players in creative mode will not collide with blocks while flying.
|
||||
This allows them to pass through blocks without obstruction.""")
|
||||
public static boolean enabled = false;
|
||||
|
||||
@Override
|
||||
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> e) {
|
||||
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "creativeNoClip", enabled));
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package fun.bm.lophine.config.modules.function;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import fun.bm.lophine.utils.RandomProfilePool;
|
||||
import me.earthme.luminol.config.IConfigModule;
|
||||
import me.earthme.luminol.config.flags.ConfigClassInfo;
|
||||
import me.earthme.luminol.config.flags.ConfigInfo;
|
||||
import me.earthme.luminol.config.flags.HotReloadUnsupported;
|
||||
import me.earthme.luminol.enums.EnumConfigCategory;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "replay-api")
|
||||
public class ReplayAPIConfig implements IConfigModule {
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "enable-cache")
|
||||
public static boolean enableCache = true;
|
||||
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "cache-photographer-time", comments = """
|
||||
Time to cache photographer profile(in seconds)""")
|
||||
public static int cachePhotographerTime = 3600;
|
||||
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "cache-photographer-size", comments = """
|
||||
Maximum size of cache photographer profile""")
|
||||
public static int cachePhotographerSize = 100;
|
||||
|
||||
@Override
|
||||
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> e) {
|
||||
RandomProfilePool.init();
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package fun.bm.lophine.config.modules.function;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import fun.bm.lophine.command.counter.CounterCommand;
|
||||
import me.earthme.luminol.config.IConfigModule;
|
||||
import me.earthme.luminol.config.flags.ConfigClassInfo;
|
||||
import me.earthme.luminol.config.flags.ConfigInfo;
|
||||
import me.earthme.luminol.config.flags.DoNotLoad;
|
||||
import me.earthme.luminol.enums.EnumConfigCategory;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "wool-hopper-counter")
|
||||
public class WoolHopperCounterConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled")
|
||||
public static boolean enabled = false;
|
||||
|
||||
@ConfigInfo(name = "unlimited-speed")
|
||||
public static boolean unlimitedSpeed = false;
|
||||
|
||||
@DoNotLoad
|
||||
private static CounterCommand counterCommand = null;
|
||||
|
||||
@Override
|
||||
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> exs) {
|
||||
if (enabled) {
|
||||
if (counterCommand == null) {
|
||||
counterCommand = new CounterCommand();
|
||||
}
|
||||
counterCommand.register();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnloaded(CommentedFileConfig configInstance) {
|
||||
if (counterCommand != null) {
|
||||
counterCommand.unregister();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package fun.bm.lophine.utils;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import fun.bm.lophine.config.modules.function.ReplayAPIConfig;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class RandomProfilePool {
|
||||
private static int lastUsedId = 0;
|
||||
private static final Object lock = new Object();
|
||||
private static Cache<Integer, GameProfile> cache;
|
||||
private static final Object lockCache = new Object();
|
||||
|
||||
public static void init() {
|
||||
if (ReplayAPIConfig.enableCache) {
|
||||
cache = CacheBuilder.newBuilder()
|
||||
.maximumSize(ReplayAPIConfig.cachePhotographerSize)
|
||||
.expireAfterWrite(ReplayAPIConfig.cachePhotographerTime, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
public static GameProfile getRandomProfile(String id) {
|
||||
if (ReplayAPIConfig.enableCache) {
|
||||
synchronized (lockCache) {
|
||||
for (Map.Entry<Integer, GameProfile> entry : cache.asMap().entrySet()) {
|
||||
int key = entry.getKey();
|
||||
cache.invalidate(key);
|
||||
GameProfile gp = entry.getValue();
|
||||
return new GameProfile(gp.id(), id, gp.properties());
|
||||
}
|
||||
}
|
||||
}
|
||||
return new GameProfile(UUID.randomUUID(), id);
|
||||
}
|
||||
|
||||
public static void putProfile(GameProfile profile) {
|
||||
if (ReplayAPIConfig.enableCache) {
|
||||
synchronized (lockCache) {
|
||||
cache.put(getNextId(), profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int getNextId() {
|
||||
synchronized (lock) {
|
||||
int newId = lastUsedId + 1;
|
||||
while (cache.getIfPresent(newId) != null) {
|
||||
newId++;
|
||||
}
|
||||
lastUsedId = newId;
|
||||
return newId;
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.entity.photographer;
|
||||
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import org.bukkit.craftbukkit.CraftServer;
|
||||
import org.bukkit.craftbukkit.entity.CraftPlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.leavesmc.leaves.replay.ServerPhotographer;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class CraftPhotographer extends CraftPlayer implements Photographer {
|
||||
|
||||
public CraftPhotographer(CraftServer server, ServerPhotographer entity) {
|
||||
super(server, entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopRecording() {
|
||||
this.stopRecording(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopRecording(boolean async) {
|
||||
this.stopRecording(async, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopRecording(boolean async, boolean save) {
|
||||
this.getHandle().remove(async, save);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pauseRecording() {
|
||||
this.getHandle().pauseRecording();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resumeRecording() {
|
||||
this.getHandle().resumeRecording();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRecordFile(@NotNull File file) {
|
||||
this.getHandle().setSaveFile(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFollowPlayer(@Nullable Player player) {
|
||||
ServerPlayer serverPlayer = player != null ? ((CraftPlayer) player).getHandle() : null;
|
||||
this.getHandle().setFollowPlayer(serverPlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getId() {
|
||||
return this.getHandle().createState.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerPhotographer getHandle() {
|
||||
return (ServerPhotographer) entity;
|
||||
}
|
||||
|
||||
public void setHandle(final ServerPhotographer entity) {
|
||||
super.setHandle(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CraftPhotographer{" + "name=" + getName() + '}';
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.entity.photographer;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.bukkit.Location;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.leavesmc.leaves.replay.BukkitRecorderOption;
|
||||
import org.leavesmc.leaves.replay.RecorderOption;
|
||||
import org.leavesmc.leaves.replay.ServerPhotographer;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
public class CraftPhotographerManager implements PhotographerManager {
|
||||
|
||||
private final Collection<Photographer> photographerViews = Collections.unmodifiableList(Lists.transform(ServerPhotographer.getPhotographers(), ServerPhotographer::getBukkitPlayer));
|
||||
|
||||
@Override
|
||||
public @Nullable Photographer getPhotographer(@NotNull UUID uuid) {
|
||||
ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid);
|
||||
if (photographer != null) {
|
||||
return photographer.getBukkitPlayer();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Photographer getPhotographer(@NotNull String id) {
|
||||
ServerPhotographer photographer = ServerPhotographer.getPhotographer(id);
|
||||
if (photographer != null) {
|
||||
return photographer.getBukkitPlayer();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location) {
|
||||
ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createDefaultOption()).createSync();
|
||||
if (photographer != null) {
|
||||
return photographer.getBukkitPlayer();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location, @NotNull BukkitRecorderOption recorderOption) {
|
||||
ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createFromBukkit(recorderOption)).createSync();
|
||||
if (photographer != null) {
|
||||
return photographer.getBukkitPlayer();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePhotographer(@NotNull String id) {
|
||||
ServerPhotographer photographer = ServerPhotographer.getPhotographer(id);
|
||||
if (photographer != null) {
|
||||
photographer.remove(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePhotographer(@NotNull UUID uuid) {
|
||||
ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid);
|
||||
if (photographer != null) {
|
||||
photographer.remove(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAllPhotographers() {
|
||||
for (ServerPhotographer photographer : ServerPhotographer.getPhotographers()) {
|
||||
photographer.remove(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Photographer> getPhotographers() {
|
||||
return photographerViews;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.replay;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.zip.Checksum;
|
||||
|
||||
public class DigestOutputStream extends OutputStream {
|
||||
|
||||
private final Checksum sum;
|
||||
private final OutputStream out;
|
||||
|
||||
public DigestOutputStream(OutputStream out, Checksum sum) {
|
||||
this.out = out;
|
||||
this.sum = sum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
out.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
out.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
sum.update(b);
|
||||
out.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte @NotNull [] b) throws IOException {
|
||||
sum.update(b);
|
||||
out.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte @NotNull [] b, int off, int len) throws IOException {
|
||||
sum.update(b, off, len);
|
||||
out.write(b, off, len);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.replay;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class RecordMetaData {
|
||||
|
||||
public static final int CURRENT_FILE_FORMAT_VERSION = 14;
|
||||
|
||||
public boolean singleplayer = false;
|
||||
public String serverName = "Lophine";
|
||||
public int duration = 0;
|
||||
public long date;
|
||||
public String mcversion;
|
||||
public String fileFormat = "MCPR";
|
||||
public int fileFormatVersion;
|
||||
public int protocol;
|
||||
public String generator;
|
||||
public int selfId = -1;
|
||||
|
||||
public Set<UUID> players = new HashSet<>();
|
||||
|
||||
public RecordMetaData copy() {
|
||||
RecordMetaData ret = new RecordMetaData();
|
||||
synchronized (this) {
|
||||
ret.singleplayer = this.singleplayer;
|
||||
ret.serverName = this.serverName;
|
||||
ret.duration = this.duration;
|
||||
ret.date = this.date;
|
||||
ret.mcversion = this.mcversion;
|
||||
ret.fileFormat = this.fileFormat;
|
||||
ret.fileFormatVersion = this.fileFormatVersion;
|
||||
ret.protocol = this.protocol;
|
||||
ret.generator = this.generator;
|
||||
ret.selfId = this.selfId;
|
||||
ret.players = new HashSet<>(this.players);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.replay;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import com.mojang.serialization.DynamicOps;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.local.LocalChannel;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.LayeredRegistryAccess;
|
||||
import net.minecraft.core.RegistrySynchronization;
|
||||
import net.minecraft.nbt.NbtOps;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.network.Connection;
|
||||
import net.minecraft.network.ConnectionProtocol;
|
||||
import net.minecraft.network.protocol.BundlePacket;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.PacketFlow;
|
||||
import net.minecraft.network.protocol.common.*;
|
||||
import net.minecraft.network.protocol.common.custom.BrandPayload;
|
||||
import net.minecraft.network.protocol.configuration.ClientboundFinishConfigurationPacket;
|
||||
import net.minecraft.network.protocol.configuration.ClientboundRegistryDataPacket;
|
||||
import net.minecraft.network.protocol.configuration.ClientboundSelectKnownPacks;
|
||||
import net.minecraft.network.protocol.configuration.ClientboundUpdateEnabledFeaturesPacket;
|
||||
import net.minecraft.network.protocol.game.*;
|
||||
import net.minecraft.network.protocol.login.ClientboundLoginFinishedPacket;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.RegistryLayer;
|
||||
import net.minecraft.server.packs.repository.KnownPack;
|
||||
import net.minecraft.tags.TagNetworkSerialization;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.PositionMoveRotation;
|
||||
import net.minecraft.world.flag.FeatureFlags;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class Recorder extends Connection {
|
||||
|
||||
public static final Logger LOGGER = LogUtils.getClassLogger();
|
||||
public final ExecutorService saveService = Executors.newSingleThreadExecutor();
|
||||
|
||||
private final ReplayFile replayFile;
|
||||
private final ServerPhotographer photographer;
|
||||
private final RecorderOption recorderOption;
|
||||
private final RecordMetaData metaData;
|
||||
private final AtomicBoolean isSaving = new AtomicBoolean(false);
|
||||
|
||||
private boolean stopped = false;
|
||||
private boolean paused = false;
|
||||
private boolean resumeOnNextPacket = true;
|
||||
|
||||
private long startTime;
|
||||
private long lastPacket;
|
||||
private long timeShift = 0;
|
||||
|
||||
private boolean isSaved;
|
||||
private ConnectionProtocol state = ConnectionProtocol.LOGIN;
|
||||
|
||||
public Recorder(ServerPhotographer photographer, RecorderOption recorderOption, File replayFile) throws IOException {
|
||||
super(PacketFlow.CLIENTBOUND);
|
||||
|
||||
this.photographer = photographer;
|
||||
this.recorderOption = recorderOption;
|
||||
this.metaData = new RecordMetaData();
|
||||
this.replayFile = new ReplayFile(replayFile, saveService);
|
||||
this.channel = new LocalChannel();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
startTime = System.currentTimeMillis();
|
||||
|
||||
metaData.singleplayer = false;
|
||||
metaData.serverName = recorderOption.serverName;
|
||||
metaData.date = startTime;
|
||||
metaData.mcversion = SharedConstants.getCurrentVersion().name();
|
||||
|
||||
// TODO start event
|
||||
this.savePacket(new ClientboundLoginFinishedPacket(photographer.getGameProfile()), ConnectionProtocol.LOGIN);
|
||||
this.startConfiguration();
|
||||
|
||||
savePacket(ClientboundPlayerPositionPacket.of(photographer.getId(), PositionMoveRotation.of(photographer), Collections.emptySet()));
|
||||
|
||||
if (recorderOption.forceWeather != null) {
|
||||
setWeather(recorderOption.forceWeather);
|
||||
}
|
||||
}
|
||||
|
||||
public void startConfiguration() {
|
||||
this.state = ConnectionProtocol.CONFIGURATION;
|
||||
MinecraftServer server = MinecraftServer.getServer();
|
||||
|
||||
this.savePacket(new ClientboundCustomPayloadPacket(new BrandPayload(server.getServerModName())), ConnectionProtocol.CONFIGURATION);
|
||||
this.savePacket(new ClientboundServerLinksPacket(server.serverLinks().untrust()), ConnectionProtocol.CONFIGURATION);
|
||||
this.savePacket(new ClientboundUpdateEnabledFeaturesPacket(FeatureFlags.REGISTRY.toNames(server.getWorldData().enabledFeatures())), ConnectionProtocol.CONFIGURATION);
|
||||
|
||||
List<KnownPack> knownPackslist = server.getResourceManager().listPacks().flatMap((iresourcepack) -> iresourcepack.location().knownPackInfo().stream()).toList();
|
||||
this.savePacket(new ClientboundSelectKnownPacks(knownPackslist), ConnectionProtocol.CONFIGURATION);
|
||||
|
||||
server.getServerResourcePack().ifPresent((info) -> this.savePacket(new ClientboundResourcePackPushPacket(
|
||||
info.id(), info.url(), info.hash(), info.isRequired(), Optional.ofNullable(info.prompt())
|
||||
)));
|
||||
|
||||
LayeredRegistryAccess<RegistryLayer> layeredregistryaccess = server.registries();
|
||||
DynamicOps<Tag> dynamicOps = layeredregistryaccess.compositeAccess().createSerializationContext(NbtOps.INSTANCE);
|
||||
RegistrySynchronization.packRegistries(dynamicOps, layeredregistryaccess.getAccessFrom(RegistryLayer.WORLDGEN), Set.copyOf(knownPackslist),
|
||||
(key, entries) ->
|
||||
this.savePacket(new ClientboundRegistryDataPacket(key, entries), ConnectionProtocol.CONFIGURATION)
|
||||
);
|
||||
this.savePacket(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(layeredregistryaccess)), ConnectionProtocol.CONFIGURATION);
|
||||
|
||||
this.savePacket(ClientboundFinishConfigurationPacket.INSTANCE, ConnectionProtocol.CONFIGURATION);
|
||||
state = ConnectionProtocol.PLAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flushChannel() {
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
stopped = true;
|
||||
}
|
||||
|
||||
public void pauseRecording() {
|
||||
resumeOnNextPacket = false;
|
||||
paused = true;
|
||||
}
|
||||
|
||||
public void resumeRecording() {
|
||||
resumeOnNextPacket = true;
|
||||
}
|
||||
|
||||
public void setWeather(RecorderOption.RecordWeather weather) {
|
||||
weather.getPackets().forEach(this::savePacket);
|
||||
}
|
||||
|
||||
public long getRecordedTime() {
|
||||
final long base = System.currentTimeMillis() - startTime;
|
||||
return base - timeShift;
|
||||
}
|
||||
|
||||
private synchronized long getCurrentTimeAndUpdate() {
|
||||
long now = getRecordedTime();
|
||||
if (paused) {
|
||||
if (resumeOnNextPacket) {
|
||||
paused = false;
|
||||
}
|
||||
timeShift += now - lastPacket;
|
||||
return lastPacket;
|
||||
}
|
||||
return lastPacket = now;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(@NotNull Packet<?> packet, @Nullable ChannelFutureListener callbacks, boolean flush) {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
switch (packet) {
|
||||
case BundlePacket<?> packet1 -> {
|
||||
packet1.subPackets().forEach(subPacket -> send(subPacket, null));
|
||||
return;
|
||||
}
|
||||
case ClientboundAddEntityPacket packet1 -> {
|
||||
if (packet1.getType() == EntityType.PLAYER) {
|
||||
synchronized (metaData) {
|
||||
metaData.players.add(packet1.getUUID());
|
||||
}
|
||||
saveMetadata();
|
||||
}
|
||||
}
|
||||
case ClientboundDisconnectPacket ignored -> {
|
||||
return;
|
||||
}
|
||||
case ClientboundTrackedWaypointPacket ignored -> {
|
||||
return;
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
if (recorderOption.forceDayTime != -1 && packet instanceof ClientboundSetTimePacket packet1) {
|
||||
// Leaves - Paper 26.1: SetTimePacket is now (gameTime, Map<Holder<WorldClock>, ClockNetworkState>).
|
||||
// Replace each world clock with a frozen state at forceDayTime.
|
||||
packet = new ClientboundSetTimePacket(
|
||||
packet1.gameTime(),
|
||||
net.minecraft.util.Util.mapValues(
|
||||
packet1.clockUpdates(),
|
||||
state -> new net.minecraft.world.clock.ClockNetworkState(recorderOption.forceDayTime, 0.0F, 0.0F)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (recorderOption.forceWeather != null && packet instanceof ClientboundGameEventPacket packet1) {
|
||||
ClientboundGameEventPacket.Type type = packet1.getEvent();
|
||||
if (type == ClientboundGameEventPacket.START_RAINING || type == ClientboundGameEventPacket.STOP_RAINING || type == ClientboundGameEventPacket.RAIN_LEVEL_CHANGE || type == ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (recorderOption.ignoreChat && (packet instanceof ClientboundSystemChatPacket || packet instanceof ClientboundPlayerChatPacket)) {
|
||||
return;
|
||||
}
|
||||
|
||||
savePacket(packet);
|
||||
}
|
||||
|
||||
private void saveMetadata() {
|
||||
saveService.execute(() -> {
|
||||
try {
|
||||
replayFile.saveMetaData(metaData);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Error saving metadata", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void savePacket(Packet<?> packet) {
|
||||
this.savePacket(packet, state);
|
||||
}
|
||||
|
||||
private void savePacket(Packet<?> packet, final ConnectionProtocol protocol) {
|
||||
final long timestamp = getCurrentTimeAndUpdate();
|
||||
try {
|
||||
replayFile.savePacket(timestamp, packet, protocol);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error saving packet on thread {}. Are you using some plugin that modify data asynchronously?", Thread.currentThread(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSaved() {
|
||||
return isSaved;
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> saveRecording(File dest, boolean save) {
|
||||
if (!isSaving.compareAndSet(false, true)) {
|
||||
LOGGER.error("saveRecording() called twice");
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("saveRecording() called twice"));
|
||||
}
|
||||
isSaved = true;
|
||||
metaData.duration = (int) lastPacket;
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
replayFile.saveMetaData(metaData);
|
||||
if (save) {
|
||||
replayFile.closeAndSave(dest);
|
||||
} else {
|
||||
replayFile.closeNotSave();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}, saveService);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.replay;
|
||||
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class RecorderOption {
|
||||
|
||||
public int recordDistance = -1;
|
||||
public String serverName = "Lophine";
|
||||
public RecordWeather forceWeather = null;
|
||||
public int forceDayTime = -1;
|
||||
public boolean ignoreChat = false;
|
||||
public boolean ignoreItem = false;
|
||||
|
||||
@NotNull
|
||||
@Contract(" -> new")
|
||||
public static RecorderOption createDefaultOption() {
|
||||
return new RecorderOption();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static RecorderOption createFromBukkit(@NotNull BukkitRecorderOption bukkitRecorderOption) {
|
||||
RecorderOption recorderOption = new RecorderOption();
|
||||
// recorderOption.recordDistance = bukkitRecorderOption.recordDistance;
|
||||
// recorderOption.ignoreItem = bukkitRecorderOption.ignoreItem;
|
||||
recorderOption.serverName = bukkitRecorderOption.serverName;
|
||||
recorderOption.ignoreChat = bukkitRecorderOption.ignoreChat;
|
||||
recorderOption.forceDayTime = bukkitRecorderOption.forceDayTime;
|
||||
recorderOption.forceWeather = switch (bukkitRecorderOption.forceWeather) {
|
||||
case RAIN -> RecordWeather.RAIN;
|
||||
case CLEAR -> RecordWeather.CLEAR;
|
||||
case THUNDER -> RecordWeather.THUNDER;
|
||||
case NULL -> null;
|
||||
};
|
||||
return recorderOption;
|
||||
}
|
||||
|
||||
public enum RecordWeather {
|
||||
CLEAR(new ClientboundGameEventPacket(ClientboundGameEventPacket.STOP_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0)),
|
||||
RAIN(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 1), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0)),
|
||||
THUNDER(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 1), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 1));
|
||||
|
||||
private final List<Packet<?>> packets;
|
||||
|
||||
RecordWeather(Packet<?>... packets) {
|
||||
this.packets = List.of(packets);
|
||||
}
|
||||
|
||||
public List<Packet<?>> getPackets() {
|
||||
return packets;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.replay;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.network.ConnectionProtocol;
|
||||
import net.minecraft.network.ProtocolInfo;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.configuration.ConfigurationProtocols;
|
||||
import net.minecraft.network.protocol.game.GameProtocols;
|
||||
import net.minecraft.network.protocol.login.LoginProtocols;
|
||||
import net.minecraft.network.protocol.status.StatusProtocols;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.protocol.core.ProtocolUtils;
|
||||
import org.leavesmc.leaves.util.UUIDSerializer;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.leavesmc.leaves.replay.Recorder.LOGGER;
|
||||
|
||||
public class ReplayFile {
|
||||
|
||||
private static final String RECORDING_FILE = "recording.tmcpr";
|
||||
private static final String RECORDING_FILE_CRC32 = "recording.tmcpr.crc32";
|
||||
private static final String MARKER_FILE = "markers.json";
|
||||
private static final String META_FILE = "metaData.json";
|
||||
|
||||
private static final Gson MARKER_GSON = new GsonBuilder().registerTypeAdapter(ReplayMarker.class, new ReplayMarker.Serializer()).create();
|
||||
private static final Gson META_GSON = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDSerializer()).create();
|
||||
|
||||
private final File tmpDir;
|
||||
private final DataOutputStream packetStream;
|
||||
private final CRC32 crc32 = new CRC32();
|
||||
|
||||
private final File markerFile;
|
||||
private final File metaFile;
|
||||
|
||||
private final Map<ConnectionProtocol, ProtocolInfo<?>> protocols;
|
||||
private final ExecutorService saveService;
|
||||
|
||||
public ReplayFile(@NotNull File name, ExecutorService saveService) throws IOException {
|
||||
this.saveService = saveService;
|
||||
this.tmpDir = new File(name.getParentFile(), name.getName() + ".tmp");
|
||||
if (tmpDir.exists()) {
|
||||
if (!ReplayFile.deleteDir(tmpDir)) {
|
||||
throw new IOException("Recording file " + name + " already exists!");
|
||||
}
|
||||
}
|
||||
|
||||
if (!tmpDir.mkdirs()) {
|
||||
throw new IOException("Failed to create temp directory for recording " + tmpDir);
|
||||
}
|
||||
|
||||
File packetFile = new File(tmpDir, RECORDING_FILE);
|
||||
this.metaFile = new File(tmpDir, META_FILE);
|
||||
this.markerFile = new File(tmpDir, MARKER_FILE);
|
||||
|
||||
this.packetStream = new DataOutputStream(new DigestOutputStream(new BufferedOutputStream(new FileOutputStream(packetFile)), crc32));
|
||||
|
||||
this.protocols = Map.of(
|
||||
ConnectionProtocol.STATUS, StatusProtocols.CLIENTBOUND,
|
||||
ConnectionProtocol.LOGIN, LoginProtocols.CLIENTBOUND,
|
||||
ConnectionProtocol.CONFIGURATION, ConfigurationProtocols.CLIENTBOUND,
|
||||
ConnectionProtocol.PLAY, GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(MinecraftServer.getServer().registryAccess()))
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private byte @NotNull [] getPacketBytes(Packet packet, ConnectionProtocol state) {
|
||||
ProtocolInfo<?> protocol = this.protocols.get(state);
|
||||
if (protocol == null) {
|
||||
throw new IllegalArgumentException("Unknown protocol state " + state);
|
||||
}
|
||||
|
||||
ByteBuf buf = Unpooled.buffer();
|
||||
protocol.codec().encode(buf, packet);
|
||||
|
||||
buf.readerIndex(0);
|
||||
byte[] ret = ByteBufUtil.getBytes(buf);
|
||||
buf.release();
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void saveMarkers(List<ReplayMarker> markers) throws IOException {
|
||||
try (Writer writer = new OutputStreamWriter(new FileOutputStream(markerFile), StandardCharsets.UTF_8)) {
|
||||
writer.write(MARKER_GSON.toJson(markers));
|
||||
}
|
||||
}
|
||||
|
||||
public void saveMetaData(@NotNull RecordMetaData data) throws IOException {
|
||||
data.fileFormat = "MCPR";
|
||||
data.fileFormatVersion = RecordMetaData.CURRENT_FILE_FORMAT_VERSION;
|
||||
data.protocol = SharedConstants.getCurrentVersion().protocolVersion();
|
||||
data.generator = ProtocolUtils.buildProtocolVersion("replay");
|
||||
RecordMetaData dataCopy = data.copy();
|
||||
|
||||
try (Writer writer = new OutputStreamWriter(new FileOutputStream(metaFile), StandardCharsets.UTF_8)) {
|
||||
writer.write(META_GSON.toJson(dataCopy));
|
||||
}
|
||||
}
|
||||
|
||||
public void savePacket(long timestamp, Packet<?> packet, ConnectionProtocol protocol) {
|
||||
byte[] data = getPacketBytes(packet, protocol);
|
||||
saveService.execute(() -> {
|
||||
try {
|
||||
packetStream.writeInt((int) timestamp);
|
||||
packetStream.writeInt(data.length);
|
||||
packetStream.write(data);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error saving packet", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public synchronized void closeAndSave(File file) throws IOException {
|
||||
packetStream.close();
|
||||
|
||||
String[] files = tmpDir.list();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (ZipOutputStream os = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
|
||||
for (String fileName : files) {
|
||||
os.putNextEntry(new ZipEntry(fileName));
|
||||
File f = new File(tmpDir, fileName);
|
||||
copy(new FileInputStream(f), os);
|
||||
}
|
||||
|
||||
os.putNextEntry(new ZipEntry(RECORDING_FILE_CRC32));
|
||||
Writer writer = new OutputStreamWriter(os);
|
||||
writer.write(Long.toString(crc32.getValue()));
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
for (String fileName : files) {
|
||||
File f = new File(tmpDir, fileName);
|
||||
Files.delete(f.toPath());
|
||||
}
|
||||
Files.delete(tmpDir.toPath());
|
||||
}
|
||||
|
||||
public synchronized void closeNotSave() throws IOException {
|
||||
packetStream.close();
|
||||
|
||||
String[] files = tmpDir.list();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String fileName : files) {
|
||||
File f = new File(tmpDir, fileName);
|
||||
Files.delete(f.toPath());
|
||||
}
|
||||
Files.delete(tmpDir.toPath());
|
||||
}
|
||||
|
||||
private void copy(@NotNull InputStream in, OutputStream out) throws IOException {
|
||||
byte[] buffer = new byte[8192];
|
||||
int len;
|
||||
while ((len = in.read(buffer)) > -1) {
|
||||
out.write(buffer, 0, len);
|
||||
}
|
||||
in.close();
|
||||
}
|
||||
|
||||
private static boolean deleteDir(File dir) {
|
||||
if (dir == null || !dir.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
File[] files = dir.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteDir(file);
|
||||
} else {
|
||||
if (!file.delete()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dir.delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.replay;
|
||||
|
||||
import com.google.gson.*;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
public class ReplayMarker {
|
||||
|
||||
public int time;
|
||||
public String name;
|
||||
public double x = 0;
|
||||
public double y = 0;
|
||||
public double z = 0;
|
||||
public float phi = 0;
|
||||
public float theta = 0;
|
||||
public float varphi = 0;
|
||||
|
||||
public static class Serializer implements JsonSerializer<ReplayMarker> {
|
||||
@Override
|
||||
public JsonElement serialize(ReplayMarker src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject ret = new JsonObject();
|
||||
JsonObject value = new JsonObject();
|
||||
JsonObject position = new JsonObject();
|
||||
ret.add("realTimestamp", new JsonPrimitive(src.time));
|
||||
ret.add("value", value);
|
||||
|
||||
value.add("name", new JsonPrimitive(src.name));
|
||||
value.add("position", position);
|
||||
|
||||
position.add("x", new JsonPrimitive(src.x));
|
||||
position.add("y", new JsonPrimitive(src.y));
|
||||
position.add("z", new JsonPrimitive(src.z));
|
||||
position.add("yaw", new JsonPrimitive(src.phi));
|
||||
position.add("pitch", new JsonPrimitive(src.theta));
|
||||
position.add("roll", new JsonPrimitive(src.varphi));
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.replay;
|
||||
|
||||
import ca.spottedleaf.moonrise.common.util.TickThread;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import fun.bm.lophine.utils.RandomProfilePool;
|
||||
import io.papermc.paper.threadedregions.RegionizedServer;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ClientInformation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.stats.ServerStatsCounter;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.bot.BotStatsCounter;
|
||||
import org.leavesmc.leaves.entity.photographer.CraftPhotographer;
|
||||
import org.leavesmc.leaves.entity.photographer.Photographer;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public class ServerPhotographer extends ServerPlayer {
|
||||
|
||||
private static final Logger LOGGER = LogUtils.getClassLogger();
|
||||
|
||||
private static final List<ServerPhotographer> photographers = new CopyOnWriteArrayList<>();
|
||||
|
||||
public PhotographerCreateState createState;
|
||||
private ServerPlayer followPlayer;
|
||||
private Recorder recorder;
|
||||
private File saveFile;
|
||||
private Vec3 lastPosVec3;
|
||||
|
||||
private final ServerStatsCounter stats;
|
||||
|
||||
private ServerPhotographer(MinecraftServer server, ServerLevel world, GameProfile profile) {
|
||||
super(server, world, profile, ClientInformation.createDefault());
|
||||
this.gameMode = new ServerPhotographerGameMode(this);
|
||||
this.followPlayer = null;
|
||||
this.stats = new BotStatsCounter(server);
|
||||
this.lastPosVec3 = this.position();
|
||||
}
|
||||
|
||||
public static ServerPhotographer createPhotographer(@NotNull PhotographerCreateState state) throws IOException {
|
||||
if (!isCreateLegal(state.id)) {
|
||||
throw new IllegalArgumentException(state.id + " is a invalid photographer id");
|
||||
}
|
||||
|
||||
MinecraftServer server = MinecraftServer.getServer();
|
||||
|
||||
ServerLevel world = ((CraftWorld) state.loc.getWorld()).getHandle();
|
||||
GameProfile profile = RandomProfilePool.getRandomProfile(state.id); // Lophine - add cache
|
||||
|
||||
ServerPhotographer photographer = new ServerPhotographer(server, world, profile);
|
||||
photographer.absSnapTo(state.loc.x(), state.loc.y(), state.loc.z(), state.loc.getYaw(), state.loc.getPitch());
|
||||
|
||||
photographer.recorder = new Recorder(photographer, state.option, new File("replay", state.id));
|
||||
photographer.saveFile = new File("replay", state.id + ".mcpr");
|
||||
photographer.createState = state;
|
||||
|
||||
photographer.recorder.start();
|
||||
if (TickThread.isTickThreadFor(world, state.loc.x(), state.loc.z())) {
|
||||
placePhotographer(server, photographer, world, state);
|
||||
} else {
|
||||
RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
||||
world, state.loc.blockX() >> 4, state.loc.blockZ() >> 4,
|
||||
() -> placePhotographer(server, photographer, world, state));
|
||||
}
|
||||
|
||||
photographers.add(photographer);
|
||||
|
||||
// TODO record distance
|
||||
|
||||
return photographer;
|
||||
}
|
||||
|
||||
private static void placePhotographer(MinecraftServer server, ServerPhotographer photographer, ServerLevel world, @NotNull PhotographerCreateState state) {
|
||||
server.getPlayerList().placeNewPhotographer(photographer.recorder, photographer, world);
|
||||
photographer.level().chunkSource.move(photographer);
|
||||
photographer.setInvisible(true);
|
||||
|
||||
LOGGER.info("Photographer {} created", state.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
this.lastPos = this.blockPosition();
|
||||
super.tick();
|
||||
|
||||
if (this.tickCount % 10 == 0) {
|
||||
connection.resetPosition();
|
||||
this.level().chunkSource.move(this);
|
||||
}
|
||||
|
||||
if (this.followPlayer != null) {
|
||||
if (this.getCamera() == this || this.getCamera().level() != this.level()) {
|
||||
this.setCamera(followPlayer);
|
||||
}
|
||||
|
||||
if (lastPosVec3.distanceToSqr(this.position()) > 1024D) {
|
||||
((CraftPhotographer) this.getBukkitPlayer()).taskScheduler.schedule(ent -> {
|
||||
this.getBukkitPlayer().teleportAsync(this.getCamera().getBukkitEntity().getLocation());
|
||||
}, null, 1L);
|
||||
}
|
||||
}
|
||||
|
||||
lastPosVec3 = this.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void die(@NotNull DamageSource damageSource) {
|
||||
super.die(damageSource);
|
||||
remove(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInvulnerableTo(@NotNull ServerLevel world, @NotNull DamageSource damageSource) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hurtServer(@NotNull ServerLevel world, @NotNull DamageSource source, float amount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHealth(float health) {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ServerStatsCounter getStats() {
|
||||
return stats;
|
||||
}
|
||||
|
||||
public void remove(boolean async) {
|
||||
this.remove(async, true);
|
||||
}
|
||||
|
||||
public void remove(boolean async, boolean save) {
|
||||
LOGGER.info("Photographer {} removed", createState.id);
|
||||
|
||||
this.recorder.stop();
|
||||
photographers.remove(this);
|
||||
|
||||
MinecraftServer.getServer().getPlayerList().removePhotographer(this);
|
||||
RandomProfilePool.putProfile(this.gameProfile); // Lophine - add cache
|
||||
if (!recorder.isSaved()) {
|
||||
CompletableFuture<Void> future = recorder.saveRecording(saveFile, save);
|
||||
if (!async) {
|
||||
future.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setFollowPlayer(ServerPlayer followPlayer) {
|
||||
this.setCamera(followPlayer);
|
||||
this.followPlayer = followPlayer;
|
||||
}
|
||||
|
||||
public ServerPlayer getFollowPlayer() {
|
||||
return followPlayer;
|
||||
}
|
||||
|
||||
public void setSaveFile(File saveFile) {
|
||||
this.saveFile = saveFile;
|
||||
}
|
||||
|
||||
public void pauseRecording() {
|
||||
this.recorder.pauseRecording();
|
||||
}
|
||||
|
||||
public void resumeRecording() {
|
||||
this.recorder.resumeRecording();
|
||||
}
|
||||
|
||||
public static ServerPhotographer getPhotographer(String id) {
|
||||
for (ServerPhotographer photographer : photographers) {
|
||||
if (photographer.createState.id.equals(id)) {
|
||||
return photographer;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static ServerPhotographer getPhotographer(UUID uuid) {
|
||||
for (ServerPhotographer photographer : photographers) {
|
||||
if (photographer.getUUID().equals(uuid)) {
|
||||
return photographer;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<ServerPhotographer> getPhotographers() {
|
||||
return photographers;
|
||||
}
|
||||
|
||||
public Photographer getBukkitPlayer() {
|
||||
return getBukkitEntity();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public CraftPhotographer getBukkitEntity() {
|
||||
return (CraftPhotographer) super.getBukkitEntity();
|
||||
}
|
||||
|
||||
public static boolean isCreateLegal(@NotNull String name) {
|
||||
if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Bukkit.getPlayerExact(name) == null && ServerPhotographer.getPhotographer(name) == null;
|
||||
}
|
||||
|
||||
public static class PhotographerCreateState {
|
||||
|
||||
public RecorderOption option;
|
||||
public Location loc;
|
||||
public final String id;
|
||||
|
||||
public PhotographerCreateState(Location loc, String id, RecorderOption option) {
|
||||
this.loc = loc;
|
||||
this.id = id;
|
||||
this.option = option;
|
||||
}
|
||||
|
||||
public ServerPhotographer createSync() {
|
||||
try {
|
||||
return createPhotographer(this);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Error happened when create photographer: ", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.replay;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.minecraft.server.level.ServerPlayerGameMode;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import org.bukkit.event.player.PlayerGameModeChangeEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class ServerPhotographerGameMode extends ServerPlayerGameMode {
|
||||
|
||||
public ServerPhotographerGameMode(ServerPhotographer photographer) {
|
||||
super(photographer);
|
||||
super.setGameModeForPlayer(GameType.SPECTATOR, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean changeGameModeForPlayer(@NotNull GameType gameMode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public PlayerGameModeChangeEvent changeGameModeForPlayer(@NotNull GameType gameMode, PlayerGameModeChangeEvent.@NotNull Cause cause, @Nullable Component cancelMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setGameModeForPlayer(@NotNull GameType gameMode, @Nullable GameType previousGameMode) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.util;
|
||||
|
||||
import fun.bm.lophine.config.modules.function.WoolHopperCounterConfig;
|
||||
import it.unimi.dsi.fastutil.objects.Object2LongLinkedOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2LongMap;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.Style;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.util.context.ContextMap;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
import net.minecraft.world.item.crafting.Recipe;
|
||||
import net.minecraft.world.item.crafting.RecipeManager;
|
||||
import net.minecraft.world.item.crafting.display.RecipeDisplay;
|
||||
import net.minecraft.world.item.crafting.display.SlotDisplayContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.AbstractBannerBlock;
|
||||
import net.minecraft.world.level.block.BeaconBeamBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static java.util.Map.entry;
|
||||
|
||||
// Powered by fabric-carpet(https://github.com/gnembon/fabric-carpet)
|
||||
|
||||
public class HopperCounter {
|
||||
|
||||
private static boolean enabled = false;
|
||||
private static final Map<DyeColor, HopperCounter> COUNTERS;
|
||||
|
||||
static {
|
||||
EnumMap<DyeColor, HopperCounter> counterMap = new EnumMap<>(DyeColor.class);
|
||||
for (DyeColor color : DyeColor.values()) {
|
||||
counterMap.put(color, new HopperCounter(color));
|
||||
}
|
||||
COUNTERS = Collections.unmodifiableMap(counterMap);
|
||||
}
|
||||
|
||||
public final DyeColor color;
|
||||
private final TextComponent coloredName;
|
||||
private final Object2LongMap<Item> counter = new Object2LongLinkedOpenHashMap<>();
|
||||
private long startTick;
|
||||
private long startMillis;
|
||||
|
||||
private HopperCounter(DyeColor color) {
|
||||
this.startTick = -1;
|
||||
this.color = color;
|
||||
this.coloredName = Component.text(color.getName(), TextColor.color(color.getTextColor()));
|
||||
}
|
||||
|
||||
public void add(MinecraftServer server, ItemStack stack) {
|
||||
if (startTick < 0) {
|
||||
startTick = server.overworld().getGameTime();
|
||||
startMillis = System.currentTimeMillis();
|
||||
}
|
||||
Item item = stack.getItem();
|
||||
counter.put(item, counter.getLong(item) + stack.getCount());
|
||||
}
|
||||
|
||||
public void reset(MinecraftServer server) {
|
||||
counter.clear();
|
||||
startTick = server.overworld().getGameTime();
|
||||
startMillis = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public static void resetAll(MinecraftServer server, boolean fresh) {
|
||||
for (HopperCounter counter : COUNTERS.values()) {
|
||||
counter.reset(server);
|
||||
if (fresh) {
|
||||
counter.startTick = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<Component> format(MinecraftServer server, boolean realTime) {
|
||||
long ticks = Math.max(realTime ? (System.currentTimeMillis() - startMillis) / 50 : server.overworld().getGameTime() - startTick, -1);
|
||||
|
||||
if (startTick < 0 || ticks == -1) {
|
||||
return Collections.singletonList(Component.text().append(coloredName, Component.text(" hasn't started counting yet")).build());
|
||||
}
|
||||
|
||||
long total = getTotalItems();
|
||||
if (total <= 0) {
|
||||
return Collections.singletonList(Component.text()
|
||||
.append(Component.text("No items for "), coloredName)
|
||||
.append(Component.text(" yet ("), Component.text(String.format("%.2f ", ticks / (20.0 * 60.0)), Style.style(TextDecoration.BOLD)))
|
||||
.append(Component.text("min"), Component.text(realTime ? " - real time" : ""), Component.text(")"))
|
||||
.build());
|
||||
}
|
||||
|
||||
List<Component> items = new ArrayList<>();
|
||||
items.add(Component.text()
|
||||
.append(Component.text("Items for "), coloredName, Component.text(" "))
|
||||
.append(Component.text("("), Component.text(String.format("%.2f ", ticks * 1.0 / (20 * 60)), Style.style(TextDecoration.BOLD)))
|
||||
.append(Component.text("min"), Component.text(realTime ? " - real time" : ""), Component.text("), "))
|
||||
.append(Component.text("total: "), Component.text(total, Style.style(TextDecoration.BOLD)), Component.text(", "))
|
||||
.append(Component.text("("), Component.text(String.format("%.1f", total * 1.0 * (20 * 60 * 60) / ticks), Style.style(TextDecoration.BOLD)))
|
||||
.append(Component.text("/h):"))
|
||||
.build());
|
||||
|
||||
items.addAll(counter.object2LongEntrySet().stream().sorted((e, f) -> Long.compare(f.getLongValue(), e.getLongValue())).map(entry -> {
|
||||
Item item = entry.getKey();
|
||||
Component name = Component.translatable(item.getDescriptionId());
|
||||
TextColor textColor = guessColor(server, item);
|
||||
|
||||
if (textColor != null) {
|
||||
name = name.style(name.style().merge(Style.style(textColor)));
|
||||
} else {
|
||||
name = name.style(name.style().merge(Style.style(TextDecoration.ITALIC)));
|
||||
}
|
||||
|
||||
long count = entry.getLongValue();
|
||||
return Component.text()
|
||||
.append(Component.text("- ", NamedTextColor.GRAY))
|
||||
.append(name)
|
||||
.append(Component.text(": ", NamedTextColor.GRAY))
|
||||
.append(Component.text(count, Style.style(TextDecoration.BOLD)), Component.text(", ", NamedTextColor.GRAY))
|
||||
.append(Component.text(String.format("%.1f", count * (20.0 * 60.0 * 60.0) / ticks), Style.style(TextDecoration.BOLD)))
|
||||
.append(Component.text("/h"))
|
||||
.build();
|
||||
}).toList());
|
||||
return items;
|
||||
}
|
||||
|
||||
private static final Map<Item, Block> DEFAULTS = Map.<Item, Block>ofEntries(
|
||||
entry(Items.DANDELION, Blocks.YELLOW_WOOL),
|
||||
entry(Items.POPPY, Blocks.RED_WOOL),
|
||||
entry(Items.BLUE_ORCHID, Blocks.LIGHT_BLUE_WOOL),
|
||||
entry(Items.ALLIUM, Blocks.MAGENTA_WOOL),
|
||||
entry(Items.AZURE_BLUET, Blocks.SNOW_BLOCK),
|
||||
entry(Items.RED_TULIP, Blocks.RED_WOOL),
|
||||
entry(Items.ORANGE_TULIP, Blocks.ORANGE_WOOL),
|
||||
entry(Items.WHITE_TULIP, Blocks.SNOW_BLOCK),
|
||||
entry(Items.PINK_TULIP, Blocks.PINK_WOOL),
|
||||
entry(Items.OXEYE_DAISY, Blocks.SNOW_BLOCK),
|
||||
entry(Items.CORNFLOWER, Blocks.BLUE_WOOL),
|
||||
entry(Items.WITHER_ROSE, Blocks.BLACK_WOOL),
|
||||
entry(Items.LILY_OF_THE_VALLEY, Blocks.WHITE_WOOL),
|
||||
entry(Items.BROWN_MUSHROOM, Blocks.BROWN_MUSHROOM_BLOCK),
|
||||
entry(Items.RED_MUSHROOM, Blocks.RED_MUSHROOM_BLOCK),
|
||||
entry(Items.STICK, Blocks.OAK_PLANKS),
|
||||
entry(Items.GOLD_INGOT, Blocks.GOLD_BLOCK),
|
||||
entry(Items.IRON_INGOT, Blocks.IRON_BLOCK),
|
||||
entry(Items.DIAMOND, Blocks.DIAMOND_BLOCK),
|
||||
entry(Items.NETHERITE_INGOT, Blocks.NETHERITE_BLOCK),
|
||||
entry(Items.SUNFLOWER, Blocks.YELLOW_WOOL),
|
||||
entry(Items.LILAC, Blocks.MAGENTA_WOOL),
|
||||
entry(Items.ROSE_BUSH, Blocks.RED_WOOL),
|
||||
entry(Items.PEONY, Blocks.PINK_WOOL),
|
||||
entry(Items.CARROT, Blocks.ORANGE_WOOL),
|
||||
entry(Items.APPLE, Blocks.RED_WOOL),
|
||||
entry(Items.WHEAT, Blocks.HAY_BLOCK),
|
||||
entry(Items.PORKCHOP, Blocks.PINK_WOOL),
|
||||
entry(Items.RABBIT, Blocks.PINK_WOOL),
|
||||
entry(Items.CHICKEN, Blocks.WHITE_TERRACOTTA),
|
||||
entry(Items.BEEF, Blocks.NETHERRACK),
|
||||
entry(Items.ENCHANTED_GOLDEN_APPLE, Blocks.GOLD_BLOCK),
|
||||
entry(Items.COD, Blocks.WHITE_TERRACOTTA),
|
||||
entry(Items.SALMON, Blocks.ACACIA_PLANKS),
|
||||
entry(Items.ROTTEN_FLESH, Blocks.BROWN_WOOL),
|
||||
entry(Items.PUFFERFISH, Blocks.YELLOW_TERRACOTTA),
|
||||
entry(Items.TROPICAL_FISH, Blocks.ORANGE_WOOL),
|
||||
entry(Items.POTATO, Blocks.WHITE_TERRACOTTA),
|
||||
entry(Items.MUTTON, Blocks.RED_WOOL),
|
||||
entry(Items.BEETROOT, Blocks.NETHERRACK),
|
||||
entry(Items.MELON_SLICE, Blocks.MELON),
|
||||
entry(Items.POISONOUS_POTATO, Blocks.SLIME_BLOCK),
|
||||
entry(Items.SPIDER_EYE, Blocks.NETHERRACK),
|
||||
entry(Items.GUNPOWDER, Blocks.GRAY_WOOL),
|
||||
entry(Items.TURTLE_SCUTE, Blocks.LIME_WOOL),
|
||||
entry(Items.ARMADILLO_SCUTE, Blocks.ANCIENT_DEBRIS),
|
||||
entry(Items.FEATHER, Blocks.WHITE_WOOL),
|
||||
entry(Items.FLINT, Blocks.BLACK_WOOL),
|
||||
entry(Items.LEATHER, Blocks.SPRUCE_PLANKS),
|
||||
entry(Items.GLOWSTONE_DUST, Blocks.GLOWSTONE),
|
||||
entry(Items.PAPER, Blocks.WHITE_WOOL),
|
||||
entry(Items.BRICK, Blocks.BRICKS),
|
||||
entry(Items.INK_SAC, Blocks.BLACK_WOOL),
|
||||
entry(Items.SNOWBALL, Blocks.SNOW_BLOCK),
|
||||
entry(Items.WATER_BUCKET, Blocks.WATER),
|
||||
entry(Items.LAVA_BUCKET, Blocks.LAVA),
|
||||
entry(Items.MILK_BUCKET, Blocks.WHITE_WOOL),
|
||||
entry(Items.CLAY_BALL, Blocks.CLAY),
|
||||
entry(Items.COCOA_BEANS, Blocks.COCOA),
|
||||
entry(Items.BONE, Blocks.BONE_BLOCK),
|
||||
entry(Items.COD_BUCKET, Blocks.BROWN_TERRACOTTA),
|
||||
entry(Items.PUFFERFISH_BUCKET, Blocks.YELLOW_TERRACOTTA),
|
||||
entry(Items.SALMON_BUCKET, Blocks.PINK_TERRACOTTA),
|
||||
entry(Items.TROPICAL_FISH_BUCKET, Blocks.ORANGE_TERRACOTTA),
|
||||
entry(Items.SUGAR, Blocks.WHITE_WOOL),
|
||||
entry(Items.BLAZE_POWDER, Blocks.GOLD_BLOCK),
|
||||
entry(Items.ENDER_PEARL, Blocks.WARPED_PLANKS),
|
||||
entry(Items.NETHER_STAR, Blocks.DIAMOND_BLOCK),
|
||||
entry(Items.PRISMARINE_CRYSTALS, Blocks.SEA_LANTERN),
|
||||
entry(Items.PRISMARINE_SHARD, Blocks.PRISMARINE),
|
||||
entry(Items.RABBIT_HIDE, Blocks.OAK_PLANKS),
|
||||
entry(Items.CHORUS_FRUIT, Blocks.PURPUR_BLOCK),
|
||||
entry(Items.SHULKER_SHELL, Blocks.SHULKER_BOX),
|
||||
entry(Items.NAUTILUS_SHELL, Blocks.BONE_BLOCK),
|
||||
entry(Items.HEART_OF_THE_SEA, Blocks.CONDUIT),
|
||||
entry(Items.HONEYCOMB, Blocks.HONEYCOMB_BLOCK),
|
||||
entry(Items.NAME_TAG, Blocks.BONE_BLOCK),
|
||||
entry(Items.TOTEM_OF_UNDYING, Blocks.YELLOW_TERRACOTTA),
|
||||
entry(Items.TRIDENT, Blocks.PRISMARINE),
|
||||
entry(Items.GHAST_TEAR, Blocks.WHITE_WOOL),
|
||||
entry(Items.PHANTOM_MEMBRANE, Blocks.BONE_BLOCK),
|
||||
entry(Items.EGG, Blocks.BONE_BLOCK),
|
||||
entry(Items.COPPER_INGOT, Blocks.COPPER_BLOCK),
|
||||
entry(Items.AMETHYST_SHARD, Blocks.AMETHYST_BLOCK)
|
||||
);
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Nullable
|
||||
public static TextColor guessColor(@NotNull MinecraftServer server, Item item) {
|
||||
RegistryAccess registryAccess = server.registryAccess();
|
||||
TextColor direct = fromItem(item, registryAccess);
|
||||
if (direct != null) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
Identifier id = registryAccess.lookupOrThrow(Registries.ITEM).getKey(item);
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
for (Recipe<?> recipe : getRecipesForOutput(server.getRecipeManager(), id, server.overworld())) {
|
||||
for (Ingredient ingredient : recipe.placementInfo().ingredients()) {
|
||||
Optional<Holder<Item>> match = ingredient.items().filter(stack -> fromItem(stack.value(), registryAccess) != null).findFirst();
|
||||
if (match.isPresent()) {
|
||||
return fromItem(match.get().value(), registryAccess);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<Recipe<?>> getRecipesForOutput(@NotNull RecipeManager recipeManager, Identifier id, Level level) {
|
||||
List<Recipe<?>> results = new ArrayList<>();
|
||||
ContextMap context = SlotDisplayContext.fromLevel(level);
|
||||
recipeManager.getRecipes().forEach(recipe -> {
|
||||
for (RecipeDisplay recipeDisplay : recipe.value().display()) {
|
||||
recipeDisplay.result().resolveForStacks(context).forEach(stack -> {
|
||||
if (BuiltInRegistries.ITEM.wrapAsHolder(stack.getItem()).unwrapKey().map(ResourceKey::identifier).orElseThrow(IllegalStateException::new).equals(id)) {
|
||||
results.add(recipe.value());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static TextColor fromItem(Item item, RegistryAccess registryAccess) {
|
||||
if (DEFAULTS.containsKey(item)) {
|
||||
return TextColor.color(appropriateColor(DEFAULTS.get(item).defaultMapColor().col));
|
||||
}
|
||||
if (item instanceof DyeItem) {
|
||||
// Leaves - Paper 26.1: DyeItem#getDyeColor() removed, color now lives in DataComponents.DYE
|
||||
DyeColor dyeColor = item.components().get(DataComponents.DYE);
|
||||
if (dyeColor != null) {
|
||||
return TextColor.color(appropriateColor(dyeColor.getMapColor().col));
|
||||
}
|
||||
}
|
||||
|
||||
Block block = null;
|
||||
final Registry<Item> itemRegistry = registryAccess.lookupOrThrow(Registries.ITEM);
|
||||
final Registry<Block> blockRegistry = registryAccess.lookupOrThrow(Registries.BLOCK);
|
||||
Identifier id = itemRegistry.getKey(item);
|
||||
if (item instanceof BlockItem blockItem) {
|
||||
block = blockItem.getBlock();
|
||||
} else if (blockRegistry.getOptional(id).isPresent()) {
|
||||
block = blockRegistry.getValue(id);
|
||||
}
|
||||
|
||||
if (block != null) {
|
||||
if (block instanceof AbstractBannerBlock) {
|
||||
return TextColor.color(appropriateColor(((AbstractBannerBlock) block).getColor().getMapColor().col));
|
||||
} else if (block instanceof BeaconBeamBlock) {
|
||||
return TextColor.color(appropriateColor(((BeaconBeamBlock) block).getColor().getMapColor().col));
|
||||
}
|
||||
return TextColor.color(appropriateColor(block.defaultMapColor().col));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static int appropriateColor(int color) {
|
||||
if (color == 0) {
|
||||
return MapColor.SNOW.col;
|
||||
}
|
||||
int r = (color >> 16 & 255);
|
||||
int g = (color >> 8 & 255);
|
||||
int b = (color & 255);
|
||||
if (r < 70) {
|
||||
r = 70;
|
||||
}
|
||||
if (g < 70) {
|
||||
g = 70;
|
||||
}
|
||||
if (b < 70) {
|
||||
b = 70;
|
||||
}
|
||||
return (r << 16) + (g << 8) + b;
|
||||
}
|
||||
|
||||
public long getTotalItems() {
|
||||
return counter.isEmpty() ? 0 : counter.values().longStream().sum();
|
||||
}
|
||||
|
||||
public static HopperCounter getCounter(DyeColor color) {
|
||||
return COUNTERS.get(color);
|
||||
}
|
||||
|
||||
public static void setEnabled(boolean is) {
|
||||
enabled = is;
|
||||
}
|
||||
|
||||
public static boolean isEnabled() {
|
||||
return WoolHopperCounterConfig.enabled && enabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.util;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.UUID;
|
||||
|
||||
public class UUIDSerializer implements JsonSerializer<UUID> {
|
||||
@Override
|
||||
public JsonElement serialize(@NotNull UUID src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.util;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static java.util.Map.entry;
|
||||
|
||||
public class WoolUtils {
|
||||
private static final Map<Block, DyeColor> WOOL_BLOCK_TO_DYE = Map.ofEntries(
|
||||
entry(Blocks.WHITE_WOOL, DyeColor.WHITE),
|
||||
entry(Blocks.ORANGE_WOOL, DyeColor.ORANGE),
|
||||
entry(Blocks.MAGENTA_WOOL, DyeColor.MAGENTA),
|
||||
entry(Blocks.LIGHT_BLUE_WOOL, DyeColor.LIGHT_BLUE),
|
||||
entry(Blocks.YELLOW_WOOL, DyeColor.YELLOW),
|
||||
entry(Blocks.LIME_WOOL, DyeColor.LIME),
|
||||
entry(Blocks.PINK_WOOL, DyeColor.PINK),
|
||||
entry(Blocks.GRAY_WOOL, DyeColor.GRAY),
|
||||
entry(Blocks.LIGHT_GRAY_WOOL, DyeColor.LIGHT_GRAY),
|
||||
entry(Blocks.CYAN_WOOL, DyeColor.CYAN),
|
||||
entry(Blocks.PURPLE_WOOL, DyeColor.PURPLE),
|
||||
entry(Blocks.BLUE_WOOL, DyeColor.BLUE),
|
||||
entry(Blocks.BROWN_WOOL, DyeColor.BROWN),
|
||||
entry(Blocks.GREEN_WOOL, DyeColor.GREEN),
|
||||
entry(Blocks.RED_WOOL, DyeColor.RED),
|
||||
entry(Blocks.BLACK_WOOL, DyeColor.BLACK)
|
||||
);
|
||||
|
||||
public static DyeColor getWoolColorAtPosition(Level worldIn, BlockPos pos) {
|
||||
BlockState state = worldIn.getBlockState(pos);
|
||||
return WOOL_BLOCK_TO_DYE.get(state.getBlock());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user