├── .gitignore ├── .gitattributes ├── src └── main │ ├── resources │ ├── assets │ │ └── datamancer │ │ │ └── icon.png │ ├── datamancer.accesswidener │ ├── datamancer.mixins.json │ └── fabric.mod.json │ └── java │ └── xyz │ └── trivaxy │ └── datamancer │ ├── access │ └── MarkerListenerAccess.java │ ├── command │ ├── placeholder │ │ ├── PlaceholderException.java │ │ ├── PlaceholderProcessor.java │ │ ├── PlaceholderBuilder.java │ │ └── Placeholder.java │ ├── TrackerCommand.java │ ├── RepeatCommand.java │ ├── DatamancerCommand.java │ ├── MarkerGogglesCommand.java │ ├── entry │ │ └── DebugEntry.java │ ├── OpenCommand.java │ ├── MakeCommand.java │ ├── FunctionProfileCommand.java │ └── WatchCommand.java │ ├── mixin │ ├── PlayerMixin.java │ ├── ExecutionContextMixin.java │ ├── MinecraftMixin.java │ ├── CommandFunctionMixin.java │ └── CallFunctionMixin.java │ ├── networking │ └── packet │ │ ├── tracker │ │ ├── ClearTrackerPacket.java │ │ └── TrackerInfoPacket.java │ │ ├── marker │ │ ├── MarkerGogglesOffPacket.java │ │ └── MarkerGogglesInfoPacket.java │ │ └── DatamancerPackets.java │ ├── tracker │ ├── TrackerList.java │ └── ServerTracker.java │ ├── Attachments.java │ ├── util │ ├── LongRingBuffer.java │ └── OurComponentUtils.java │ ├── client │ ├── DatamancerClient.java │ └── rendering │ │ ├── tracker │ │ └── TrackerRenderer.java │ │ └── marker │ │ └── MarkerRenderer.java │ ├── profile │ ├── PerformanceEntry.java │ ├── FunctionReportColumn.java │ ├── FunctionReport.java │ └── FunctionProfiler.java │ ├── Datamancer.java │ ├── MarkerInfoHandler.java │ └── watch │ └── DataPackWatcher.java ├── settings.gradle ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── gradle.properties ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /run/ 2 | /out/ 3 | /.gradle/ 4 | /.idea/ 5 | /build/ 6 | icon.ai 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/main/resources/assets/datamancer/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trivaxy/datamancer/HEAD/src/main/resources/assets/datamancer/icon.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | gradlePluginPortal() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/access/MarkerListenerAccess.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.access; 2 | 3 | public interface MarkerListenerAccess { 4 | 5 | boolean isListeningForMarkers(); 6 | 7 | void setListeningForMarkers(boolean listeningForMarkers); 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jun 23 14:30:09 EET 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/command/placeholder/PlaceholderException.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.command.placeholder; 2 | 3 | public class PlaceholderException extends Exception { 4 | public PlaceholderException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/datamancer.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v1 named 2 | 3 | accessible class net/minecraft/commands/functions/FunctionBuilder 4 | accessible field net/minecraft/commands/functions/FunctionBuilder plainEntries Ljava/util/List; 5 | accessible field net/minecraft/commands/execution/tasks/CallFunction function Lnet/minecraft/commands/functions/InstantiatedFunction; 6 | -------------------------------------------------------------------------------- /src/main/resources/datamancer.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "xyz.trivaxy.datamancer.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "mixins": [ 7 | "CallFunctionMixin", 8 | "CommandFunctionMixin", 9 | "ExecutionContextMixin", 10 | "PlayerMixin" 11 | ], 12 | "client": [ 13 | "MinecraftMixin" 14 | ], 15 | "injectors": { 16 | "defaultRequire": 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/command/placeholder/PlaceholderProcessor.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.command.placeholder; 2 | 3 | import com.mojang.brigadier.exceptions.CommandSyntaxException; 4 | import net.minecraft.commands.CommandSourceStack; 5 | import net.minecraft.network.chat.Component; 6 | 7 | @FunctionalInterface 8 | public interface PlaceholderProcessor { 9 | 10 | Component process(CommandSourceStack source, Placeholder.Arguments arguments) throws PlaceholderException, CommandSyntaxException; 11 | } 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx1G 3 | # Fabric Properties 4 | # check these on https://modmuss50.me/fabric.html 5 | minecraft_version=1.21 6 | loader_version=0.15.11 7 | # Mod Properties 8 | mod_version=1.2.1 9 | maven_group=xyz.trivaxy 10 | archives_base_name=datamancer 11 | # Dependencies 12 | # check this on https://modmuss50.me/fabric.html 13 | fabric_version=0.100.3+1.21 14 | # Parchment Properties 15 | parchment_version=2024.06.23 16 | # ASCII Table 17 | ascii_table_version=1.8.0 -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/mixin/PlayerMixin.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.mixin; 2 | 3 | import net.minecraft.world.entity.player.Player; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | import org.spongepowered.asm.mixin.Unique; 6 | import xyz.trivaxy.datamancer.access.MarkerListenerAccess; 7 | 8 | @Mixin(Player.class) 9 | public class PlayerMixin implements MarkerListenerAccess { 10 | 11 | @Unique 12 | private boolean listeningForMarkers = false; 13 | 14 | @Unique 15 | public boolean isListeningForMarkers() { 16 | return listeningForMarkers; 17 | } 18 | 19 | @Unique 20 | public void setListeningForMarkers(boolean listeningForMarkers) { 21 | this.listeningForMarkers = listeningForMarkers; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/mixin/ExecutionContextMixin.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.mixin; 2 | 3 | import net.minecraft.commands.execution.ExecutionContext; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | import org.spongepowered.asm.mixin.injection.At; 6 | import org.spongepowered.asm.mixin.injection.Inject; 7 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 8 | import xyz.trivaxy.datamancer.profile.FunctionProfiler; 9 | 10 | @Mixin(ExecutionContext.class) 11 | public class ExecutionContextMixin { 12 | 13 | @Inject(method = "runCommandQueue", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;info(Ljava/lang/String;Ljava/lang/Object;)V")) 14 | private void onCommandQuotaExceeded(CallbackInfo ci) { 15 | if (FunctionProfiler.getInstance().isEnabled()) 16 | FunctionProfiler.getInstance().signalOverflow(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/networking/packet/tracker/ClearTrackerPacket.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.networking.packet.tracker; 2 | 3 | import net.minecraft.network.RegistryFriendlyByteBuf; 4 | import net.minecraft.network.codec.StreamCodec; 5 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 6 | import xyz.trivaxy.datamancer.Datamancer; 7 | 8 | public record ClearTrackerPacket() implements CustomPacketPayload { 9 | 10 | public static final CustomPacketPayload.Type PACKET_ID = new CustomPacketPayload.Type<>(Datamancer.in("clear_tracker")); 11 | public static final StreamCodec PACKET_CODEC = StreamCodec.of((encoder, obj) -> {}, buf -> new ClearTrackerPacket()); 12 | 13 | @Override 14 | public Type type() { 15 | return PACKET_ID; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/networking/packet/marker/MarkerGogglesOffPacket.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.networking.packet.marker; 2 | 3 | import net.minecraft.network.RegistryFriendlyByteBuf; 4 | import net.minecraft.network.codec.StreamCodec; 5 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 6 | import xyz.trivaxy.datamancer.Datamancer; 7 | 8 | public record MarkerGogglesOffPacket() implements CustomPacketPayload { 9 | 10 | public static final CustomPacketPayload.Type PACKET_ID = new CustomPacketPayload.Type<>(Datamancer.in("marker_goggles_off")); 11 | public static final StreamCodec PACKET_CODEC = StreamCodec.of((encoder, obj) -> {}, buf -> new MarkerGogglesOffPacket()); 12 | 13 | @Override 14 | public Type type() { 15 | return PACKET_ID; 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/tracker/TrackerList.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.tracker; 2 | 3 | import java.util.*; 4 | 5 | public class TrackerList { 6 | 7 | private final List templates; 8 | 9 | public TrackerList(List templates) { 10 | this.templates = new ArrayList<>(templates); 11 | } 12 | 13 | public TrackerList() { 14 | this.templates = new ArrayList<>(); 15 | } 16 | 17 | public void addTemplate(String template) { 18 | templates.add(template); 19 | } 20 | 21 | public List getTemplates() { 22 | return templates; 23 | } 24 | 25 | public int size() { 26 | return templates.size(); 27 | } 28 | 29 | public void removeAt(int index) { 30 | if (index < 0 || index >= templates.size()) 31 | return; 32 | 33 | templates.remove(index); 34 | } 35 | 36 | public void clear() { 37 | templates.clear(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/command/placeholder/PlaceholderBuilder.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.command.placeholder; 2 | 3 | import com.mojang.brigadier.arguments.ArgumentType; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class PlaceholderBuilder { 9 | 10 | private final List> argumentTypes = new ArrayList<>(); 11 | private int optionals = 0; 12 | 13 | public PlaceholderBuilder argument(ArgumentType argumentType) { 14 | if (optionals > 0) 15 | return optional(argumentType); 16 | 17 | argumentTypes.add(argumentType); 18 | return this; 19 | } 20 | 21 | public PlaceholderBuilder optional(ArgumentType argumentType) { 22 | optionals++; 23 | argumentTypes.add(argumentType); 24 | return this; 25 | } 26 | 27 | public Placeholder process(PlaceholderProcessor processor) { 28 | return new Placeholder(argumentTypes, optionals, processor); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/Attachments.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer; 2 | 3 | import com.mojang.serialization.Codec; 4 | import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; 5 | import net.fabricmc.fabric.api.attachment.v1.AttachmentType; 6 | import xyz.trivaxy.datamancer.tracker.TrackerList; 7 | import xyz.trivaxy.datamancer.watch.DataPackWatcher; 8 | 9 | public class Attachments { 10 | 11 | public static final AttachmentType WATCHER_STATE_ATTACHMENT = AttachmentRegistry.createPersistent( 12 | Datamancer.in("watcher_state"), 13 | DataPackWatcher.State.CODEC 14 | ); 15 | 16 | public static final AttachmentType PLAYER_TRACKER_LIST_ATTACHMENT = AttachmentRegistry.builder() 17 | .persistent(Codec.STRING.listOf().xmap(TrackerList::new, TrackerList::getTemplates)) 18 | .copyOnDeath() 19 | .initializer(() -> new TrackerList()) 20 | .buildAndRegister(Datamancer.in("player_tracker_list")); 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Datamancer 2 | 3 | 4 | <*The must have tool for datapack developers and command enthusiasts*> 5 | 6 | Datamancer is a Minecraft mod targetting Fabric for versions 1.20 and above. Its purpose is to boost productivity by providing utilities and features that aid in the development of datapacks and command creations. 7 | 8 | Getting started is simple! Just install the mod from either CurseForge or Modrinth and then head over to the [wiki page](https://github.com/Trivaxy/datamancer/wiki) to get going. 9 | 10 | If you need help using the mod, want to submit a report, or got suggestions to share, then check out [Datamancer's discord server](https://discord.gg/Hnd8afe6J7). 11 | 12 | # Contributions 13 | 14 | 15 | If you find a bug or want to provide feedback, please open an issue. 16 | 17 | I'm happy to accept contributions as well! All I ask is that you open an issue first, so we can discuss the changes you want to make. 18 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/networking/packet/DatamancerPackets.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.networking.packet; 2 | 3 | import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; 4 | import xyz.trivaxy.datamancer.networking.packet.marker.MarkerGogglesInfoPacket; 5 | import xyz.trivaxy.datamancer.networking.packet.marker.MarkerGogglesOffPacket; 6 | import xyz.trivaxy.datamancer.networking.packet.tracker.ClearTrackerPacket; 7 | import xyz.trivaxy.datamancer.networking.packet.tracker.TrackerInfoPacket; 8 | 9 | public class DatamancerPackets { 10 | 11 | public static void registerPacketTypes() { 12 | PayloadTypeRegistry.playS2C().register(MarkerGogglesOffPacket.PACKET_ID, MarkerGogglesOffPacket.PACKET_CODEC); 13 | PayloadTypeRegistry.playS2C().register(MarkerGogglesInfoPacket.PACKET_ID, MarkerGogglesInfoPacket.PACKET_CODEC); 14 | PayloadTypeRegistry.playS2C().register(TrackerInfoPacket.PACKET_ID, TrackerInfoPacket.PACKET_CODEC); 15 | PayloadTypeRegistry.playS2C().register(ClearTrackerPacket.PACKET_ID, ClearTrackerPacket.PACKET_CODEC); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Trivaxy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/mixin/MinecraftMixin.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.mixin; 2 | 3 | import com.llamalad7.mixinextras.injector.wrapoperation.Operation; 4 | import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; 5 | import net.minecraft.client.Minecraft; 6 | import org.objectweb.asm.Opcodes; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import xyz.trivaxy.datamancer.profile.FunctionProfiler; 10 | 11 | @Mixin(Minecraft.class) 12 | public class MinecraftMixin { 13 | 14 | @WrapOperation( 15 | method = "runTick", 16 | at = @At(value = "FIELD", target = "Lnet/minecraft/client/Minecraft;pause:Z", opcode = Opcodes.PUTFIELD) 17 | ) 18 | private void onPauseStatusChange(Minecraft instance, boolean paused, Operation original) { 19 | original.call(instance, paused); 20 | 21 | FunctionProfiler profiler = FunctionProfiler.getInstance(); 22 | 23 | if (paused && profiler.isEnabled()) 24 | profiler.pause(); 25 | else if (!paused && profiler.isEnabled()) 26 | profiler.resume(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "datamancer", 4 | "version": "${version}", 5 | "name": "Datamancer", 6 | "description": "The must-have tool for every datapack developer!", 7 | "authors": [ 8 | "Trivaxy" 9 | ], 10 | "contact": { 11 | "homepage": "https://discord.com/invite/Hnd8afe6J7", 12 | "sources": "https://github.com/Trivaxy/datamancer/tree/main", 13 | "issues": "https://github.com/Trivaxy/datamancer/issues" 14 | }, 15 | "license": "MIT", 16 | "icon": "assets/datamancer/icon.png", 17 | "environment": "*", 18 | "entrypoints": { 19 | "client": [ 20 | "xyz.trivaxy.datamancer.client.DatamancerClient" 21 | ], 22 | "main": [ 23 | "xyz.trivaxy.datamancer.Datamancer" 24 | ], 25 | "cardinal-components": [ 26 | "xyz.trivaxy.datamancer.Datamancer" 27 | ] 28 | }, 29 | "mixins": [ 30 | "datamancer.mixins.json" 31 | ], 32 | "accessWidener": "datamancer.accesswidener", 33 | "custom": { 34 | "cardinal-components": [ 35 | "datamancer:watcher" 36 | ] 37 | }, 38 | "depends": { 39 | "fabricloader": ">=0.14.22", 40 | "fabric": ">0.88.1+1.20.1", 41 | "minecraft": ">1.20.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/mixin/CommandFunctionMixin.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.mixin; 2 | 3 | import com.llamalad7.mixinextras.injector.wrapoperation.Operation; 4 | import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; 5 | import com.llamalad7.mixinextras.sugar.Local; 6 | import com.mojang.brigadier.StringReader; 7 | import net.minecraft.commands.functions.CommandFunction; 8 | import net.minecraft.commands.functions.FunctionBuilder; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.injection.At; 11 | import xyz.trivaxy.datamancer.command.entry.DebugEntry; 12 | 13 | @Mixin(CommandFunction.class) 14 | public interface CommandFunctionMixin { 15 | @WrapOperation(method = "fromLines", at = @At(value = "INVOKE", target = "Lcom/mojang/brigadier/StringReader;peek()C", ordinal = 0)) 16 | private static char detectLineIfItsDebug(StringReader reader, Operation original, @Local FunctionBuilder builder) { 17 | char peeked = original.call(reader); 18 | 19 | if (peeked == '#' && reader.canRead(2) && reader.peek(1) == '!') 20 | builder.plainEntries.add(new DebugEntry<>(reader.getString().substring(2))); 21 | 22 | return peeked; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/util/LongRingBuffer.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.util; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Iterator; 6 | 7 | // An insert-only, non-random access ring buffer that overwrites old values 8 | public class LongRingBuffer implements Iterable { 9 | private final long[] buffer; 10 | private int index = 0; 11 | private int size = 0; 12 | 13 | public LongRingBuffer(int size) { 14 | buffer = new long[size]; 15 | } 16 | 17 | public void add(long value) { 18 | buffer[index] = value; 19 | index = (index + 1) % buffer.length; 20 | size = Math.min(size + 1, buffer.length); 21 | } 22 | 23 | public void clear() { 24 | index = 0; 25 | size = 0; 26 | } 27 | 28 | public int size() { 29 | return size; 30 | } 31 | 32 | @NotNull 33 | @Override 34 | public Iterator iterator() { 35 | return new Iterator<>() { 36 | private int i = 0; 37 | 38 | @Override 39 | public boolean hasNext() { 40 | return i < size; 41 | } 42 | 43 | @Override 44 | public Long next() { 45 | return buffer[i++]; 46 | } 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/networking/packet/tracker/TrackerInfoPacket.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.networking.packet.tracker; 2 | 3 | import net.minecraft.network.RegistryFriendlyByteBuf; 4 | import net.minecraft.network.codec.StreamCodec; 5 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 6 | import xyz.trivaxy.datamancer.Datamancer; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public record TrackerInfoPacket(List entries) implements CustomPacketPayload { 12 | 13 | public static final CustomPacketPayload.Type PACKET_ID = new CustomPacketPayload.Type<>(Datamancer.in("tracker_info")); 14 | public static final StreamCodec PACKET_CODEC = StreamCodec.of((buf, obj) -> obj.write(buf), TrackerInfoPacket::new); 15 | 16 | public TrackerInfoPacket(RegistryFriendlyByteBuf buf) { 17 | this(read(buf)); 18 | } 19 | 20 | public void write(RegistryFriendlyByteBuf buf) { 21 | buf.writeInt(entries.size()); 22 | 23 | for (String entry : entries) 24 | buf.writeUtf(entry); 25 | } 26 | 27 | public static List read(RegistryFriendlyByteBuf buf) { 28 | int size = buf.readInt(); 29 | List entries = new ArrayList<>(); 30 | 31 | for (int i = 0; i < size; i++) 32 | entries.add(buf.readUtf()); 33 | 34 | return entries; 35 | } 36 | 37 | @Override 38 | public Type type() { 39 | return PACKET_ID; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/client/DatamancerClient.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.client; 2 | 3 | import net.fabricmc.api.ClientModInitializer; 4 | import net.fabricmc.api.Environment; 5 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 6 | import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; 7 | import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; 8 | import xyz.trivaxy.datamancer.client.rendering.marker.MarkerRenderer; 9 | import xyz.trivaxy.datamancer.client.rendering.tracker.TrackerRenderer; 10 | import xyz.trivaxy.datamancer.networking.packet.marker.MarkerGogglesInfoPacket; 11 | import xyz.trivaxy.datamancer.networking.packet.marker.MarkerGogglesOffPacket; 12 | import xyz.trivaxy.datamancer.networking.packet.tracker.ClearTrackerPacket; 13 | import xyz.trivaxy.datamancer.networking.packet.tracker.TrackerInfoPacket; 14 | 15 | @Environment(net.fabricmc.api.EnvType.CLIENT) 16 | public class DatamancerClient implements ClientModInitializer { 17 | 18 | @Override 19 | public void onInitializeClient() { 20 | ClientPlayNetworking.registerGlobalReceiver(MarkerGogglesInfoPacket.PACKET_ID, MarkerRenderer::handleMarkerInfoPacket); 21 | ClientPlayNetworking.registerGlobalReceiver(MarkerGogglesOffPacket.PACKET_ID, MarkerRenderer::handleMarkerGogglesOffPacket); 22 | ClientPlayNetworking.registerGlobalReceiver(TrackerInfoPacket.PACKET_ID, TrackerRenderer::handleInfoPacket); 23 | ClientPlayNetworking.registerGlobalReceiver(ClearTrackerPacket.PACKET_ID, TrackerRenderer::handleClearPacket); 24 | WorldRenderEvents.AFTER_TRANSLUCENT.register(MarkerRenderer::renderMarkers); 25 | HudRenderCallback.EVENT.register(TrackerRenderer::renderTracker); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/profile/PerformanceEntry.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.profile; 2 | 3 | import net.minecraft.resources.ResourceLocation; 4 | import xyz.trivaxy.datamancer.util.LongRingBuffer; 5 | 6 | public final class PerformanceEntry { 7 | 8 | private final ResourceLocation functionId; 9 | private final LongRingBuffer data = new LongRingBuffer(300); 10 | private long totalExecutionCount = 0; 11 | 12 | public PerformanceEntry(ResourceLocation functionId) { 13 | this.functionId = functionId; 14 | } 15 | 16 | public ResourceLocation getFunctionId() { 17 | return functionId; 18 | } 19 | 20 | public void record(long executionTime) { 21 | data.add(executionTime); 22 | totalExecutionCount++; 23 | } 24 | 25 | public void reset() { 26 | data.clear(); 27 | } 28 | 29 | public long getTotalExecutionCount() { 30 | return totalExecutionCount; 31 | } 32 | 33 | public double calculateMean() { 34 | double sum = 0; 35 | 36 | for (long value : data) { 37 | sum += value; 38 | } 39 | 40 | return sum / data.size(); 41 | } 42 | 43 | public double calculateStandardDeviation() { 44 | double mean = calculateMean(); 45 | double sum = 0; 46 | 47 | for (long value : data) { 48 | sum += Math.pow(value - mean, 2); 49 | } 50 | 51 | return Math.sqrt(sum / data.size()); 52 | } 53 | 54 | public double findMin() { 55 | double min = Double.MAX_VALUE; 56 | 57 | for (long value : data) { 58 | min = Math.min(min, value); 59 | } 60 | 61 | return min; 62 | } 63 | 64 | public double findMax() { 65 | double max = Double.MIN_VALUE; 66 | 67 | for (long value : data) { 68 | max = Math.max(max, value); 69 | } 70 | 71 | return max; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/command/TrackerCommand.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.command; 2 | 3 | import com.mojang.brigadier.CommandDispatcher; 4 | import com.mojang.brigadier.arguments.IntegerArgumentType; 5 | import com.mojang.brigadier.arguments.StringArgumentType; 6 | import net.minecraft.commands.CommandSourceStack; 7 | import net.minecraft.commands.Commands; 8 | import xyz.trivaxy.datamancer.Datamancer; 9 | 10 | import static net.minecraft.commands.Commands.argument; 11 | import static net.minecraft.commands.Commands.literal; 12 | 13 | public class TrackerCommand extends DatamancerCommand { 14 | 15 | @Override 16 | public void register(CommandDispatcher dispatcher, Commands.CommandSelection environment) { 17 | dispatcher.register(literal("tracker") 18 | .requires(source -> source.hasPermission(2) && source.isPlayer()) 19 | .then(literal("add") 20 | .then(argument("template", StringArgumentType.greedyString()) 21 | .executes(context -> { 22 | Datamancer.getTracker().addTrackableForPlayer(context.getSource().getPlayer(), StringArgumentType.getString(context, "template")); 23 | return 1; 24 | }) 25 | ) 26 | ) 27 | .then(literal("clear") 28 | .executes(context -> { 29 | Datamancer.getTracker().clearTrackablesForPlayer(context.getSource().getPlayer()); 30 | return 1; 31 | }) 32 | ) 33 | .then(literal("remove") 34 | .then(argument("index", IntegerArgumentType.integer(0)) 35 | .executes(context -> { 36 | Datamancer.getTracker().removeTrackableAtIndexForPlayer(context.getSource().getPlayer(), IntegerArgumentType.getInteger(context, "index")); 37 | return 1; 38 | }) 39 | ) 40 | ) 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/command/RepeatCommand.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.command; 2 | 3 | import com.mojang.brigadier.CommandDispatcher; 4 | import net.minecraft.commands.CommandSourceStack; 5 | import net.minecraft.commands.Commands; 6 | import net.minecraft.commands.arguments.item.FunctionArgument; 7 | import net.minecraft.commands.functions.CommandFunction; 8 | import net.minecraft.server.commands.FunctionCommand; 9 | 10 | import java.util.Collection; 11 | 12 | import static com.mojang.brigadier.arguments.IntegerArgumentType.getInteger; 13 | import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; 14 | import static net.minecraft.commands.Commands.argument; 15 | import static net.minecraft.commands.Commands.literal; 16 | 17 | public class RepeatCommand extends DatamancerCommand { 18 | 19 | @Override 20 | public void register(CommandDispatcher dispatcher, Commands.CommandSelection environment) { 21 | dispatcher.register(literal("repeat") 22 | .requires(source -> source.hasPermission(2)) 23 | .then(argument("count", integer(1)) 24 | .then(argument("function", FunctionArgument.functions()).suggests(FunctionCommand.SUGGEST_FUNCTION) 25 | .executes(context -> { 26 | int count = getInteger(context, "count"); 27 | Collection> functions = FunctionArgument.getFunctions(context, "function"); 28 | 29 | repeatFunctions(context.getSource(), count, functions); 30 | return 0; 31 | }) 32 | ) 33 | ) 34 | ); 35 | } 36 | 37 | private static void repeatFunctions(CommandSourceStack source, int count, Collection> functions) { 38 | for (int i = 0; i < count; i++) 39 | for (CommandFunction function : functions) 40 | source.getServer().getFunctions().execute(function, source.withSuppressedOutput().withMaximumPermission(2)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/networking/packet/marker/MarkerGogglesInfoPacket.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.networking.packet.marker; 2 | 3 | import com.mojang.datafixers.util.Pair; 4 | import net.minecraft.network.RegistryFriendlyByteBuf; 5 | import net.minecraft.network.codec.StreamCodec; 6 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 7 | import net.minecraft.world.phys.Vec3; 8 | import xyz.trivaxy.datamancer.Datamancer; 9 | 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.UUID; 13 | 14 | public record MarkerGogglesInfoPacket(Map> markers) implements CustomPacketPayload { 15 | 16 | public static final CustomPacketPayload.Type PACKET_ID = new CustomPacketPayload.Type<>(Datamancer.in("marker_goggles_info")); 17 | public static final StreamCodec PACKET_CODEC = StreamCodec.of((buf, obj) -> obj.write(buf), MarkerGogglesInfoPacket::new); 18 | 19 | public MarkerGogglesInfoPacket(RegistryFriendlyByteBuf buf) { 20 | this(read(buf)); 21 | } 22 | 23 | public void write(RegistryFriendlyByteBuf buf) { 24 | buf.writeInt(markers.size()); 25 | 26 | for (var entry : markers.entrySet()) { 27 | buf.writeUUID(entry.getKey()); // uuid 28 | buf.writeVec3(entry.getValue().getFirst()); // vec3 29 | buf.writeInt(entry.getValue().getSecond()); // color code 30 | } 31 | } 32 | 33 | private static Map> read(RegistryFriendlyByteBuf buf) { 34 | int markerCount = buf.readInt(); 35 | 36 | HashMap> incomingMarkers = new HashMap<>(); 37 | 38 | for (int i = 0; i < markerCount; i++) { 39 | UUID markerUUID = buf.readUUID(); 40 | Vec3 markerPos = buf.readVec3(); 41 | int markerColor = buf.readInt(); 42 | 43 | incomingMarkers.put(markerUUID, Pair.of(markerPos, markerColor)); 44 | } 45 | 46 | return incomingMarkers; 47 | } 48 | 49 | @Override 50 | public Type type() { 51 | return PACKET_ID; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/profile/FunctionReportColumn.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.profile; 2 | 3 | import com.github.freva.asciitable.HorizontalAlign; 4 | 5 | import java.util.Comparator; 6 | import java.util.function.Function; 7 | 8 | public enum FunctionReportColumn { 9 | NAME("Function", HorizontalAlign.RIGHT, entry -> entry.getFunctionId().toString(), Comparator.comparing(PerformanceEntry::getFunctionId)), 10 | MEAN("Mean (μs)", HorizontalAlign.LEFT, entry -> String.format("%.5f", entry.calculateMean()), Comparator.comparingDouble(PerformanceEntry::calculateMean)), 11 | SD("Standard Deviation (μs)", HorizontalAlign.LEFT, entry -> String.format("%.5f", entry.calculateStandardDeviation()), Comparator.comparingDouble(PerformanceEntry::calculateStandardDeviation)), 12 | MIN("Min (μs)", HorizontalAlign.LEFT, entry -> String.format("%.5f", entry.findMin()), Comparator.comparingDouble(PerformanceEntry::findMin)), 13 | MAX("Max (μs)", HorizontalAlign.LEFT, entry -> String.format("%.5f", entry.findMax()), Comparator.comparingDouble(PerformanceEntry::findMax)), 14 | ITERATIONS("Iterations", HorizontalAlign.LEFT, entry -> String.valueOf(entry.getTotalExecutionCount()), Comparator.comparing(PerformanceEntry::getTotalExecutionCount)),; 15 | 16 | private final String name; 17 | private final HorizontalAlign alignment; 18 | private final Function valueRetriever; 19 | private final Comparator comparator; 20 | 21 | FunctionReportColumn(String name, HorizontalAlign alignment, Function valueRetriever, Comparator comparator) { 22 | this.name = name; 23 | this.alignment = alignment; 24 | this.valueRetriever = valueRetriever; 25 | this.comparator = comparator; 26 | } 27 | 28 | public String getColumnHeaderName() { 29 | return name; 30 | } 31 | 32 | public HorizontalAlign getAlignment() { 33 | return alignment; 34 | } 35 | 36 | public String getValueInEntry(PerformanceEntry entry) { 37 | return valueRetriever.apply(entry); 38 | } 39 | 40 | public Comparator getComparator() { 41 | return comparator; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/command/DatamancerCommand.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.command; 2 | 3 | import com.mojang.brigadier.CommandDispatcher; 4 | import net.minecraft.client.Minecraft; 5 | import net.minecraft.commands.CommandSourceStack; 6 | import net.minecraft.network.chat.Component; 7 | import net.minecraft.network.chat.TextColor; 8 | 9 | import static net.minecraft.commands.Commands.CommandSelection; 10 | 11 | @SuppressWarnings("StaticInitializerReferencesSubClass") 12 | public abstract class DatamancerCommand { 13 | 14 | public static final Component PREFIX = Component 15 | .literal("[Datamancer] ") 16 | .withStyle(style -> style.withColor(TextColor.fromRgb(2804458))); 17 | 18 | public abstract void register(CommandDispatcher dispatcher, CommandSelection environment); 19 | 20 | protected final void replySuccess(CommandSourceStack source, Component message) { 21 | source.sendSuccess(() -> Component.empty().append(PREFIX).append(message), false); 22 | } 23 | 24 | protected final void replySuccessClientside(Component message) { 25 | ClientsidePrinter.printToChat(Component.empty().append(PREFIX).append(message)); 26 | } 27 | 28 | protected final void replyFailure(CommandSourceStack source, Component message) { 29 | source.sendFailure(Component.empty().append(PREFIX).append(message)); 30 | } 31 | 32 | private static final DatamancerCommand[] COMMANDS = new DatamancerCommand[] { 33 | new FunctionProfileCommand(), 34 | new RepeatCommand(), 35 | new MarkerGogglesCommand(), 36 | new WatchCommand(), 37 | new MakeCommand(), 38 | new OpenCommand(), 39 | new TrackerCommand() 40 | }; 41 | 42 | public static void registerCommands(CommandDispatcher dispatcher, CommandSelection environment) { 43 | for (DatamancerCommand command : COMMANDS) { 44 | command.register(dispatcher, environment); 45 | } 46 | } 47 | 48 | protected static class ClientsidePrinter { 49 | 50 | public static void printToChat(Component component) { 51 | Minecraft.getInstance().gui.getChat().addMessage(component); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/command/MarkerGogglesCommand.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.command; 2 | 3 | import com.mojang.brigadier.CommandDispatcher; 4 | import com.mojang.brigadier.context.CommandContext; 5 | import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 6 | import net.minecraft.commands.CommandSourceStack; 7 | import net.minecraft.commands.Commands; 8 | import net.minecraft.network.chat.Component; 9 | import net.minecraft.server.level.ServerPlayer; 10 | import xyz.trivaxy.datamancer.access.MarkerListenerAccess; 11 | import xyz.trivaxy.datamancer.networking.packet.marker.MarkerGogglesOffPacket; 12 | 13 | import static net.minecraft.commands.Commands.literal; 14 | 15 | public class MarkerGogglesCommand extends DatamancerCommand { 16 | 17 | @Override 18 | public void register(CommandDispatcher dispatcher, Commands.CommandSelection environment) { 19 | dispatcher.register(literal("markergoggles") 20 | .requires(source -> source.hasPermission(2)) 21 | .executes(this::execute) 22 | ); 23 | 24 | // we love brigadier bugs... can't use redirect here 25 | dispatcher.register(literal("mg") 26 | .requires(source -> source.hasPermission(2)) 27 | .executes(this::execute) 28 | ); 29 | } 30 | 31 | private int execute(CommandContext context) { 32 | if (!context.getSource().isPlayer()) { 33 | replyFailure(context.getSource(), Component.literal("This command can only be executed by a player")); 34 | return 0; 35 | } 36 | 37 | ServerPlayer player = context.getSource().getPlayer(); 38 | MarkerListenerAccess listener = (MarkerListenerAccess) player; 39 | 40 | if (listener.isListeningForMarkers()) { 41 | listener.setListeningForMarkers(false); 42 | ServerPlayNetworking.send(player, new MarkerGogglesOffPacket()); 43 | replySuccess(context.getSource(), Component.literal("Disabled marker goggles")); 44 | } else { 45 | listener.setListeningForMarkers(true); 46 | replySuccess(context.getSource(), Component.literal("Enabled marker goggles")); 47 | } 48 | 49 | return 0; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/util/OurComponentUtils.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.util; 2 | 3 | import com.mojang.brigadier.exceptions.CommandSyntaxException; 4 | import net.minecraft.ChatFormatting; 5 | import net.minecraft.commands.arguments.NbtPathArgument; 6 | import net.minecraft.core.BlockPos; 7 | import net.minecraft.nbt.NbtUtils; 8 | import net.minecraft.nbt.Tag; 9 | import net.minecraft.network.chat.Component; 10 | import net.minecraft.network.chat.MutableComponent; 11 | 12 | import java.util.List; 13 | 14 | // Named as OurComponentUtils to avoid conflict with Minecraft's ComponentUtils class 15 | public class OurComponentUtils { 16 | public static Component getPrettyPrintedTag(Tag tag, NbtPathArgument.NbtPath path) { 17 | if (path == null) 18 | return NbtUtils.toPrettyComponent(tag); 19 | 20 | List nbtInPath; 21 | 22 | try { 23 | nbtInPath = path.get(tag); 24 | } catch (CommandSyntaxException e) { 25 | return Component.literal("None").withStyle(ChatFormatting.RED); 26 | } 27 | 28 | return nbtInPath 29 | .stream() 30 | .map(NbtUtils::toPrettyComponent) 31 | .reduce(Component.empty(), (acc, c) -> ((MutableComponent)acc).append(" ").append(c)); 32 | } 33 | 34 | public static Component joinComponents(List components, String separator) { 35 | MutableComponent result = Component.empty(); 36 | 37 | for (int i = 0; i < components.size(); i++) { 38 | result.append(components.get(i)); 39 | 40 | if (i < components.size() - 1) 41 | result.append(separator); 42 | } 43 | 44 | return result; 45 | } 46 | 47 | public static Component prettyPrintedCoordinates(float x, float y, float z) { 48 | return Component 49 | .literal(String.valueOf(x)).withStyle(ChatFormatting.GOLD) 50 | .append(Component.literal(", ").withStyle(ChatFormatting.WHITE)) 51 | .append(Component.literal(String.valueOf(y)).withStyle(ChatFormatting.GOLD)) 52 | .append(Component.literal(", ").withStyle(ChatFormatting.WHITE)) 53 | .append(Component.literal(String.valueOf(z)).withStyle(ChatFormatting.GOLD)); 54 | 55 | } 56 | 57 | public static Component prettyPrintedCoordinates(BlockPos pos) { 58 | return prettyPrintedCoordinates(pos.getX(), pos.getY(), pos.getZ()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/mixin/CallFunctionMixin.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.mixin; 2 | 3 | import com.llamalad7.mixinextras.injector.wrapoperation.Operation; 4 | import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; 5 | import com.llamalad7.mixinextras.sugar.Local; 6 | import net.minecraft.commands.ExecutionCommandSource; 7 | import net.minecraft.commands.execution.CommandQueueEntry; 8 | import net.minecraft.commands.execution.ExecutionContext; 9 | import net.minecraft.commands.execution.Frame; 10 | import net.minecraft.commands.execution.UnboundEntryAction; 11 | import net.minecraft.commands.execution.tasks.CallFunction; 12 | import net.minecraft.commands.execution.tasks.ContinuationTask; 13 | import net.minecraft.resources.ResourceLocation; 14 | import org.spongepowered.asm.mixin.Mixin; 15 | import org.spongepowered.asm.mixin.injection.At; 16 | import xyz.trivaxy.datamancer.profile.FunctionProfiler; 17 | 18 | import java.util.List; 19 | 20 | @Mixin(CallFunction.class) 21 | public abstract class CallFunctionMixin> implements UnboundEntryAction { 22 | 23 | @WrapOperation(method = "execute(Lnet/minecraft/commands/ExecutionCommandSource;Lnet/minecraft/commands/execution/ExecutionContext;Lnet/minecraft/commands/execution/Frame;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/commands/execution/tasks/ContinuationTask;schedule(Lnet/minecraft/commands/execution/ExecutionContext;Lnet/minecraft/commands/execution/Frame;Ljava/util/List;Lnet/minecraft/commands/execution/tasks/ContinuationTask$TaskProvider;)V")) 24 | private void wrapFunctionCall(ExecutionContext executionContext, Frame innerFrame, List> list, ContinuationTask.TaskProvider> taskProvider, Operation original, @Local(argsOnly = true) Frame outerFrame) { 25 | ResourceLocation functionId = ((CallFunction) (Object) this).function.id(); 26 | FunctionProfiler profiler = FunctionProfiler.getInstance(); 27 | 28 | if (profiler.isEnabled()) { 29 | executionContext.queueNext(new CommandQueueEntry<>(outerFrame, (frame, context) -> profiler.pushWatch(functionId))); 30 | original.call(executionContext, innerFrame, list, taskProvider); 31 | executionContext.queueNext(new CommandQueueEntry<>(outerFrame, (frame, context) -> profiler.popWatch())); 32 | } else { 33 | original.call(executionContext, innerFrame, list, taskProvider); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/tracker/ServerTracker.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.tracker; 2 | 3 | import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 4 | import net.minecraft.ChatFormatting; 5 | import net.minecraft.network.chat.Component; 6 | import net.minecraft.server.MinecraftServer; 7 | import net.minecraft.server.level.ServerPlayer; 8 | import xyz.trivaxy.datamancer.Attachments; 9 | import xyz.trivaxy.datamancer.command.entry.DebugEntry; 10 | import xyz.trivaxy.datamancer.networking.packet.tracker.ClearTrackerPacket; 11 | import xyz.trivaxy.datamancer.networking.packet.tracker.TrackerInfoPacket; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | public class ServerTracker { 17 | 18 | private final MinecraftServer server; 19 | 20 | public ServerTracker(MinecraftServer server) { 21 | this.server = server; 22 | } 23 | 24 | public void addTrackableForPlayer(ServerPlayer player, String template) { 25 | player.getAttachedOrCreate(Attachments.PLAYER_TRACKER_LIST_ATTACHMENT).addTemplate(template); 26 | } 27 | 28 | public void sendAllTrackedInfo() { 29 | for (ServerPlayer player : server.getPlayerList().getPlayers()) { 30 | if (!player.hasPermissions(2) || !player.hasAttached(Attachments.PLAYER_TRACKER_LIST_ATTACHMENT)) 31 | continue; 32 | 33 | sendInfoToPlayer(player); 34 | } 35 | } 36 | 37 | private void sendInfoToPlayer(ServerPlayer player) { 38 | TrackerList trackerList = player.getAttachedOrCreate(Attachments.PLAYER_TRACKER_LIST_ATTACHMENT); 39 | List payload = new ArrayList<>(); 40 | 41 | for (String template : trackerList.getTemplates()) { 42 | Component expandedResult = Component.literal("X").withStyle(ChatFormatting.RED).withStyle(ChatFormatting.BOLD); 43 | 44 | try { 45 | expandedResult = DebugEntry.processTemplate(player.createCommandSourceStack(), template); 46 | } catch (Exception e) { 47 | // TODO: Currently we ignore the exception, ideally we don't reprocess the template as long as it's invalid 48 | } 49 | 50 | // TODO: Should we handle the case where the components are large to avoid huge packets? 51 | payload.add(Component.Serializer.toJson(expandedResult, server.registryAccess())); 52 | } 53 | 54 | ServerPlayNetworking.send(player, new TrackerInfoPacket(payload)); 55 | } 56 | 57 | public void clearTrackablesForPlayer(ServerPlayer player) { 58 | player.getAttachedOrCreate(Attachments.PLAYER_TRACKER_LIST_ATTACHMENT).clear(); 59 | ServerPlayNetworking.send(player, new ClearTrackerPacket()); 60 | } 61 | 62 | public void removeTrackableAtIndexForPlayer(ServerPlayer player, int index) { 63 | player.getAttachedOrCreate(Attachments.PLAYER_TRACKER_LIST_ATTACHMENT).removeAt(index); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/client/rendering/tracker/TrackerRenderer.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.client.rendering.tracker; 2 | 3 | import com.mojang.datafixers.util.Pair; 4 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 5 | import net.minecraft.ChatFormatting; 6 | import net.minecraft.client.DeltaTracker; 7 | import net.minecraft.client.Minecraft; 8 | import net.minecraft.client.gui.Font; 9 | import net.minecraft.client.gui.GuiGraphics; 10 | import net.minecraft.network.chat.Component; 11 | import xyz.trivaxy.datamancer.networking.packet.tracker.ClearTrackerPacket; 12 | import xyz.trivaxy.datamancer.networking.packet.tracker.TrackerInfoPacket; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.stream.Collectors; 17 | 18 | public class TrackerRenderer { 19 | 20 | private static List entries = new ArrayList<>(); 21 | private static final int LINE_PADDING = 2; 22 | private static final int MAX_VALUE_LENGTH = 8; 23 | 24 | public static void renderTracker(GuiGraphics guiGraphics, DeltaTracker delta) { 25 | if (entries.isEmpty() || Minecraft.getInstance().options.smoothCamera) 26 | return; 27 | 28 | Font font = Minecraft.getInstance().font; 29 | int trackerHeight = entries.size() * (font.lineHeight + LINE_PADDING); 30 | int maxExpandedWidth = entries 31 | .stream() 32 | .map(font::width) 33 | .max(Integer::compareTo) 34 | .get(); 35 | 36 | guiGraphics.drawManaged(() -> { 37 | guiGraphics.fill(0, guiGraphics.guiHeight() / 2 - trackerHeight / 2, maxExpandedWidth + font.width("00") + 2, guiGraphics.guiHeight() / 2 + trackerHeight / 2 + 1, Minecraft.getInstance().options.getBackgroundColor(0.7f)); 38 | 39 | int y = guiGraphics.guiHeight() / 2 - trackerHeight / 2 + LINE_PADDING; 40 | int i = 0; 41 | for (var entry : entries) { 42 | guiGraphics.drawString(font, String.valueOf(i), 2, y, ChatFormatting.RED.getColor()); 43 | guiGraphics.drawString(font, entry, font.width("00") + 1, y, -1); 44 | y += font.lineHeight + LINE_PADDING; 45 | i++; 46 | } 47 | }); 48 | } 49 | 50 | public static void handleInfoPacket(TrackerInfoPacket trackerInfoPacket, ClientPlayNetworking.Context context) { 51 | context.client().execute(() -> { 52 | entries = trackerInfoPacket 53 | .entries() 54 | .stream() 55 | .map(entry -> (Component) Component.Serializer.fromJson(entry, context.player().registryAccess())) 56 | .collect(Collectors.toList()); 57 | }); 58 | } 59 | 60 | public static void handleClearPacket(ClearTrackerPacket clearTrackerPacket, ClientPlayNetworking.Context context) { 61 | context.client().execute(() -> entries.clear()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/profile/FunctionReport.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.profile; 2 | 3 | import com.github.freva.asciitable.AsciiTable; 4 | import com.github.freva.asciitable.Column; 5 | import net.minecraft.resources.ResourceLocation; 6 | 7 | import java.io.FileWriter; 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.util.*; 13 | import java.util.stream.Collectors; 14 | 15 | public class FunctionReport { 16 | 17 | private final List entries; 18 | private final int overflowCount; 19 | private final List overflowStacktrace; 20 | public static final Path OUTPUT_PATH = Paths.get("datamancer", "function_report.txt"); 21 | 22 | public FunctionReport(Collection entries, int overflowCount, List overflowStacktrace) { 23 | this.entries = entries.stream().toList(); 24 | this.overflowCount = overflowCount; 25 | this.overflowStacktrace = overflowStacktrace; 26 | } 27 | 28 | public String constructTable() { 29 | return AsciiTable.getTable( 30 | AsciiTable.FANCY_ASCII, 31 | entries, 32 | Arrays.stream(FunctionReportColumn.values()) 33 | .map(column -> new Column().header(column.getColumnHeaderName()).dataAlign(column.getAlignment()).with(column::getValueInEntry)) 34 | .toList() 35 | ); 36 | } 37 | 38 | public String getReport() { 39 | StringBuilder builder = new StringBuilder(); 40 | 41 | if (overflowCount != 0) { 42 | builder.append("Note: ").append(overflowCount).append(" overflow(s) were detected\n\n"); 43 | builder.append("Last overflow stacktrace:\n"); 44 | 45 | Map stackTraces = overflowStacktrace.stream() 46 | .collect(Collectors.groupingBy(s -> s, LinkedHashMap::new, Collectors.summingInt(e -> 1))); 47 | 48 | int indent = 2; 49 | for (Map.Entry entry : stackTraces.entrySet()) { 50 | builder.append(" ".repeat(indent)).append("- ").append(entry.getKey()); 51 | 52 | if (entry.getValue() > 1) 53 | builder.append(" (").append(entry.getValue()).append(")"); 54 | 55 | builder.append("\n"); 56 | indent += 2; 57 | } 58 | } 59 | 60 | builder.append("\n"); 61 | 62 | if (overflowCount != 0) 63 | builder.append("Caution: the presence of overflow(s) can drastically affect the accuracy of the report\n\n"); 64 | 65 | builder.append(constructTable()); 66 | 67 | return builder.toString(); 68 | } 69 | 70 | public void writeToFile() throws IOException { 71 | Files.createDirectories(OUTPUT_PATH.getParent()); 72 | 73 | try (FileWriter writer = new FileWriter(OUTPUT_PATH.toFile())) { 74 | writer.write(getReport()); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/command/entry/DebugEntry.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.command.entry; 2 | 3 | import com.mojang.brigadier.exceptions.CommandSyntaxException; 4 | import net.minecraft.commands.CommandSourceStack; 5 | import net.minecraft.commands.ExecutionCommandSource; 6 | import net.minecraft.commands.execution.ExecutionContext; 7 | import net.minecraft.commands.execution.Frame; 8 | import net.minecraft.commands.execution.UnboundEntryAction; 9 | import net.minecraft.network.chat.Component; 10 | import net.minecraft.network.chat.MutableComponent; 11 | import net.minecraft.server.level.ServerPlayer; 12 | import net.minecraft.server.players.PlayerList; 13 | import xyz.trivaxy.datamancer.Datamancer; 14 | import xyz.trivaxy.datamancer.command.placeholder.Placeholder; 15 | import xyz.trivaxy.datamancer.command.placeholder.PlaceholderException; 16 | 17 | import java.util.regex.Matcher; 18 | import java.util.regex.Pattern; 19 | 20 | public class DebugEntry> implements UnboundEntryAction { 21 | 22 | private final String template; 23 | private static final Pattern PLACEHOLDER_REGEX_PATTERN = Pattern.compile("\\{([A-Za-z_]+)(\\h+([^{}]+))*}"); 24 | 25 | public DebugEntry(String template) { 26 | this.template = template; 27 | } 28 | 29 | @Override 30 | public void execute(T source, ExecutionContext executionContext, Frame frame) { 31 | CommandSourceStack commandSourceStack = (CommandSourceStack) source; 32 | 33 | try { 34 | Component result = processTemplate(commandSourceStack, template); 35 | PlayerList players = commandSourceStack.getServer().getPlayerList(); 36 | 37 | for (ServerPlayer player : players.getPlayers()) { 38 | if (player.hasPermissions(2)) 39 | player.sendSystemMessage(result); 40 | } 41 | } catch (Exception e) { 42 | Datamancer.logError("Could not expand placeholder: " + e.getMessage()); 43 | } 44 | } 45 | 46 | public static Component processTemplate(CommandSourceStack commandSourceStack, String template) throws PlaceholderException, CommandSyntaxException { 47 | Matcher matcher = PLACEHOLDER_REGEX_PATTERN.matcher(template); 48 | MutableComponent processedTemplate = Component.empty(); 49 | 50 | int lastEnd = 0; 51 | 52 | while (matcher.find()) { 53 | processedTemplate.append(template.substring(lastEnd, matcher.start())); 54 | lastEnd = matcher.end(); 55 | 56 | String processorType = matcher.group(1); 57 | String arguments = matcher.group(3); 58 | 59 | if (!Placeholder.PLACEHOLDERS.containsKey(processorType)) 60 | throw new PlaceholderException("Unknown placeholder type: " + processorType); 61 | 62 | Component replacement = Placeholder.PLACEHOLDERS.get(processorType).expand(commandSourceStack, arguments); 63 | processedTemplate.append(replacement); 64 | } 65 | 66 | if (lastEnd < template.length()) 67 | processedTemplate.append(template.substring(lastEnd)); 68 | 69 | return processedTemplate; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/Datamancer.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer; 2 | 3 | import net.fabricmc.api.ModInitializer; 4 | import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; 5 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; 6 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; 7 | import net.minecraft.resources.ResourceLocation; 8 | import net.minecraft.world.level.Level; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import xyz.trivaxy.datamancer.command.DatamancerCommand; 12 | import xyz.trivaxy.datamancer.networking.packet.DatamancerPackets; 13 | import xyz.trivaxy.datamancer.profile.FunctionProfiler; 14 | import xyz.trivaxy.datamancer.tracker.ServerTracker; 15 | import xyz.trivaxy.datamancer.watch.DataPackWatcher; 16 | 17 | public class Datamancer implements ModInitializer { 18 | 19 | public static final String MOD_ID = "datamancer"; 20 | private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); 21 | private static DataPackWatcher watcher; 22 | private static ServerTracker tracker; 23 | 24 | @Override 25 | public void onInitialize() { 26 | DatamancerPackets.registerPacketTypes(); 27 | CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> DatamancerCommand.registerCommands(dispatcher, environment)); 28 | ServerLifecycleEvents.START_DATA_PACK_RELOAD.register(((server, resourceManager) -> FunctionProfiler.getInstance().restart())); 29 | ServerLifecycleEvents.SERVER_STARTED.register(server -> { 30 | watcher = new DataPackWatcher(server.getLevel(Level.OVERWORLD).getAttachedOrCreate(Attachments.WATCHER_STATE_ATTACHMENT, DataPackWatcher.State::empty)); 31 | 32 | if (watcher.isActive()) 33 | watcher.start(server); 34 | 35 | tracker = new ServerTracker(server); 36 | }); 37 | ServerLifecycleEvents.SERVER_STOPPING.register(server -> watcher.stop()); 38 | ServerTickEvents.END_SERVER_TICK.register(MarkerInfoHandler::sendMarkerInfoToPlayers); 39 | ServerTickEvents.END_SERVER_TICK.register(server -> tracker.sendAllTrackedInfo()); 40 | } 41 | 42 | public static DataPackWatcher getWatcher() { 43 | return watcher; 44 | } 45 | 46 | public static ServerTracker getTracker() { 47 | return tracker; 48 | } 49 | 50 | public static ResourceLocation in(String path) { 51 | return ResourceLocation.fromNamespaceAndPath(MOD_ID, path); 52 | } 53 | 54 | public static String inRaw(String path) { 55 | return in(path).toString(); 56 | } 57 | 58 | public static void log(String message) { 59 | LOGGER.info(message); 60 | } 61 | 62 | public static void logWarn(String message) { 63 | LOGGER.warn(message); 64 | } 65 | 66 | public static void logError(String message) { 67 | LOGGER.error(message); 68 | } 69 | 70 | public static void logError(String message, Throwable throwable) { 71 | LOGGER.error(message + ": ", throwable); 72 | } 73 | 74 | public static void logError(Throwable throwable) { 75 | LOGGER.error(throwable.toString()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/client/rendering/marker/MarkerRenderer.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.client.rendering.marker; 2 | 3 | import com.mojang.blaze3d.systems.RenderSystem; 4 | import com.mojang.blaze3d.vertex.*; 5 | import com.mojang.datafixers.util.Pair; 6 | import com.mojang.math.Axis; 7 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 8 | import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; 9 | import net.minecraft.client.renderer.GameRenderer; 10 | import net.minecraft.world.phys.Vec3; 11 | import org.joml.Matrix4f; 12 | import org.lwjgl.opengl.GL11; 13 | import xyz.trivaxy.datamancer.networking.packet.marker.MarkerGogglesInfoPacket; 14 | import xyz.trivaxy.datamancer.networking.packet.marker.MarkerGogglesOffPacket; 15 | 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | import java.util.UUID; 19 | 20 | public class MarkerRenderer { 21 | 22 | private static Map> markers = new HashMap<>(); 23 | 24 | public static void renderMarkers(WorldRenderContext context) { 25 | for (Pair pair : markers.values()) { 26 | renderMarker(context, pair.getFirst(), pair.getSecond()); 27 | } 28 | } 29 | 30 | private static void renderMarker(WorldRenderContext context, Vec3 position, long color) { 31 | PoseStack poseStack = context.matrixStack(); 32 | Vec3 cameraPos = context.camera().getPosition(); 33 | 34 | poseStack.pushPose(); 35 | poseStack.translate(-cameraPos.x, -cameraPos.y, -cameraPos.z); 36 | poseStack.translate(position.x, position.y, position.z); 37 | poseStack.scale(0.1f, 0.1f, 0.1f); 38 | poseStack.mulPose(context.camera().rotation()); 39 | poseStack.mulPose(Axis.YP.rotationDegrees(180.0f)); 40 | 41 | Tesselator tesselator = Tesselator.getInstance(); 42 | BufferBuilder buffer = tesselator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); 43 | Matrix4f posMatrix = poseStack.last().pose(); 44 | 45 | float r = ((color >> 16) & 0xFF) / 255f; 46 | float g = ((color >> 8) & 0xFF) / 255f; 47 | float b = (color & 0xFF) / 255f; 48 | 49 | buffer.addVertex(posMatrix, -0.5f, 0.5f, 0).setColor(r, g, b, 1f); 50 | buffer.addVertex(posMatrix, -0.5f, -0.5f, 0).setColor(r, g, b, 1f); 51 | buffer.addVertex(posMatrix, 0.5f, -0.5f, 0).setColor(r, g, b, 1f); 52 | buffer.addVertex(posMatrix, 0.5f, 0.5f, 0).setColor(r, g, b, 1f); 53 | 54 | RenderSystem.setShader(GameRenderer::getPositionColorShader); 55 | RenderSystem.disableCull(); 56 | RenderSystem.depthFunc(GL11.GL_ALWAYS); 57 | 58 | BufferUploader.drawWithShader(buffer.buildOrThrow()); 59 | 60 | poseStack.popPose(); 61 | RenderSystem.depthFunc(GL11.GL_LEQUAL); 62 | RenderSystem.enableCull(); 63 | } 64 | 65 | public static void handleMarkerInfoPacket(MarkerGogglesInfoPacket packet, ClientPlayNetworking.Context context) { 66 | context.client().execute(() -> markers = packet.markers()); 67 | } 68 | 69 | public static void handleMarkerGogglesOffPacket(MarkerGogglesOffPacket packet, ClientPlayNetworking.Context context) { 70 | context.client().execute(() -> markers.clear()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/command/OpenCommand.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.command; 2 | 3 | import com.mojang.brigadier.CommandDispatcher; 4 | import com.mojang.brigadier.arguments.StringArgumentType; 5 | import com.mojang.brigadier.suggestion.SuggestionProvider; 6 | import net.minecraft.ChatFormatting; 7 | import net.minecraft.Util; 8 | import net.minecraft.client.Minecraft; 9 | import net.minecraft.commands.CommandSourceStack; 10 | import net.minecraft.commands.Commands; 11 | import net.minecraft.commands.SharedSuggestionProvider; 12 | import net.minecraft.network.chat.ClickEvent; 13 | import net.minecraft.network.chat.Component; 14 | import net.minecraft.server.packs.repository.Pack; 15 | import net.minecraft.world.level.storage.LevelResource; 16 | import xyz.trivaxy.datamancer.Datamancer; 17 | 18 | import java.io.File; 19 | import java.io.IOException; 20 | import java.nio.file.Path; 21 | 22 | import static net.minecraft.commands.Commands.argument; 23 | import static net.minecraft.commands.Commands.literal; 24 | 25 | public class OpenCommand extends DatamancerCommand { 26 | 27 | private static final SuggestionProvider ALL_PACKS = (commandContext, suggestionsBuilder) -> SharedSuggestionProvider.suggest( 28 | commandContext.getSource().getServer().getPackRepository().getAvailablePacks() 29 | .stream() 30 | .map(Pack::getId) 31 | .filter(s -> s.startsWith("file/")) 32 | .map(s -> s.substring(5)) 33 | .map(StringArgumentType::escapeIfRequired), suggestionsBuilder 34 | ); 35 | 36 | @Override 37 | public void register(CommandDispatcher dispatcher, Commands.CommandSelection environment) { 38 | dispatcher.register(literal("open") 39 | .requires(source -> source.hasPermission(3)) 40 | .then(argument("name", StringArgumentType.string()) 41 | .suggests(ALL_PACKS) 42 | .executes(context -> { 43 | if (!context.getSource().isPlayer() || context.getSource().getServer().isDedicatedServer()) { 44 | replyFailure(context.getSource(), Component.literal("Command is only executable by players in singleplayer")); 45 | return 0; 46 | } 47 | 48 | Path datapacksFolder = context.getSource().getServer().getWorldPath(LevelResource.DATAPACK_DIR); 49 | Path packFolder = datapacksFolder.resolve(StringArgumentType.getString(context, "name")); 50 | 51 | if (!packFolder.toFile().exists()) { 52 | replyFailure(context.getSource(), Component.literal("Pack does not exist")); 53 | return 0; 54 | } 55 | 56 | Component fileLink = Component 57 | .literal("Opened pack successfully.") 58 | .withStyle(ChatFormatting.UNDERLINE) 59 | .withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_FILE, packFolder.toAbsolutePath().toString()))); 60 | 61 | Util.getPlatform().openFile(new File(packFolder.toAbsolutePath().toString())); 62 | 63 | replySuccessClientside(fileLink); 64 | return 1; 65 | }) 66 | ) 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/profile/FunctionProfiler.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.profile; 2 | 3 | import com.google.common.base.Stopwatch; 4 | import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; 5 | import net.minecraft.resources.ResourceLocation; 6 | 7 | import java.util.*; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.stream.Collectors; 10 | 11 | public class FunctionProfiler { 12 | 13 | private final HashMap performances = new HashMap<>(); 14 | private final Stopwatch stopwatch = Stopwatch.createStarted(); // it's fine for this to run forever (unless you live 584 years) 15 | private final Deque functionStack = new ArrayDeque<>(); 16 | private final LongArrayFIFOQueue timestampStack = new LongArrayFIFOQueue(); 17 | private boolean enabled = false; 18 | private int overflowCount = 0; 19 | private static List overflowStacktrace = new ArrayList<>(); 20 | private static final FunctionProfiler INSTANCE = new FunctionProfiler(); 21 | 22 | public void restart() { 23 | performances.clear(); 24 | functionStack.clear(); 25 | timestampStack.clear(); 26 | overflowCount = 0; 27 | overflowStacktrace.clear(); 28 | } 29 | 30 | public void pushWatch(ResourceLocation functionId) { 31 | performances.computeIfAbsent(functionId, PerformanceEntry::new); 32 | functionStack.push(functionId); 33 | timestampStack.enqueue(stopwatch.elapsed(TimeUnit.MICROSECONDS)); 34 | } 35 | 36 | public void popWatch() { 37 | ResourceLocation functionId = functionStack.pop(); 38 | long profiledTime = stopwatch.elapsed(TimeUnit.MICROSECONDS) - timestampStack.dequeueLastLong(); 39 | performances.get(functionId).record(profiledTime); 40 | } 41 | 42 | public int watchCount() { 43 | return performances.size(); 44 | } 45 | 46 | public boolean isEnabled() { 47 | return enabled; 48 | } 49 | 50 | public void enable() { 51 | enabled = true; 52 | resume(); 53 | } 54 | 55 | public void disable() { 56 | enabled = false; 57 | stopwatch.reset(); 58 | } 59 | 60 | public void pause() { 61 | if (!enabled) 62 | return; 63 | 64 | if (stopwatch.isRunning()) 65 | stopwatch.stop(); 66 | } 67 | 68 | public void resume() { 69 | if (!enabled) 70 | return; 71 | 72 | if (!stopwatch.isRunning()) 73 | stopwatch.start(); 74 | } 75 | 76 | public void signalOverflow() { 77 | overflowCount++; 78 | 79 | overflowStacktrace = new ArrayList<>(functionStack); 80 | Collections.reverse(overflowStacktrace); 81 | 82 | functionStack.clear(); 83 | timestampStack.clear(); 84 | } 85 | 86 | public FunctionReport getReport(FunctionReportColumn sortByColumn, boolean ascending) { 87 | return new FunctionReport( 88 | performances.values() 89 | .stream() 90 | .filter(entry -> entry.getTotalExecutionCount() != 0) 91 | .sorted(ascending ? sortByColumn.getComparator() : sortByColumn.getComparator().reversed()) 92 | .toList(), 93 | overflowCount, 94 | overflowStacktrace 95 | ); 96 | } 97 | 98 | public static FunctionProfiler getInstance() { 99 | return INSTANCE; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/MarkerInfoHandler.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer; 2 | 3 | import com.mojang.datafixers.util.Pair; 4 | import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 5 | import net.minecraft.resources.ResourceLocation; 6 | import net.minecraft.server.MinecraftServer; 7 | import net.minecraft.server.level.ServerPlayer; 8 | import net.minecraft.world.entity.Marker; 9 | import net.minecraft.world.phys.Vec3; 10 | import xyz.trivaxy.datamancer.access.MarkerListenerAccess; 11 | import xyz.trivaxy.datamancer.networking.packet.marker.MarkerGogglesInfoPacket; 12 | 13 | import java.nio.charset.StandardCharsets; 14 | import java.security.MessageDigest; 15 | import java.security.NoSuchAlgorithmException; 16 | import java.util.*; 17 | 18 | public class MarkerInfoHandler { 19 | 20 | public static final ResourceLocation MARKER_INFO_PACKET_ID = Datamancer.in("marker_info"); 21 | public static final ResourceLocation MARKER_GOGGLES_OFF = Datamancer.in("marker_goggles_off"); 22 | 23 | public static void sendMarkerInfoToPlayers(MinecraftServer server) { 24 | server.getPlayerList().getPlayers().forEach(player -> { 25 | if (!player.hasPermissions(2) || !((MarkerListenerAccess) player).isListeningForMarkers()) 26 | return; 27 | 28 | MarkerGogglesInfoPacket packet = createMarkerInfoPacketFor(player); 29 | ServerPlayNetworking.send(player, packet); 30 | }); 31 | } 32 | 33 | private static MarkerGogglesInfoPacket createMarkerInfoPacketFor(ServerPlayer player) { 34 | List markers = player 35 | .serverLevel() 36 | .getEntitiesOfClass(Marker.class, player.getBoundingBox().inflate(20)); 37 | 38 | Map> markerInfo = new HashMap<>(); 39 | 40 | for (Marker marker : markers) { 41 | markerInfo.put(marker.getUUID(), Pair.of(marker.position(), calculateMarkerColor(marker))); 42 | } 43 | 44 | return new MarkerGogglesInfoPacket(markerInfo); 45 | } 46 | 47 | private static int calculateUUIDColor(UUID uuid) { 48 | try { 49 | MessageDigest digest = MessageDigest.getInstance("SHA-256"); 50 | digest.update(uuid.toString().getBytes(StandardCharsets.UTF_8)); 51 | 52 | byte[] hash = digest.digest(); 53 | return (hash[0] & 0xFF) << 16 | (hash[1] & 0xFF) << 8 | (hash[2] & 0xFF); 54 | } catch (NoSuchAlgorithmException e) { 55 | throw new RuntimeException("SHA-256 implementation not found. What kind of system are you running on???", e); 56 | } 57 | } 58 | 59 | private static int calculateMarkerColor(Marker marker) { 60 | int color = !marker.getTags().isEmpty() ? calculateTagsColor(marker.getTags()) : calculateUUIDColor(marker.getUUID()); 61 | 62 | int red = applyGammaCorrection((color >> 16) & 0xFF); 63 | int green = applyGammaCorrection((color >> 8) & 0xFF); 64 | int blue = applyGammaCorrection(color & 0xFF); 65 | 66 | return red << 16 | green << 8 | blue; 67 | } 68 | 69 | private static int applyGammaCorrection(int component) { 70 | double gamma = 1.2; 71 | double correctedComponent = 255.0 * Math.pow(component / 255.0, gamma); 72 | return (int) Math.min(255, correctedComponent); 73 | } 74 | 75 | private static int calculateTagsColor(Collection tags) { 76 | try { 77 | MessageDigest digest = MessageDigest.getInstance("SHA-256"); 78 | 79 | for (String tag : tags) { 80 | digest.update(tag.getBytes(StandardCharsets.UTF_8)); 81 | } 82 | 83 | byte[] hash = digest.digest(); 84 | return (hash[0] & 0xFF) << 16 | (hash[1] & 0xFF) << 8 | (hash[2] & 0xFF); 85 | } catch (NoSuchAlgorithmException e) { 86 | throw new RuntimeException("SHA-256 implementation not found. What kind of system are you running on???", e); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/command/MakeCommand.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.command; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.JsonObject; 6 | import com.mojang.brigadier.CommandDispatcher; 7 | import com.mojang.brigadier.arguments.StringArgumentType; 8 | import com.mojang.brigadier.context.CommandContext; 9 | import net.minecraft.ChatFormatting; 10 | import net.minecraft.DetectedVersion; 11 | import net.minecraft.commands.CommandSourceStack; 12 | import net.minecraft.commands.Commands; 13 | import net.minecraft.network.chat.ClickEvent; 14 | import net.minecraft.network.chat.Component; 15 | import net.minecraft.server.packs.PackType; 16 | import net.minecraft.world.level.storage.LevelResource; 17 | import xyz.trivaxy.datamancer.Datamancer; 18 | 19 | import java.io.FileWriter; 20 | import java.io.IOException; 21 | import java.nio.file.Path; 22 | 23 | import static net.minecraft.commands.Commands.argument; 24 | import static net.minecraft.commands.Commands.literal; 25 | 26 | public class MakeCommand extends DatamancerCommand { 27 | 28 | @Override 29 | public void register(CommandDispatcher dispatcher, Commands.CommandSelection environment) { 30 | dispatcher.register(literal("make") 31 | .requires(source -> source.hasPermission(3)) 32 | .then(argument("name", StringArgumentType.word()) 33 | .executes(context -> { 34 | String name = StringArgumentType.getString(context, "name"); 35 | return createPack(context, name, null); 36 | }) 37 | .then(argument("description", StringArgumentType.greedyString()) 38 | .executes(context -> { 39 | String name = StringArgumentType.getString(context, "name"); 40 | String description = StringArgumentType.getString(context, "description"); 41 | return createPack(context, name, description); 42 | } 43 | ) 44 | )) 45 | ); 46 | } 47 | 48 | private int createPack(CommandContext context, String name, String description) { 49 | Path datapacksFolder = context.getSource().getServer().getWorldPath(LevelResource.DATAPACK_DIR); 50 | Path packFolder = datapacksFolder.resolve(name); 51 | int datapackVersion = DetectedVersion.BUILT_IN.getPackVersion(PackType.SERVER_DATA); 52 | 53 | JsonObject packMeta = new JsonObject(); 54 | packMeta.addProperty("pack_format", datapackVersion); 55 | packMeta.addProperty("description", description == null ? "" : description); 56 | 57 | JsonObject root = new JsonObject(); 58 | root.add("pack", packMeta); 59 | 60 | if (packFolder.toFile().exists()) { 61 | replyFailure(context.getSource(), Component.literal("Pack already exists")); 62 | return 0; 63 | } 64 | 65 | if (!packFolder.toFile().mkdirs()) { 66 | errorAndCleanup(context.getSource(), name, "unable to create pack folder"); 67 | return 0; 68 | } 69 | 70 | Gson gson = new GsonBuilder().setPrettyPrinting().create(); 71 | 72 | try (FileWriter writer = new FileWriter(packFolder.resolve("pack.mcmeta").toFile())) { 73 | writer.write(gson.toJson(root)); 74 | } catch (IOException e) { 75 | errorAndCleanup(context.getSource(), name, "unable to write pack.mcmeta"); 76 | return 0; 77 | } 78 | 79 | if (!packFolder.resolve("data").resolve(name).toFile().mkdirs()) { 80 | errorAndCleanup(context.getSource(), name, "unable to create data folder"); 81 | return 0; 82 | } 83 | 84 | if (!context.getSource().isPlayer() || context.getSource().getServer().isDedicatedServer()) { 85 | replySuccess(context.getSource(), Component.literal("Created pack successfully.")); 86 | return 1; 87 | } 88 | 89 | Component fileLink = Component 90 | .literal("Created pack successfully.") 91 | .withStyle(ChatFormatting.UNDERLINE) 92 | .withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_FILE, packFolder.toAbsolutePath().toString()))); 93 | 94 | replySuccessClientside(fileLink); 95 | 96 | return 1; 97 | } 98 | 99 | private void errorAndCleanup(CommandSourceStack source, String packName, String reason) { 100 | Datamancer.logError("Failed to create datapack \"" + packName + "\": " + reason); 101 | replyFailure(source, Component.literal("Failed to create datapack")); 102 | 103 | Path packFolder = source.getServer().getWorldPath(LevelResource.DATAPACK_DIR).resolve(packName); 104 | 105 | if (packFolder.toFile().exists()) { 106 | if (!packFolder.toFile().delete()) { 107 | Datamancer.logError("Failed to delete pack folder during cleanup: " + packFolder.toAbsolutePath().toString()); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/command/FunctionProfileCommand.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.command; 2 | 3 | import com.mojang.brigadier.CommandDispatcher; 4 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; 5 | import com.mojang.brigadier.context.CommandContext; 6 | import com.mojang.datafixers.types.Func; 7 | import net.minecraft.ChatFormatting; 8 | import net.minecraft.commands.CommandSourceStack; 9 | import net.minecraft.commands.Commands; 10 | import net.minecraft.network.chat.ClickEvent; 11 | import net.minecraft.network.chat.Component; 12 | import xyz.trivaxy.datamancer.Datamancer; 13 | import xyz.trivaxy.datamancer.profile.FunctionProfiler; 14 | import xyz.trivaxy.datamancer.profile.FunctionReport; 15 | import xyz.trivaxy.datamancer.profile.FunctionReportColumn; 16 | 17 | import static net.minecraft.commands.Commands.literal; 18 | 19 | public class FunctionProfileCommand extends DatamancerCommand { 20 | 21 | private static final FunctionProfiler PROFILER = FunctionProfiler.getInstance(); 22 | 23 | @Override 24 | public void register(CommandDispatcher dispatcher, Commands.CommandSelection environment) { 25 | dispatcher.register(literal("fprofile") 26 | .requires(source -> source.hasPermission(2)) 27 | .then(literal("start") 28 | .executes(context -> { 29 | if (PROFILER.isEnabled()) { 30 | replyFailure(context.getSource(), Component.literal("Already watching! Use /fprofile stop to stop profiling, or /fprofile clear to erase profiling data and continue.")); 31 | return 0; 32 | } 33 | 34 | PROFILER.enable(); 35 | replySuccess(context.getSource(), Component.literal("Profiler started!")); 36 | return 1; 37 | }) 38 | ) 39 | .then(literal("stop") 40 | .executes(context -> { 41 | if (!PROFILER.isEnabled()) { 42 | replyFailure(context.getSource(), Component.literal("Profiler is already stopped")); 43 | return 0; 44 | } 45 | 46 | PROFILER.disable(); 47 | replySuccess(context.getSource(), Component.literal("Profiler stopped!")); 48 | return 1; 49 | }) 50 | ) 51 | .then(literal("clear") 52 | .executes(context -> { 53 | PROFILER.restart(); 54 | replySuccess(context.getSource(), Component.literal("Cleared profiler data")); 55 | return 0; 56 | }) 57 | ) 58 | .then(createDumpSubcommand()) 59 | ); 60 | } 61 | 62 | private LiteralArgumentBuilder createDumpSubcommand() { 63 | LiteralArgumentBuilder node = literal("dump"); 64 | 65 | for (FunctionReportColumn column : FunctionReportColumn.values()) { 66 | node = node.then(literal(column.name().toLowerCase()) 67 | .then(literal("ascending").executes(context -> dumpReport(context, column, true))) 68 | .then(literal("descending").executes(context -> dumpReport(context, column, false))) 69 | ); 70 | } 71 | 72 | // If sorting options aren't specified, default is descending mean 73 | node = node.executes(context -> dumpReport(context, FunctionReportColumn.MEAN, false)); 74 | 75 | return node; 76 | } 77 | 78 | private int dumpReport(CommandContext context, FunctionReportColumn sortByColumn, boolean ascending) { 79 | if (!PROFILER.isEnabled() || PROFILER.watchCount() == 0) { 80 | replyFailure(context.getSource(), Component.literal("Not watching any functions")); 81 | return 0; 82 | } 83 | 84 | FunctionReport report = PROFILER.getReport(sortByColumn, ascending); 85 | 86 | try { 87 | report.writeToFile(); 88 | } catch (Exception e) { 89 | replyFailure(context.getSource(), Component.literal("Failed to write report to file")); 90 | Datamancer.logError("Could not write function report to " + FunctionReport.OUTPUT_PATH, e); 91 | return -1; 92 | } 93 | 94 | if (!context.getSource().isPlayer() || context.getSource().getServer().isDedicatedServer()) { 95 | replySuccess(context.getSource(), Component.literal("Report saved")); 96 | return PROFILER.watchCount(); 97 | } 98 | 99 | Component fileLink = Component 100 | .literal(FunctionReport.OUTPUT_PATH.toString()) 101 | .withStyle(ChatFormatting.UNDERLINE) 102 | .withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_FILE, FunctionReport.OUTPUT_PATH.toAbsolutePath().toString()))); 103 | 104 | replySuccessClientside(Component.literal("Report saved to ").append(fileLink)); 105 | 106 | return PROFILER.watchCount(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/watch/DataPackWatcher.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.watch; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import com.mojang.serialization.Codec; 5 | import com.mojang.serialization.codecs.RecordCodecBuilder; 6 | import com.sun.nio.file.ExtendedWatchEventModifier; 7 | import net.minecraft.server.MinecraftServer; 8 | import net.minecraft.server.packs.repository.PackRepository; 9 | import net.minecraft.world.level.storage.LevelResource; 10 | import org.apache.commons.io.FilenameUtils; 11 | import xyz.trivaxy.datamancer.Datamancer; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.nio.file.*; 16 | import java.util.*; 17 | import java.util.concurrent.ExecutorService; 18 | import java.util.concurrent.Executors; 19 | 20 | public class DataPackWatcher { 21 | 22 | private ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor(); 23 | private State watcherState; 24 | 25 | private static final Set watchedFileExtensions = ImmutableSet.of( 26 | "mcfunction", 27 | "json", 28 | "nbt", 29 | "mcmeta" 30 | ); 31 | 32 | public DataPackWatcher(State state) { 33 | watcherState = state; 34 | } 35 | 36 | public void watchPack(String id) { 37 | watcherState.watchedPackIds.add(id); 38 | } 39 | 40 | public void unwatchPack(String id) { 41 | watcherState.watchedPackIds.remove(id); 42 | } 43 | 44 | public boolean isWatching(String id) { 45 | return watcherState.watchedPackIds.contains(id); 46 | } 47 | 48 | public void start(MinecraftServer server) { 49 | PackRepository repo = server.getPackRepository(); 50 | Path datapacksFolder = server.getWorldPath(LevelResource.DATAPACK_DIR); 51 | 52 | try { 53 | EXECUTOR_SERVICE.execute(() -> { 54 | try { 55 | WatchService watchService = FileSystems.getDefault().newWatchService(); 56 | datapacksFolder.register( 57 | watchService, 58 | new WatchEvent.Kind[] { 59 | StandardWatchEventKinds.ENTRY_MODIFY, 60 | StandardWatchEventKinds.ENTRY_CREATE, 61 | StandardWatchEventKinds.ENTRY_DELETE 62 | }, 63 | ExtendedWatchEventModifier.FILE_TREE 64 | ); 65 | 66 | while (true) { 67 | WatchKey key; 68 | 69 | try { 70 | key = watchService.take(); 71 | } catch (InterruptedException e) { 72 | Thread.currentThread().interrupt(); 73 | break; 74 | } 75 | 76 | Thread.sleep(50); 77 | 78 | List> events = key.pollEvents(); 79 | 80 | if (events.isEmpty() || watcherState.watchedPackIds.isEmpty()) { 81 | key.reset(); 82 | continue; 83 | } 84 | 85 | WatchEvent event = events.get(0); 86 | 87 | if (!shouldAcceptWatchEvent(event)) { 88 | key.reset(); 89 | continue; 90 | } 91 | 92 | Path path = (Path) event.context(); 93 | String packId = "file/" + path.getName(0); 94 | 95 | repo.reload(); 96 | 97 | // if a pack is on the watchlist but not enabled, remove it 98 | if (repo.isAvailable(packId) && !repo.getSelectedIds().contains(packId)) { 99 | unwatchPack(packId); 100 | key.reset(); 101 | continue; 102 | } 103 | 104 | if (!isWatching(packId)) { 105 | key.reset(); 106 | continue; 107 | } 108 | 109 | Datamancer.log("Auto reloading..."); 110 | 111 | List packs = new ArrayList<>(repo.getSelectedIds()); 112 | 113 | server.reloadResources(packs).exceptionally(e -> { 114 | Datamancer.logError("DataPackWatcher failed to reload packs", e); 115 | return null; 116 | }); 117 | 118 | key.reset(); 119 | } 120 | 121 | watchService.close(); 122 | } catch (IOException e) { 123 | Datamancer.logError("Error while watching datapacks folder", e); 124 | } catch (InterruptedException e) { 125 | Thread.currentThread().interrupt(); 126 | } 127 | }); 128 | } catch (Exception e) { 129 | Datamancer.logError("Failed to start DataPackWatcher", e); 130 | } 131 | 132 | watcherState.active = true; 133 | } 134 | 135 | public void stop() { 136 | EXECUTOR_SERVICE.shutdownNow(); 137 | } 138 | 139 | public void shutdown() { 140 | stop(); 141 | watcherState.active = false; 142 | } 143 | 144 | private boolean shouldAcceptWatchEvent(WatchEvent event) { 145 | Path path = (Path) event.context(); 146 | File file = new File(String.valueOf(path)); 147 | 148 | // a directory getting deleted or changed should trigger a reload 149 | if (file.isDirectory()) { 150 | return event.kind() == StandardWatchEventKinds.ENTRY_DELETE || event.kind() == StandardWatchEventKinds.ENTRY_MODIFY; 151 | } 152 | 153 | return watchedFileExtensions.contains(FilenameUtils.getExtension(file.getName())); 154 | } 155 | 156 | public boolean isActive() { 157 | return watcherState.active; 158 | } 159 | 160 | public Collection getWatchList() { 161 | return watcherState.watchedPackIds; 162 | } 163 | 164 | public static class State { 165 | private final Set watchedPackIds; 166 | private boolean active; 167 | 168 | public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( 169 | Codec.STRING.listOf().xmap(Set::copyOf, List::copyOf).fieldOf("watched_pack_ids").forGetter(State::getWatchedPacks), 170 | Codec.BOOL.fieldOf("active").forGetter(State::isActive) 171 | ).apply(instance, State::new)); 172 | 173 | public State(Set watchedPackIds, boolean active) { 174 | this.watchedPackIds = watchedPackIds; 175 | this.active = active; 176 | } 177 | 178 | public static State empty() { 179 | return new State(new HashSet<>(), false); 180 | } 181 | 182 | // for codec 183 | 184 | private Set getWatchedPacks() { 185 | return watchedPackIds; 186 | } 187 | 188 | private boolean isActive() { 189 | return active; 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/command/WatchCommand.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.command; 2 | 3 | import com.mojang.brigadier.CommandDispatcher; 4 | import com.mojang.brigadier.arguments.StringArgumentType; 5 | import com.mojang.brigadier.suggestion.SuggestionProvider; 6 | import net.minecraft.commands.CommandSourceStack; 7 | import net.minecraft.commands.Commands; 8 | import net.minecraft.commands.SharedSuggestionProvider; 9 | import net.minecraft.network.chat.Component; 10 | import net.minecraft.network.chat.ComponentUtils; 11 | import net.minecraft.server.packs.repository.Pack; 12 | import net.minecraft.server.packs.repository.PackRepository; 13 | import net.minecraft.server.packs.repository.PackSource; 14 | import xyz.trivaxy.datamancer.Datamancer; 15 | import xyz.trivaxy.datamancer.watch.DataPackWatcher; 16 | 17 | import java.util.Collection; 18 | 19 | public class WatchCommand extends DatamancerCommand { 20 | 21 | private static final SuggestionProvider SELECTED_PACKS = (commandContext, suggestionsBuilder) -> 22 | SharedSuggestionProvider.suggest( 23 | commandContext.getSource().getServer().getPackRepository().getAvailablePacks() 24 | .stream() 25 | .filter(pack -> pack.getPackSource() == PackSource.WORLD) 26 | .map(Pack::getId) 27 | .map(StringArgumentType::escapeIfRequired), suggestionsBuilder); 28 | 29 | @Override 30 | public void register(CommandDispatcher dispatcher, Commands.CommandSelection environment) { 31 | dispatcher.register(Commands.literal("watch") 32 | .requires(source -> source.hasPermission(2)) 33 | .then(Commands.literal("add") 34 | .then(Commands.argument("pack", StringArgumentType.string()) 35 | .suggests(SELECTED_PACKS) 36 | .executes(context -> { 37 | String packName = StringArgumentType.getString(context, "pack"); 38 | DataPackWatcher watcher = Datamancer.getWatcher(); 39 | Pack pack = context.getSource().getServer().getPackRepository().getPack(packName); 40 | 41 | if (pack == null) { 42 | replyFailure(context.getSource(), Component.literal("Unknown pack: ").append(Component.literal(packName))); 43 | return 0; 44 | } 45 | 46 | Component packLink = pack.getChatLink(false); 47 | 48 | if (watcher.isWatching(packName)) { 49 | replyFailure(context.getSource(), Component.literal("Already watching pack ").append(packLink)); 50 | return 0; 51 | } 52 | 53 | watcher.watchPack(packName); 54 | replySuccess(context.getSource(), Component.literal("Started watching pack ").append(packLink)); 55 | 56 | return 1; 57 | }) 58 | ) 59 | ) 60 | .then(Commands.literal("remove") 61 | .then(Commands.argument("pack", StringArgumentType.string()) 62 | .suggests(SELECTED_PACKS) 63 | .executes(context -> { 64 | String packName = StringArgumentType.getString(context, "pack"); 65 | DataPackWatcher watcher = Datamancer.getWatcher(); 66 | Pack pack = context.getSource().getServer().getPackRepository().getPack(packName); 67 | 68 | if (pack == null) { 69 | replyFailure(context.getSource(), Component.literal("Unknown pack: ").append(Component.literal(packName))); 70 | return 0; 71 | } 72 | 73 | Component packLink = pack.getChatLink(false); 74 | 75 | if (!watcher.isWatching(packName)) { 76 | replyFailure(context.getSource(), Component.literal("Not watching pack ").append(packLink)); 77 | return 0; 78 | } 79 | 80 | watcher.unwatchPack(packName); 81 | replySuccess(context.getSource(), Component.literal("Stopped watching pack ").append(packLink)); 82 | 83 | return 1; 84 | }) 85 | ) 86 | ) 87 | .then(Commands.literal("start") 88 | .executes(context -> { 89 | DataPackWatcher watcher = Datamancer.getWatcher(); 90 | 91 | if (watcher.isActive()) { 92 | replyFailure(context.getSource(), Component.literal("Watcher already active")); 93 | return 0; 94 | } 95 | 96 | watcher.start(context.getSource().getServer()); 97 | replySuccess(context.getSource(), Component.literal("Started watcher")); 98 | 99 | return 1; 100 | }) 101 | ) 102 | .then(Commands.literal("stop") 103 | .executes(context -> { 104 | DataPackWatcher watcher = Datamancer.getWatcher(); 105 | 106 | if (!watcher.isActive()) { 107 | replyFailure(context.getSource(), Component.literal("Watcher not active")); 108 | return 0; 109 | } 110 | 111 | watcher.shutdown(); 112 | replySuccess(context.getSource(), Component.literal("Stopped watcher")); 113 | return 1; 114 | }) 115 | ) 116 | .then(Commands.literal("list") 117 | .executes(context -> { 118 | DataPackWatcher watcher = Datamancer.getWatcher(); 119 | Collection watchList = watcher.getWatchList(); 120 | 121 | if (watchList.isEmpty()) { 122 | replySuccess(context.getSource(), Component.literal("Not watching any packs")); 123 | return 1; 124 | } 125 | 126 | replySuccess(context.getSource(), 127 | Component.literal("Currently watching ") 128 | .append(String.valueOf(watchList.size())) 129 | .append(" packs: ") 130 | .append( 131 | ComponentUtils.formatList(watchList, packId -> 132 | getPackLink(context.getSource().getServer().getPackRepository(), packId) 133 | ) 134 | ) 135 | ); 136 | return 1; 137 | }) 138 | ) 139 | ); 140 | } 141 | 142 | private static Component getPackLink(PackRepository repository, String packId) { 143 | Pack pack = repository.getPack(packId); 144 | 145 | if (pack == null) 146 | return null; 147 | 148 | return pack.getChatLink(true); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/xyz/trivaxy/datamancer/command/placeholder/Placeholder.java: -------------------------------------------------------------------------------- 1 | package xyz.trivaxy.datamancer.command.placeholder; 2 | 3 | import com.mojang.brigadier.StringReader; 4 | import com.mojang.brigadier.arguments.ArgumentType; 5 | import com.mojang.brigadier.arguments.FloatArgumentType; 6 | import com.mojang.brigadier.arguments.StringArgumentType; 7 | import com.mojang.brigadier.exceptions.CommandSyntaxException; 8 | import net.minecraft.ChatFormatting; 9 | import net.minecraft.advancements.critereon.NbtPredicate; 10 | import net.minecraft.commands.CommandSourceStack; 11 | import net.minecraft.commands.arguments.*; 12 | import net.minecraft.commands.arguments.coordinates.BlockPosArgument; 13 | import net.minecraft.commands.arguments.coordinates.Coordinates; 14 | import net.minecraft.commands.arguments.selector.EntitySelector; 15 | import net.minecraft.core.BlockPos; 16 | import net.minecraft.network.chat.Component; 17 | import net.minecraft.network.chat.MutableComponent; 18 | import net.minecraft.server.ServerScoreboard; 19 | import net.minecraft.server.commands.data.BlockDataAccessor; 20 | import net.minecraft.server.level.ServerLevel; 21 | import net.minecraft.world.entity.Entity; 22 | import net.minecraft.world.entity.player.Player; 23 | import net.minecraft.world.entity.projectile.ProjectileUtil; 24 | import net.minecraft.world.level.block.entity.BlockEntity; 25 | import net.minecraft.world.level.block.state.BlockState; 26 | import net.minecraft.world.level.block.state.properties.Property; 27 | import net.minecraft.world.level.storage.CommandStorage; 28 | import net.minecraft.world.phys.BlockHitResult; 29 | import net.minecraft.world.phys.EntityHitResult; 30 | import net.minecraft.world.phys.HitResult; 31 | import net.minecraft.world.phys.Vec3; 32 | import net.minecraft.world.scores.Objective; 33 | import net.minecraft.world.scores.ScoreHolder; 34 | import xyz.trivaxy.datamancer.util.OurComponentUtils; 35 | 36 | import java.time.LocalDateTime; 37 | import java.time.format.DateTimeFormatter; 38 | import java.time.format.FormatStyle; 39 | import java.util.Collection; 40 | import java.util.Collections; 41 | import java.util.List; 42 | import java.util.Map; 43 | import java.util.stream.Collectors; 44 | 45 | public class Placeholder { 46 | 47 | private final List> argumentTypes; 48 | private final PlaceholderProcessor processor; 49 | private final int optionals; 50 | 51 | public static final Map PLACEHOLDERS = Map.of( 52 | "score", new PlaceholderBuilder() 53 | .argument(ScoreHolderArgument.scoreHolders()) 54 | .argument(ObjectiveArgument.objective()) 55 | .process((context, arguments) -> { 56 | ServerScoreboard scoreboard = context.getServer().getScoreboard(); 57 | Collection scoreHolders = arguments.get(0).getNames(context, Collections::emptyList); 58 | Objective objective = scoreboard.getObjective(arguments.get(1)); 59 | 60 | if (objective == null) 61 | return Component.literal("None"); 62 | 63 | String result = scoreHolders 64 | .stream() 65 | .filter(s -> scoreboard.getPlayerScoreInfo(s, objective) != null) 66 | .map(s -> String.valueOf(scoreboard.getOrCreatePlayerScore(s, objective).get())) 67 | .collect(Collectors.joining(", ")); 68 | 69 | if (result.isBlank()) 70 | result = "None"; 71 | 72 | return Component.literal(result); 73 | }), 74 | 75 | "entity", new PlaceholderBuilder() 76 | .argument(EntityArgument.entity()) 77 | .optional(NbtPathArgument.nbtPath()) 78 | .process((context, arguments) -> { 79 | EntitySelector entitySelector = arguments.get(0); 80 | Entity entity = null; 81 | 82 | try { 83 | entity = entitySelector.findSingleEntity(context); 84 | } catch (CommandSyntaxException e) { 85 | return Component.literal("None"); 86 | } 87 | 88 | return OurComponentUtils.getPrettyPrintedTag(NbtPredicate.getEntityTagToCompare(entity), arguments.get(1)); 89 | }), 90 | "block", new PlaceholderBuilder() 91 | .argument(BlockPosArgument.blockPos()) 92 | .optional(NbtPathArgument.nbtPath()) 93 | .process((context, arguments) -> { 94 | Coordinates coords = arguments.get(0); 95 | BlockPos pos = coords.getBlockPos(context); 96 | ServerLevel level = context.getLevel(); 97 | 98 | if (!level.isLoaded(pos)) 99 | return Component.literal("Unloaded"); 100 | 101 | BlockEntity blockEntity = level.getBlockEntity(pos); 102 | 103 | if (blockEntity == null) 104 | return Component.literal("None"); 105 | 106 | return OurComponentUtils.getPrettyPrintedTag(new BlockDataAccessor(blockEntity, pos).getData(), arguments.get(1)); 107 | }), 108 | "storage", new PlaceholderBuilder() 109 | .argument(ResourceLocationArgument.id()) 110 | .optional(NbtPathArgument.nbtPath()) 111 | .process((context, arguments) -> { 112 | CommandStorage storage = context.getServer().getCommandStorage(); 113 | 114 | return OurComponentUtils.getPrettyPrintedTag(storage.get(arguments.get(0)), arguments.get(1)); 115 | }), 116 | "list", new PlaceholderBuilder() 117 | .argument(EntityArgument.entities()) 118 | .process((context, argument) -> { 119 | EntitySelector entitySelector = argument.get(0); 120 | List entities = entitySelector.findEntities(context); 121 | 122 | if (entities.isEmpty()) 123 | return Component.literal("None"); 124 | 125 | return OurComponentUtils.joinComponents(entities.stream().map(Entity::getDisplayName).collect(Collectors.toList()), ", "); 126 | }), 127 | "state", new PlaceholderBuilder() 128 | .argument(BlockPosArgument.blockPos()) 129 | .process((context, argument) -> { 130 | Coordinates coords = argument.get(0); 131 | BlockPos pos = coords.getBlockPos(context); 132 | ServerLevel level = context.getLevel(); 133 | 134 | if (!level.isLoaded(pos)) 135 | return Component.literal("Unloaded"); 136 | 137 | return prettyPrintBlockState(level.getBlockState(pos)); 138 | }), 139 | "at", new PlaceholderBuilder() 140 | .optional(EntityArgument.entities()) 141 | .process((context, argument) -> { 142 | EntitySelector entitySelector = argument.get(0); 143 | 144 | if (entitySelector == null) 145 | return prettyPrintPositions(Collections.singletonList(context.getPosition())); 146 | 147 | List entities = entitySelector.findEntities(context); 148 | 149 | if (entities.isEmpty()) 150 | return Component.literal("None"); 151 | 152 | return prettyPrintPositions(entities.stream().map(Entity::position).collect(Collectors.toList())); 153 | }), 154 | "count", new PlaceholderBuilder() 155 | .argument(EntityArgument.entities()) 156 | .process((context, argument) -> { 157 | EntitySelector entitySelector = argument.get(0); 158 | List entities = entitySelector.findEntities(context); 159 | 160 | return Component.literal(String.valueOf(entities.size())); 161 | }), 162 | "time", new PlaceholderBuilder() 163 | .argument(StringArgumentType.word()) 164 | .process((context, argument) -> switch ((String) argument.get(0)) { 165 | case "daytime" -> Component.literal(String.valueOf(context.getLevel().getDayTime() % 24000L)); 166 | case "gametime" -> Component.literal(String.valueOf(context.getLevel().getGameTime() % 2147483647L)); 167 | case "day" -> Component.literal(String.valueOf(context.getLevel().getDayTime() / 24000L % 2147483647L)); 168 | case "realtime" -> 169 | Component.literal(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).format(LocalDateTime.now())); 170 | default -> Component.literal("None"); 171 | }), 172 | "looking", new PlaceholderBuilder() 173 | .optional(EntityArgument.entity()) 174 | .optional(FloatArgumentType.floatArg(0, 100)) 175 | .process((context, argument) -> { 176 | Entity entity = context.getEntity(); 177 | 178 | EntitySelector entitySelector = argument.get(0); 179 | if (entitySelector != null) 180 | entity = entitySelector.findSingleEntity(context); 181 | 182 | float range = 10; 183 | if (argument.get(1) != null) 184 | range = argument.get(1); 185 | 186 | if (entity == null) 187 | return Component.literal("None"); 188 | 189 | HitResult result = ProjectileUtil.getHitResultOnViewVector(entity, entity1 -> !entity1.isSpectator() && entity1.isPickable(), range); 190 | 191 | if (result instanceof BlockHitResult block) { 192 | return prettyPrintBlockState(context.getLevel().getBlockState(block.getBlockPos())); 193 | } else if (result instanceof EntityHitResult entityHit) { 194 | return entityHit.getEntity().getDisplayName(); 195 | } 196 | 197 | throw new IllegalStateException("{looking} failed to run"); 198 | }) 199 | ); 200 | 201 | public Placeholder(List> argumentTypes, int optionals, PlaceholderProcessor processor) { 202 | this.argumentTypes = argumentTypes; 203 | this.optionals = optionals; 204 | this.processor = processor; 205 | } 206 | 207 | public Component expand(CommandSourceStack source, String arguments) throws CommandSyntaxException, PlaceholderException { 208 | Object[] parsed = new Object[argumentTypes.size()]; 209 | int parsedCount = 0; 210 | 211 | if (arguments == null || arguments.isEmpty()) { 212 | assertArgCount(0); 213 | return processor.process(source, new Arguments(parsed)); 214 | } 215 | 216 | StringReader reader = new StringReader(arguments); 217 | 218 | while (reader.canRead() && parsedCount < argumentTypes.size()) { 219 | parsed[parsedCount] = argumentTypes.get(parsedCount).parse(reader); 220 | reader.skipWhitespace(); 221 | parsedCount++; 222 | } 223 | 224 | if (reader.canRead()) 225 | throw new PlaceholderException("Too many arguments, expected at most " + argumentTypes.size()); 226 | 227 | assertArgCount(parsedCount); 228 | 229 | return processor.process(source, new Arguments(parsed)); 230 | } 231 | 232 | private void assertArgCount(int actual) throws PlaceholderException { 233 | int max = argumentTypes.size(); 234 | int min = max - optionals; 235 | 236 | if (actual < min) 237 | throw new PlaceholderException("Expected at least " + min + " arguments, got " + actual); 238 | 239 | if (actual > max) 240 | throw new PlaceholderException("Too many arguments, expected at most " + max); 241 | } 242 | 243 | private static Component prettyPrintBlockState(BlockState state) { 244 | MutableComponent result = state.getBlock().getName().withStyle(ChatFormatting.RED); 245 | List> properties = state.getProperties().stream().toList(); 246 | 247 | if (properties.isEmpty()) 248 | return result; 249 | 250 | result.append(Component.literal("[").withStyle(ChatFormatting.WHITE)); 251 | 252 | for (int i = 0; i < properties.size(); i++) { 253 | result.append(Component.literal(properties.get(i).getName()).withStyle(ChatFormatting.GRAY)); 254 | result.append(Component.literal("=").withStyle(ChatFormatting.WHITE)); 255 | result.append(Component.literal(state.getValue(properties.get(i)).toString()).withStyle(ChatFormatting.AQUA)); 256 | 257 | if (i < properties.size() - 1) 258 | result.append(Component.literal(", ").withStyle(ChatFormatting.WHITE)); 259 | } 260 | 261 | result.append(Component.literal("]").withStyle(ChatFormatting.WHITE)); 262 | 263 | return result; 264 | } 265 | 266 | private static Component prettyPrintPositions(List positions) { 267 | MutableComponent result = Component.empty(); 268 | 269 | for (int i = 0; i < positions.size(); i++) { 270 | Vec3 pos = positions.get(i); 271 | 272 | result.append(Component.literal("[").withStyle(ChatFormatting.WHITE)); 273 | result.append(Component.literal(String.format("%.4f", pos.x)).withStyle(ChatFormatting.AQUA)); 274 | result.append(Component.literal(", ").withStyle(ChatFormatting.WHITE)); 275 | result.append(Component.literal(String.format("%.4f", pos.y)).withStyle(ChatFormatting.AQUA)); 276 | result.append(Component.literal(", ").withStyle(ChatFormatting.WHITE)); 277 | result.append(Component.literal(String.format("%.4f", pos.z)).withStyle(ChatFormatting.AQUA)); 278 | result.append(Component.literal("]").withStyle(ChatFormatting.WHITE)); 279 | 280 | if (i < positions.size() - 1) 281 | result.append(Component.literal(", ").withStyle(ChatFormatting.WHITE)); 282 | } 283 | 284 | return result; 285 | } 286 | 287 | @SuppressWarnings("unchecked") 288 | public static class Arguments { 289 | 290 | private final Object[] arguments; 291 | 292 | public Arguments(Object[] arguments) { 293 | this.arguments = arguments; 294 | } 295 | 296 | public T get(int index) { 297 | return (T) arguments[index]; 298 | } 299 | } 300 | } 301 | --------------------------------------------------------------------------------