From a520e6fcc4d59a425dd5e6648ad4fc7571feb73b Mon Sep 17 00:00:00 2001 From: melontini <104443436+melontini@users.noreply.github.com> Date: Sat, 23 Mar 2024 23:14:59 +0700 Subject: [PATCH] Start implementation --- .github/ISSUE_TEMPLATE/bug_report.yml | 49 +++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/crash_report.yml | 41 +++++++++ .github/workflows/build.yml | 35 ++++++++ .github/workflows/publish.yml | 65 ++++++++++++++ README.md | 29 +++++++ build.gradle | 14 ++++ gradle.properties | 22 +++-- jitpack.yml | 2 + .../me/melontini/commander/Commander.java | 17 ++++ .../melontini/commander/command/Command.java | 9 ++ .../commander/command/CommandType.java | 11 +++ .../commander/command/ConditionedCommand.java | 41 +++++++++ .../command/brigadier/BrigadierCommands.java | 12 +++ .../command/brigadier/ExplodeCommand.java | 29 +++++++ .../command/builtin/BuiltInCommands.java | 33 ++++++++ .../command/builtin/action/CancelCommand.java | 46 ++++++++++ .../builtin/action/CommandCommand.java | 47 +++++++++++ .../command/builtin/action/PrintCommand.java | 23 +++++ .../command/builtin/logic/AllOfCommand.java | 38 +++++++++ .../command/builtin/logic/AnyOfCommand.java | 38 +++++++++ .../builtin/logic/DefaultedCommand.java | 38 +++++++++ .../command/builtin/logic/RandomCommand.java | 34 ++++++++ .../command/selector/BuiltInSelectors.java | 71 ++++++++++++++++ .../command/selector/ConditionedSelector.java | 39 +++++++++ .../commander/command/selector/Selector.java | 9 ++ .../commander/data/DynamicEventManager.java | 75 +++++++++++++++++ .../commander/data/Subscription.java | 61 ++++++++++++++ .../commander/data/types/CommandTypes.java | 33 ++++++++ .../commander/data/types/EventTypes.java | 37 ++++++++ .../commander/data/types/SelectorTypes.java | 22 +++++ .../commander/event/EventContext.java | 38 +++++++++ .../melontini/commander/event/EventType.java | 48 +++++++++++ .../event/builtin/BuiltInEvents.java | 24 ++++++ .../commander/event/builtin/EntityEvents.java | 84 +++++++++++++++++++ .../commander/event/builtin/PlayerEvents.java | 70 ++++++++++++++++ .../commander/event/builtin/ServerTick.java | 47 +++++++++++ .../me/melontini/commander/util/DataType.java | 7 ++ .../melontini/commander/util/MagicCodecs.java | 83 ++++++++++++++++++ 39 files changed, 1414 insertions(+), 12 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/crash_report.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/publish.yml create mode 100644 README.md create mode 100644 jitpack.yml create mode 100644 src/main/java/me/melontini/commander/command/Command.java create mode 100644 src/main/java/me/melontini/commander/command/CommandType.java create mode 100644 src/main/java/me/melontini/commander/command/ConditionedCommand.java create mode 100644 src/main/java/me/melontini/commander/command/brigadier/BrigadierCommands.java create mode 100644 src/main/java/me/melontini/commander/command/brigadier/ExplodeCommand.java create mode 100644 src/main/java/me/melontini/commander/command/builtin/BuiltInCommands.java create mode 100644 src/main/java/me/melontini/commander/command/builtin/action/CancelCommand.java create mode 100644 src/main/java/me/melontini/commander/command/builtin/action/CommandCommand.java create mode 100644 src/main/java/me/melontini/commander/command/builtin/action/PrintCommand.java create mode 100644 src/main/java/me/melontini/commander/command/builtin/logic/AllOfCommand.java create mode 100644 src/main/java/me/melontini/commander/command/builtin/logic/AnyOfCommand.java create mode 100644 src/main/java/me/melontini/commander/command/builtin/logic/DefaultedCommand.java create mode 100644 src/main/java/me/melontini/commander/command/builtin/logic/RandomCommand.java create mode 100644 src/main/java/me/melontini/commander/command/selector/BuiltInSelectors.java create mode 100644 src/main/java/me/melontini/commander/command/selector/ConditionedSelector.java create mode 100644 src/main/java/me/melontini/commander/command/selector/Selector.java create mode 100644 src/main/java/me/melontini/commander/data/DynamicEventManager.java create mode 100644 src/main/java/me/melontini/commander/data/Subscription.java create mode 100644 src/main/java/me/melontini/commander/data/types/CommandTypes.java create mode 100644 src/main/java/me/melontini/commander/data/types/EventTypes.java create mode 100644 src/main/java/me/melontini/commander/data/types/SelectorTypes.java create mode 100644 src/main/java/me/melontini/commander/event/EventContext.java create mode 100644 src/main/java/me/melontini/commander/event/EventType.java create mode 100644 src/main/java/me/melontini/commander/event/builtin/BuiltInEvents.java create mode 100644 src/main/java/me/melontini/commander/event/builtin/EntityEvents.java create mode 100644 src/main/java/me/melontini/commander/event/builtin/PlayerEvents.java create mode 100644 src/main/java/me/melontini/commander/event/builtin/ServerTick.java create mode 100644 src/main/java/me/melontini/commander/util/DataType.java create mode 100644 src/main/java/me/melontini/commander/util/MagicCodecs.java diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..19d45b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,49 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + Before proceeding, please make sure to do this first. 📋 + - Ensure you are running the **latest** version of the mod. + - A similar issue hasn't been filled before. + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: textarea + attributes: + label: Screenshots + description: If applicable, add screenshots to help explain your problem. + validations: + required: false + - type: input + attributes: + label: Logs + description: | + If available, add logs to help identify your problem. + Please upload your log to [Github Gist](https://gist.github.com/), [mslo.gs](https://mclo.gs/) or any other pasting platform. + validations: + required: false + - type: input + id: mod-version + attributes: + label: Mod Version + description: What version of the mod are you running? + validations: + required: true + - type: input + id: mc-version + attributes: + label: Minecraft Version + description: What version of Minecraft are you running? + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a81c8aa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Suggesting Ideas | 💡 + url: https://github.com/constellation-mc/commander/discussions/categories/ideas + about: If you have a suggestion, please post it here. Be sure to check pinned to see how to make a good suggestion! diff --git a/.github/ISSUE_TEMPLATE/crash_report.yml b/.github/ISSUE_TEMPLATE/crash_report.yml new file mode 100644 index 0000000..19c107a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crash_report.yml @@ -0,0 +1,41 @@ +name: Crash Report +description: File a crash report +title: "[Crash]: " +labels: ["crash"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this crash report! + Before proceeding, please make sure to do this first. 📋 + - Ensure you are running the **latest** version of the mod. + - A similar report hasn't been filled before. + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Please explain in detail how this crash happened. + validations: + required: true + - type: input + attributes: + label: Logs + description: | + `latest.log` or `crash-20xx-xx-xx_xx.xx.xx-(client/server).log`. + Please upload your log to [Github Gist](https://gist.github.com/), [mslo.gs](https://mclo.gs/) or any other pasting platform. + validations: + required: true + - type: input + id: mod-version + attributes: + label: Mod Version + description: What version of the mod are you running? + validations: + required: true + - type: input + id: mc-version + attributes: + label: Minecraft Version + description: What version of Minecraft are you running? + validations: + required: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..89f49d6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +# Automatically build the project and run any configured tests for every push +# and submitted pull request. This can help catch issues that only occur on +# certain platforms or Java versions, and provides a first line of defence +# against bad commits. + +name: build +on: [ pull_request, push ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: checkout repository + uses: actions/checkout@v4.1.1 + - name: validate gradle wrapper + uses: gradle/wrapper-validation-action@v2.1.1 + - name: setup jdk 17 + uses: actions/setup-java@v4.1.0 + with: + distribution: 'temurin' + java-version: 17 + cache: gradle + - name: make gradle wrapper executable + run: chmod +x ./gradlew + - name: build + run: ./gradlew build + - name: Retrieve Version #https://stackoverflow.com/questions/71089787/how-to-get-version-name-from-android-gradle-file-in-github-actions + run: | + echo "VERSION_INFORMATION=$(${{github.workspace}}/gradlew -q printVersionName)" >> $GITHUB_ENV + echo "GIT_HASH=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_ENV + - name: capture build artifacts + uses: actions/upload-artifact@v4.3.1 + with: + name: ${{ env.VERSION_INFORMATION }} [${{ env.GIT_HASH }}] #https://stackoverflow.com/questions/58886293/getting-current-branch-and-commit-hash-in-github-action + path: build/libs/ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..24d1ccc --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,65 @@ + +name: publish +on: + workflow_dispatch: + inputs: + version_type: + description: "The type of this version. e.g alpha" + type: choice + default: beta + options: + - release + - beta + - alpha + required: false + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: checkout repository + uses: actions/checkout@v4.1.1 + - name: validate gradle wrapper + uses: gradle/wrapper-validation-action@v2.1.1 + - name: setup jdk 17 + uses: actions/setup-java@v4.1.0 + with: + distribution: 'temurin' + java-version: 17 + - name: make gradle wrapper executable + run: chmod +x ./gradlew + - name: build + run: ./gradlew build + - name: retrieve version #https://stackoverflow.com/questions/71089787/how-to-get-version-name-from-android-gradle-file-in-github-actions + run: | + echo "VERSION_INFORMATION=$(${{github.workspace}}/gradlew -q printVersionName)" >> $GITHUB_ENV + echo "VERSION_PLAIN=$(${{github.workspace}}/gradlew -q printVersion)" >> $GITHUB_ENV + - name: publish minecraft mods + uses: Kir-Antipov/mc-publish@v3.3.0 + with: + version-type: ${{ inputs.version_type }} + changelog-file: CHANGELOG.md + name: ${{ env.VERSION_INFORMATION }} + version: ${{ env.VERSION_PLAIN }} + + game-versions: | + 1.20.1 + + loaders: | + fabric + + java: | + 17 + + github-tag: ${{ env.VERSION_PLAIN }} + github-token: ${{ secrets.GITHUB_TOKEN }} + github-commitish: ${{ github.sha }} + github-prerelease: false + + #modrinth-id: TseYlb0f + modrinth-token: ${{ secrets.MODRINTH_TOKEN }} + modrinth-featured: true + modrinth-unfeature-mode: subset + modrinth-dependencies: | + fabric-api(required) + dark-matter(embedded) diff --git a/README.md b/README.md new file mode 100644 index 0000000..3652ea3 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +## Commander + +### Quick Introduction + +**Commander** introduces a new event system to the data pack format. + +Each file placed in `commander/events` represents an event subscription. Commander mirrors some fabric events under the `commander` namespace. e.g. `server_tick/start` + +```json +{ + "event": "namespace:event", //the event this file subscribes to. + "parameters": null, //optional parameters block + "commands": [ + { + "type": "namespace:command", //command type + "condition": { //optional conditions block. Uses the vanilla predicates system + "condition": "minecraft:random_chance", + "chance": 0.5 + }, + "parameter": null, //misc command parameters + "parameter_2": 2 + } + ] +} +``` + +`parameters` is a block of addition subscription info. Can be omitted if the event does not expect any parameters. + +`commands` block defines actions the event will perform when invoked. Commands don't always interact with the game world, some commands are purely logical (`commander:random`), some are service commands (`commander:cancel`). diff --git a/build.gradle b/build.gradle index 1908485..7ca105d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ plugins { id 'fabric-loom' version '1.5-SNAPSHOT' id 'maven-publish' + id 'io.freefair.lombok' version '8.3' } version = project.mod_version @@ -16,6 +17,7 @@ repositories { // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. // See https://docs.gradle.org/current/userguide/declaring_repositories.html // for more information about repositories. + maven { url 'https://jitpack.io' } } dependencies { @@ -26,6 +28,11 @@ dependencies { // Fabric API. This is technically optional, but you probably want it anyway. modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + + def dmModules = ["base", "mixin", "minecraft", "content", "crash-handler", "data", "mirage", "glitter"] + for (final def module in dmModules) { + modApi include("me.melontini.dark-matter:dark-matter-${module}:${project.dark_matter}") + } } processResources { @@ -70,6 +77,13 @@ jar { } } +sourcesJar { + exclude { + sourceSets.main.allSource.contains it.file + } + from delombok +} + // configure the maven publication publishing { publications { diff --git a/gradle.properties b/gradle.properties index 8567c4a..f3542f7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,17 +1,15 @@ # Done to increase the memory available to gradle. org.gradle.jvmargs=-Xmx1G - # Fabric Properties - # check these on https://modmuss50.me/fabric.html - minecraft_version=1.20.4 - yarn_mappings=24w11a+build.5 - loader_version=0.15.7 - +# check these on https://modmuss50.me/fabric.html +minecraft_version=1.20.1 +yarn_mappings=1.20.1+build.10 +loader_version=0.15.7 # Mod Properties - mod_version = 0.0.1 - maven_group = me.melontini - archives_base_name = commander - +mod_version=0.0.1 +maven_group=me.melontini +archives_base_name=commander # Dependencies - # check this on https://modmuss50.me/fabric.html - fabric_version=0.96.11+1.20.5 +# check this on https://modmuss50.me/fabric.html +fabric_version=0.92.0+1.20.1 +dark_matter=941b55fd97 diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000..1e41e00 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,2 @@ +jdk: + - openjdk17 \ No newline at end of file diff --git a/src/main/java/me/melontini/commander/Commander.java b/src/main/java/me/melontini/commander/Commander.java index dc21607..f3c24ec 100644 --- a/src/main/java/me/melontini/commander/Commander.java +++ b/src/main/java/me/melontini/commander/Commander.java @@ -1,11 +1,28 @@ package me.melontini.commander; +import me.melontini.commander.command.builtin.BuiltInCommands; +import me.melontini.commander.command.selector.BuiltInSelectors; +import me.melontini.commander.data.DynamicEventManager; +import me.melontini.commander.event.builtin.BuiltInEvents; +import me.melontini.dark_matter.api.data.loading.ServerReloadersEvent; import net.fabricmc.api.ModInitializer; +import net.minecraft.util.Identifier; +//TODO: +// Better validation during `apply` +// Wrap most common/server fabric events. public class Commander implements ModInitializer { + public static Identifier id(String path) { + return new Identifier("commander", path); + } + @Override public void onInitialize() { + ServerReloadersEvent.EVENT.register(context -> context.register(new DynamicEventManager())); + BuiltInEvents.init(); + BuiltInCommands.init(); + BuiltInSelectors.init(); } } diff --git a/src/main/java/me/melontini/commander/command/Command.java b/src/main/java/me/melontini/commander/command/Command.java new file mode 100644 index 0000000..3007ecc --- /dev/null +++ b/src/main/java/me/melontini/commander/command/Command.java @@ -0,0 +1,9 @@ +package me.melontini.commander.command; + +import me.melontini.commander.event.EventContext; + + +public interface Command { + boolean execute(EventContext context); + CommandType type(); +} diff --git a/src/main/java/me/melontini/commander/command/CommandType.java b/src/main/java/me/melontini/commander/command/CommandType.java new file mode 100644 index 0000000..671283e --- /dev/null +++ b/src/main/java/me/melontini/commander/command/CommandType.java @@ -0,0 +1,11 @@ +package me.melontini.commander.command; + +import com.mojang.serialization.Codec; + +public record CommandType(Codec codec) { + + @Override + public boolean equals(Object obj) { + return obj == this; + } +} diff --git a/src/main/java/me/melontini/commander/command/ConditionedCommand.java b/src/main/java/me/melontini/commander/command/ConditionedCommand.java new file mode 100644 index 0000000..7f27c77 --- /dev/null +++ b/src/main/java/me/melontini/commander/command/ConditionedCommand.java @@ -0,0 +1,41 @@ +package me.melontini.commander.command; + +import com.mojang.serialization.*; +import me.melontini.commander.data.types.CommandTypes; +import me.melontini.commander.event.EventContext; +import me.melontini.commander.util.MagicCodecs; +import net.minecraft.loot.condition.LootCondition; + +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +public record ConditionedCommand(Optional condition, Command other) { + + public static final Codec CODEC = new MapCodec() { + @Override + public RecordBuilder encode(ConditionedCommand input, DynamicOps ops, RecordBuilder prefix) { + var r = ((MapCodecCodec) CommandTypes.CODEC).codec().encode(input.other(), ops, prefix); + input.condition().map(condition1 -> MagicCodecs.LOOT_CONDITION.encodeStart(ops, condition1)).ifPresent(tDataResult -> r.add("condition", tDataResult)); + return r; + } + + @Override + public DataResult decode(DynamicOps ops, MapLike input) { + var r = ((MapCodecCodec) CommandTypes.CODEC).codec().decode(ops, input); + T condition = input.get("condition"); + if (condition == null) return r.map(command -> new ConditionedCommand(Optional.empty(), command)); + return r.map(command -> MagicCodecs.LOOT_CONDITION.parse(ops, condition).map(condition1 -> new ConditionedCommand(Optional.of(condition1), command))).flatMap(Function.identity()); + } + + @Override + public Stream keys(DynamicOps ops) { + return Stream.of("condition").map(ops::createString); + } + }.codec(); + + public boolean execute(EventContext context) { + if (!this.condition.map(condition1 -> condition1.test(context.lootContext())).orElse(true)) return false; + return other().execute(context); + } +} diff --git a/src/main/java/me/melontini/commander/command/brigadier/BrigadierCommands.java b/src/main/java/me/melontini/commander/command/brigadier/BrigadierCommands.java new file mode 100644 index 0000000..2c090fe --- /dev/null +++ b/src/main/java/me/melontini/commander/command/brigadier/BrigadierCommands.java @@ -0,0 +1,12 @@ +package me.melontini.commander.command.brigadier; + +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; + +public class BrigadierCommands { + + public static void init() { + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { + ExplodeCommand.register(dispatcher); + }); + } +} diff --git a/src/main/java/me/melontini/commander/command/brigadier/ExplodeCommand.java b/src/main/java/me/melontini/commander/command/brigadier/ExplodeCommand.java new file mode 100644 index 0000000..b2086ba --- /dev/null +++ b/src/main/java/me/melontini/commander/command/brigadier/ExplodeCommand.java @@ -0,0 +1,29 @@ +package me.melontini.commander.command.brigadier; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.FloatArgumentType; +import net.minecraft.command.argument.Vec3ArgumentType; +import net.minecraft.entity.Entity; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +public class ExplodeCommand { + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(CommandManager.literal("cmd:explode").requires(source -> source.hasPermissionLevel(2)) + .executes(context -> execute(context.getSource().getWorld(), context.getSource().getEntity(), context.getSource().getPosition(), 4)) + .then(CommandManager.argument("pos", Vec3ArgumentType.vec3()) + .executes(context -> execute(context.getSource().getWorld(), context.getSource().getEntity(), Vec3ArgumentType.getVec3(context, "pos"), 4)) + .then(CommandManager.argument("power", FloatArgumentType.floatArg(0)) + .executes(context -> execute(context.getSource().getWorld(), context.getSource().getEntity(), Vec3ArgumentType.getVec3(context, "pos"), FloatArgumentType.getFloat(context, "power")))))); + } + + private static int execute(World world, Entity entity, Vec3d vec, float power) { + world.createExplosion(entity, + vec.getX(), vec.getY(), vec.getZ(), + power, World.ExplosionSourceType.TNT); + return 1; + } +} diff --git a/src/main/java/me/melontini/commander/command/builtin/BuiltInCommands.java b/src/main/java/me/melontini/commander/command/builtin/BuiltInCommands.java new file mode 100644 index 0000000..a1df5bb --- /dev/null +++ b/src/main/java/me/melontini/commander/command/builtin/BuiltInCommands.java @@ -0,0 +1,33 @@ +package me.melontini.commander.command.builtin; + +import lombok.experimental.UtilityClass; +import me.melontini.commander.command.CommandType; +import me.melontini.commander.command.brigadier.BrigadierCommands; +import me.melontini.commander.command.builtin.action.CancelCommand; +import me.melontini.commander.command.builtin.action.CommandCommand; +import me.melontini.commander.command.builtin.action.PrintCommand; +import me.melontini.commander.command.builtin.logic.AllOfCommand; +import me.melontini.commander.command.builtin.logic.AnyOfCommand; +import me.melontini.commander.command.builtin.logic.DefaultedCommand; +import me.melontini.commander.command.builtin.logic.RandomCommand; +import me.melontini.commander.data.types.CommandTypes; + +import static me.melontini.commander.Commander.id; + +@UtilityClass +public class BuiltInCommands { + + public static final CommandType RANDOM = CommandTypes.register(id("random"), RandomCommand.CODEC); + public static final CommandType ALL_OF = CommandTypes.register(id("all_of"), AllOfCommand.CODEC); + public static final CommandType ANY_OF = CommandTypes.register(id("any_of"), AnyOfCommand.CODEC); + public static final CommandType DEFAULTED = CommandTypes.register(id("defaulted"), DefaultedCommand.CODEC); + + + public static final CommandType CANCEL = CommandTypes.register(id("cancel"), CancelCommand.CODEC); + public static final CommandType COMMANDS = CommandTypes.register(id("commands"), CommandCommand.CODEC); + public static final CommandType PRINT = CommandTypes.register(id("print"), PrintCommand.CODEC); + + public static void init() { + BrigadierCommands.init(); + } +} diff --git a/src/main/java/me/melontini/commander/command/builtin/action/CancelCommand.java b/src/main/java/me/melontini/commander/command/builtin/action/CancelCommand.java new file mode 100644 index 0000000..6d20161 --- /dev/null +++ b/src/main/java/me/melontini/commander/command/builtin/action/CancelCommand.java @@ -0,0 +1,46 @@ +package me.melontini.commander.command.builtin.action; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; +import me.melontini.commander.command.Command; +import me.melontini.commander.command.CommandType; +import me.melontini.commander.command.builtin.BuiltInCommands; +import me.melontini.commander.event.EventContext; +import me.melontini.commander.event.EventType; +import net.minecraft.util.dynamic.Codecs; + +@Getter +@Accessors(fluent = true) +@RequiredArgsConstructor +public final class CancelCommand implements Command { + + public static final Codec CODEC = Codecs.JSON_ELEMENT.fieldOf("value").xmap(CancelCommand::new, CancelCommand::element).codec(); + + private final JsonElement element; + private Object value;//This is not great + + @Override + public boolean execute(EventContext context) { + var term = context.type().context().get(EventType.CANCEL_TERM); + if (term.isEmpty()) throw new IllegalStateException("Event does not support cancellation"); + + if (value == null) { + value = term.get().parse(JsonOps.INSTANCE, element).getOrThrow(false, string -> { + throw new JsonParseException(string); + }); + } + + context.setReturnValue(value); + return true; + } + + @Override + public CommandType type() { + return BuiltInCommands.CANCEL; + } +} diff --git a/src/main/java/me/melontini/commander/command/builtin/action/CommandCommand.java b/src/main/java/me/melontini/commander/command/builtin/action/CommandCommand.java new file mode 100644 index 0000000..d984410 --- /dev/null +++ b/src/main/java/me/melontini/commander/command/builtin/action/CommandCommand.java @@ -0,0 +1,47 @@ +package me.melontini.commander.command.builtin.action; + +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import me.melontini.commander.command.Command; +import me.melontini.commander.command.CommandType; +import me.melontini.commander.command.builtin.BuiltInCommands; +import me.melontini.commander.command.selector.ConditionedSelector; +import me.melontini.commander.event.EventContext; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.util.Identifier; + +import java.util.List; + +public record CommandCommand(ConditionedSelector selector, Either, Identifier> commands) implements Command { + + public static final Codec CODEC = RecordCodecBuilder.create(data -> data.group( + ConditionedSelector.CODEC.fieldOf("selector").forGetter(CommandCommand::selector), + Codec.either(Codec.STRING.listOf(), Identifier.CODEC).fieldOf("commands").forGetter(CommandCommand::commands) + ).apply(data, CommandCommand::new)); + + @Override + public boolean execute(EventContext context) { + var opt = selector().select(context).map(ServerCommandSource::withSilent); + if (opt.isEmpty()) return false; + var server = context.lootContext().getWorld().getServer(); + + if (commands().left().isPresent()) { + for (String command : commands().left().get()) { + server.getCommandManager().executeWithPrefix(opt.get(), command); + } + } + if (commands().right().isPresent()) { + var func = server.getCommandFunctionManager().getFunction(commands().right().get()); + func.ifPresentOrElse(commandFunction -> server.getCommandFunctionManager().execute(commandFunction, opt.get()), () -> { + throw new IllegalStateException("Unknown function %s!".formatted(commands().right().orElseThrow())); + }); + } + return true; + } + + @Override + public CommandType type() { + return BuiltInCommands.COMMANDS; + } +} diff --git a/src/main/java/me/melontini/commander/command/builtin/action/PrintCommand.java b/src/main/java/me/melontini/commander/command/builtin/action/PrintCommand.java new file mode 100644 index 0000000..325b493 --- /dev/null +++ b/src/main/java/me/melontini/commander/command/builtin/action/PrintCommand.java @@ -0,0 +1,23 @@ +package me.melontini.commander.command.builtin.action; + +import com.mojang.serialization.Codec; +import me.melontini.commander.command.Command; +import me.melontini.commander.command.CommandType; +import me.melontini.commander.command.builtin.BuiltInCommands; +import me.melontini.commander.event.EventContext; + +public record PrintCommand(String text) implements Command { + + public static final Codec CODEC = Codec.STRING.fieldOf("text").xmap(PrintCommand::new, PrintCommand::text).codec(); + + @Override + public boolean execute(EventContext context) { + System.out.println(text()); + return true; + } + + @Override + public CommandType type() { + return BuiltInCommands.PRINT; + } +} diff --git a/src/main/java/me/melontini/commander/command/builtin/logic/AllOfCommand.java b/src/main/java/me/melontini/commander/command/builtin/logic/AllOfCommand.java new file mode 100644 index 0000000..9dc864b --- /dev/null +++ b/src/main/java/me/melontini/commander/command/builtin/logic/AllOfCommand.java @@ -0,0 +1,38 @@ +package me.melontini.commander.command.builtin.logic; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import me.melontini.commander.command.Command; +import me.melontini.commander.command.CommandType; +import me.melontini.commander.command.ConditionedCommand; +import me.melontini.commander.command.builtin.BuiltInCommands; +import me.melontini.commander.event.EventContext; +import me.melontini.dark_matter.api.data.codecs.ExtraCodecs; + +import java.util.List; +import java.util.Optional; + +public record AllOfCommand(List commands, Optional then) implements Command { + + public static final Codec CODEC = RecordCodecBuilder.create(data -> data.group( + ExtraCodecs.list(ConditionedCommand.CODEC).fieldOf("commands").forGetter(AllOfCommand::commands), + ExtraCodecs.optional("then", ConditionedCommand.CODEC).forGetter(AllOfCommand::then) + ).apply(data, AllOfCommand::new)); + + @Override + public boolean execute(EventContext context) { + boolean b = true; + for (ConditionedCommand command : commands()) { + b &= command.execute(context); + } + if (b) { + return then().map(conditionedCommand -> conditionedCommand.execute(context)).orElse(true); + } + return false; + } + + @Override + public CommandType type() { + return BuiltInCommands.ALL_OF; + } +} diff --git a/src/main/java/me/melontini/commander/command/builtin/logic/AnyOfCommand.java b/src/main/java/me/melontini/commander/command/builtin/logic/AnyOfCommand.java new file mode 100644 index 0000000..ef3e3b4 --- /dev/null +++ b/src/main/java/me/melontini/commander/command/builtin/logic/AnyOfCommand.java @@ -0,0 +1,38 @@ +package me.melontini.commander.command.builtin.logic; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import me.melontini.commander.command.Command; +import me.melontini.commander.command.CommandType; +import me.melontini.commander.command.ConditionedCommand; +import me.melontini.commander.command.builtin.BuiltInCommands; +import me.melontini.commander.event.EventContext; +import me.melontini.dark_matter.api.data.codecs.ExtraCodecs; + +import java.util.List; +import java.util.Optional; + +public record AnyOfCommand(List commands, Optional then) implements Command { + + public static final Codec CODEC = RecordCodecBuilder.create(data -> data.group( + ExtraCodecs.list(ConditionedCommand.CODEC).fieldOf("commands").forGetter(AnyOfCommand::commands), + ExtraCodecs.optional("then", ConditionedCommand.CODEC).forGetter(AnyOfCommand::then) + ).apply(data, AnyOfCommand::new)); + + @Override + public boolean execute(EventContext context) { + boolean b = false; + for (ConditionedCommand command : commands()) { + b |= command.execute(context); + } + if (b) { + return then().map(conditionedCommand -> conditionedCommand.execute(context)).orElse(true); + } + return false; + } + + @Override + public CommandType type() { + return BuiltInCommands.ANY_OF; + } +} diff --git a/src/main/java/me/melontini/commander/command/builtin/logic/DefaultedCommand.java b/src/main/java/me/melontini/commander/command/builtin/logic/DefaultedCommand.java new file mode 100644 index 0000000..3c94b51 --- /dev/null +++ b/src/main/java/me/melontini/commander/command/builtin/logic/DefaultedCommand.java @@ -0,0 +1,38 @@ +package me.melontini.commander.command.builtin.logic; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import me.melontini.commander.command.Command; +import me.melontini.commander.command.CommandType; +import me.melontini.commander.command.ConditionedCommand; +import me.melontini.commander.command.builtin.BuiltInCommands; +import me.melontini.commander.event.EventContext; +import me.melontini.dark_matter.api.data.codecs.ExtraCodecs; + +import java.util.List; +import java.util.Optional; + +public record DefaultedCommand(List commands, Optional then) implements Command { + + public static final Codec CODEC = RecordCodecBuilder.create(data -> data.group( + ExtraCodecs.list(ConditionedCommand.CODEC).fieldOf("commands").forGetter(DefaultedCommand::commands), + ExtraCodecs.optional("then", ConditionedCommand.CODEC).forGetter(DefaultedCommand::then) + ).apply(data, DefaultedCommand::new)); + + @Override + public boolean execute(EventContext context) { + boolean b = false; + for (ConditionedCommand command : commands()) { + b |= command.execute(context); + } + if (!b) { + return then().map(conditionedCommand -> conditionedCommand.execute(context)).orElse(false); + } + return true; + } + + @Override + public CommandType type() { + return BuiltInCommands.DEFAULTED; + } +} diff --git a/src/main/java/me/melontini/commander/command/builtin/logic/RandomCommand.java b/src/main/java/me/melontini/commander/command/builtin/logic/RandomCommand.java new file mode 100644 index 0000000..8e8ae83 --- /dev/null +++ b/src/main/java/me/melontini/commander/command/builtin/logic/RandomCommand.java @@ -0,0 +1,34 @@ +package me.melontini.commander.command.builtin.logic; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import me.melontini.commander.command.Command; +import me.melontini.commander.command.CommandType; +import me.melontini.commander.command.ConditionedCommand; +import me.melontini.commander.command.builtin.BuiltInCommands; +import me.melontini.commander.event.EventContext; +import me.melontini.dark_matter.api.data.codecs.ExtraCodecs; +import net.minecraft.util.collection.WeightedList; + +public record RandomCommand(WeightedList commands, int rolls) implements Command { + + public static final Codec CODEC = RecordCodecBuilder.create(data -> data.group( + ExtraCodecs.weightedList(ConditionedCommand.CODEC).fieldOf("commands").forGetter(RandomCommand::commands), + ExtraCodecs.optional("rolls", Codec.INT, 1).forGetter(RandomCommand::rolls) + ).apply(data, RandomCommand::new)); + + @Override + public boolean execute(EventContext context) { + boolean b = false; + for (int i = 0; i < rolls(); i++) { + var itr = this.commands().shuffle().iterator(); + if (itr.hasNext()) b |= itr.next().execute(context); + } + return b; + } + + @Override + public CommandType type() { + return BuiltInCommands.RANDOM; + } +} diff --git a/src/main/java/me/melontini/commander/command/selector/BuiltInSelectors.java b/src/main/java/me/melontini/commander/command/selector/BuiltInSelectors.java new file mode 100644 index 0000000..fbb70df --- /dev/null +++ b/src/main/java/me/melontini/commander/command/selector/BuiltInSelectors.java @@ -0,0 +1,71 @@ +package me.melontini.commander.command.selector; + +import lombok.experimental.UtilityClass; +import me.melontini.commander.data.types.SelectorTypes; +import me.melontini.dark_matter.api.base.util.Utilities; +import me.melontini.dark_matter.api.minecraft.util.TextUtil; +import net.minecraft.entity.Entity; +import net.minecraft.loot.context.LootContextParameters; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec2f; +import net.minecraft.util.math.Vec3d; + +import static me.melontini.commander.Commander.id; + +@UtilityClass +@SuppressWarnings("unused") +public final class BuiltInSelectors { + + public static final Selector SERVER = SelectorTypes.register(mc("server"), context -> context.lootContext().getWorld().getServer().getCommandSource()); + public static final Selector ORIGIN = SelectorTypes.register(mc("origin"), context -> { + var world = context.lootContext().getWorld(); + var o = context.lootContext().requireParameter(LootContextParameters.ORIGIN); + + return new ServerCommandSource(world.getServer(), o, Vec2f.ZERO, + world, 4, "Origin", TextUtil.literal("Origin"), + world.getServer(), null); + }); + public static final Selector THIS_ENTITY = SelectorTypes.register(mc("this_entity"), context -> forEntity(context.lootContext().requireParameter(LootContextParameters.THIS_ENTITY))); + public static final Selector KILLER_ENTITY = SelectorTypes.register(mc("killer_entity"), context -> forEntity(context.lootContext().requireParameter(LootContextParameters.KILLER_ENTITY))); + public static final Selector DIRECT_KILLER_ENTITY = SelectorTypes.register(mc("direct_killer_entity"), context -> forEntity(context.lootContext().requireParameter(LootContextParameters.DIRECT_KILLER_ENTITY))); + public static final Selector LAST_DAMAGE_PLAYER = SelectorTypes.register(mc("last_damage_player"), context -> forEntity(context.lootContext().requireParameter(LootContextParameters.LAST_DAMAGE_PLAYER))); + public static final Selector BLOCK_ENTITY = SelectorTypes.register(mc("block_entity"), context -> { + var be = context.lootContext().requireParameter(LootContextParameters.BLOCK_ENTITY); + return new ServerCommandSource(be.getWorld().getServer(), Vec3d.ofCenter(be.getPos()), Vec2f.ZERO, + (ServerWorld) be.getWorld(), 4, "BlockEntity", TextUtil.literal("BlockEntity"), + be.getWorld().getServer(), null); + }); + + public static final Selector DAMAGE_SOURCE_SOURCE = SelectorTypes.register(id("damage_source/source"), context -> { + var s = context.lootContext().requireParameter(LootContextParameters.DAMAGE_SOURCE).getSource(); + return s != null ? forEntity(s) : null; + }); + public static final Selector DAMAGE_SOURCE_ATTACKER = SelectorTypes.register(id("damage_source/attacker"), context -> { + var s = context.lootContext().requireParameter(LootContextParameters.DAMAGE_SOURCE).getAttacker(); + return s != null ? forEntity(s) : null; + }); + + public static final Selector RANDOM_PLAYER = SelectorTypes.register(id("random_player"), context -> { + var l = context.lootContext().getWorld().getServer().getPlayerManager().getPlayerList(); + if (l == null || l.isEmpty()) return null; + return forEntity(Utilities.pickAtRandom(l)); + }); + + public static void init() { + } + + public static Identifier mc(String string) { + return new Identifier("minecraft", string); + } + + public static ServerCommandSource forEntity(Entity entity) { + return new ServerCommandSource( + entity.getWorld().getServer(), entity.getPos(), + new Vec2f(entity.getPitch(), entity.getYaw()), + (ServerWorld) entity.getWorld(), 4, entity.getEntityName(), entity.getName(), + entity.getWorld().getServer(), entity + ); + } +} diff --git a/src/main/java/me/melontini/commander/command/selector/ConditionedSelector.java b/src/main/java/me/melontini/commander/command/selector/ConditionedSelector.java new file mode 100644 index 0000000..56eec4c --- /dev/null +++ b/src/main/java/me/melontini/commander/command/selector/ConditionedSelector.java @@ -0,0 +1,39 @@ +package me.melontini.commander.command.selector; + +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import me.melontini.commander.data.types.SelectorTypes; +import me.melontini.commander.event.EventContext; +import me.melontini.commander.util.MagicCodecs; +import me.melontini.dark_matter.api.data.codecs.ExtraCodecs; +import net.minecraft.loot.condition.LootCondition; +import net.minecraft.loot.context.LootContext; +import net.minecraft.loot.context.LootContextParameterSet; +import net.minecraft.loot.context.LootContextParameters; +import net.minecraft.loot.context.LootContextTypes; +import net.minecraft.server.command.ServerCommandSource; + +import java.util.Optional; +import java.util.function.Function; + +public record ConditionedSelector(Optional condition, Selector other) { + + public static final Codec CODEC = Codec.either(RecordCodecBuilder.create(data -> data.group( + ExtraCodecs.optional("condition", MagicCodecs.LOOT_CONDITION).forGetter(ConditionedSelector::condition), + SelectorTypes.CODEC.fieldOf("value").forGetter(ConditionedSelector::other) + ).apply(data, ConditionedSelector::new)), SelectorTypes.CODEC).xmap(e -> e.map(Function.identity(), selector -> new ConditionedSelector(Optional.empty(), selector)), Either::left); + + public Optional select(EventContext context) { + var source = other.select(context); + if (source == null) return Optional.empty(); + if (condition.isEmpty()) return Optional.of(source); + + LootContextParameterSet.Builder builder = new LootContextParameterSet.Builder(context.lootContext().getWorld()); + builder.add(LootContextParameters.ORIGIN, source.getPosition()); + builder.addOptional(LootContextParameters.THIS_ENTITY, source.getEntity()); + LootContext sourceContext = new LootContext.Builder(builder.build(LootContextTypes.COMMAND)).build(null); + + return condition.get().test(sourceContext) ? Optional.of(source) : Optional.empty(); + } +} diff --git a/src/main/java/me/melontini/commander/command/selector/Selector.java b/src/main/java/me/melontini/commander/command/selector/Selector.java new file mode 100644 index 0000000..d4fc48a --- /dev/null +++ b/src/main/java/me/melontini/commander/command/selector/Selector.java @@ -0,0 +1,9 @@ +package me.melontini.commander.command.selector; + +import me.melontini.commander.event.EventContext; +import net.minecraft.server.command.ServerCommandSource; +import org.jetbrains.annotations.Nullable; + +public interface Selector { + @Nullable ServerCommandSource select(EventContext context); +} diff --git a/src/main/java/me/melontini/commander/data/DynamicEventManager.java b/src/main/java/me/melontini/commander/data/DynamicEventManager.java new file mode 100644 index 0000000..96a52d2 --- /dev/null +++ b/src/main/java/me/melontini/commander/data/DynamicEventManager.java @@ -0,0 +1,75 @@ +package me.melontini.commander.data; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.mojang.serialization.JsonOps; +import lombok.experimental.Accessors; +import me.melontini.commander.command.ConditionedCommand; +import me.melontini.commander.data.types.EventTypes; +import me.melontini.commander.event.EventType; +import me.melontini.commander.util.DataType; +import me.melontini.dark_matter.api.data.loading.ReloaderType; +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; +import net.minecraft.resource.JsonDataLoader; +import net.minecraft.resource.ResourceManager; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.Identifier; +import net.minecraft.util.profiler.Profiler; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Accessors(fluent = true) +public class DynamicEventManager extends JsonDataLoader implements IdentifiableResourceReloadListener { + + public static final ReloaderType RELOADER = ReloaderType.create(new Identifier("commander:events")); + public static final DataType> DEFAULT = new DataType<>(); + + final Map customData = new IdentityHashMap<>(); + + public DynamicEventManager() { + super(new Gson(), RELOADER.identifier().toString().replace(":", "/")); + } + + public static T getData(MinecraftServer server, EventType type, DataType key) { + return server.dm$getReloader(DynamicEventManager.RELOADER).getData(type, key); + } + + public T getData(EventType type, DataType key) { + return key.cast(this.customData.get(type)); + } + + @Override + protected void apply(Map parsed, ResourceManager manager, Profiler profiler) { + Maps.transformValues(parsed, input -> Subscription.CODEC.parse(JsonOps.INSTANCE, input).getOrThrow(false, string -> { + throw new JsonParseException(string); + })).values().stream().collect(Collectors.groupingBy(Subscription::type)).forEach((eventType, subscriptions) -> { + var finalizer = eventType.context().get(EventType.FINALIZER); + if (finalizer.isPresent()) { + this.customData.put(eventType, finalizer.get().apply(subscriptions)); + return; + } + this.customData.put(eventType, subscriptions.stream().flatMap(s -> s.list().stream()).toList()); + }); + + Sets.difference(EventTypes.types(), customData.keySet()).forEach(type -> { + var finalizer = type.context().get(EventType.FINALIZER); + if (finalizer.isPresent()) { + this.customData.put(type, finalizer.get().apply(Collections.emptyList())); + return; + } + this.customData.put(type, Collections.emptyList()); + }); + } + + @Override + public Identifier getFabricId() { + return RELOADER.identifier(); + } +} diff --git a/src/main/java/me/melontini/commander/data/Subscription.java b/src/main/java/me/melontini/commander/data/Subscription.java new file mode 100644 index 0000000..a493943 --- /dev/null +++ b/src/main/java/me/melontini/commander/data/Subscription.java @@ -0,0 +1,61 @@ +package me.melontini.commander.data; + +import com.mojang.serialization.*; +import me.melontini.commander.command.ConditionedCommand; +import me.melontini.commander.data.types.EventTypes; +import me.melontini.commander.event.EventType; +import me.melontini.dark_matter.api.base.util.Utilities; +import me.melontini.dark_matter.api.data.codecs.ExtraCodecs; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +public record Subscription(EventType type, Object parameters, List list) { + + private static final Codec> LIST_CODEC = ExtraCodecs.list(ConditionedCommand.CODEC); + public static final Codec CODEC = new MapCodec() { + @Override + public RecordBuilder encode(Subscription input, DynamicOps ops, RecordBuilder prefix) { + var map = ops.mapBuilder(); + var r = EventTypes.CODEC.encodeStart(ops, input.type()); + map.add("event", r); + if (!input.list().isEmpty()) map.add("commands", LIST_CODEC.encodeStart(ops, input.list())); + if (input.parameters() != null) { + var opt = input.type().context().get(EventType.EXTENSION); + opt.ifPresent(codec -> map.add("parameters", codec.encodeStart(ops, Utilities.cast(input.parameters())))); + } + return map; + } + + @Override + public DataResult decode(DynamicOps ops, MapLike input) { + T type = input.get("event"); + if (type == null) return DataResult.error(() -> "Missing 'event' field in %s".formatted(input)); + DataResult event = EventTypes.CODEC.parse(ops, type); + + T parameters = input.get("parameters"); + return event.flatMap(eventType -> { + var opt = eventType.context().get(EventType.EXTENSION); + if (opt.isPresent()) { + if (parameters == null) + return DataResult.error(() -> "Missing required 'parameters' field in %s".formatted(input)); + return opt.get().parse(ops, parameters).map(Optional::ofNullable); + } + return DataResult.success(Optional.empty()); + }).map(o -> event.map(eventType -> { + T commands = input.get("commands"); + if (commands != null) + return LIST_CODEC.parse(ops, commands).map(list1 -> new Subscription(eventType, o.orElse(null), list1)); + return DataResult.success(new Subscription(eventType, o.orElse(null), Collections.emptyList())); + })).flatMap(Function.identity()).map(Function.identity()).flatMap(Function.identity()).map(Function.identity()); + } + + @Override + public Stream keys(DynamicOps ops) { + return Stream.of("event", "parameters", "commands").map(ops::createString); + } + }.codec(); +} diff --git a/src/main/java/me/melontini/commander/data/types/CommandTypes.java b/src/main/java/me/melontini/commander/data/types/CommandTypes.java new file mode 100644 index 0000000..5f7e30d --- /dev/null +++ b/src/main/java/me/melontini/commander/data/types/CommandTypes.java @@ -0,0 +1,33 @@ +package me.melontini.commander.data.types; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.mojang.serialization.Codec; +import lombok.experimental.UtilityClass; +import me.melontini.commander.command.Command; +import me.melontini.commander.command.CommandType; +import me.melontini.commander.util.MagicCodecs; +import net.minecraft.util.Identifier; + +@UtilityClass +public final class CommandTypes { + + private static final BiMap COMMANDS = HashBiMap.create(); + private static final Codec TYPE_CODEC = MagicCodecs.mapLookup(COMMANDS); + public static final Codec CODEC = TYPE_CODEC.dispatch("type", Command::type, CommandType::codec); + + public static Identifier getId(CommandType type) { + return COMMANDS.inverse().get(type); + } + + public static CommandType getType(Identifier identifier) { + return COMMANDS.get(identifier); + } + + public static CommandType register(Identifier identifier, Codec codec) { + CommandType type = new CommandType(codec); + var old = COMMANDS.put(identifier, type); + if (old != null) throw new IllegalStateException("Already registered command %s".formatted(identifier)); + return type; + } +} diff --git a/src/main/java/me/melontini/commander/data/types/EventTypes.java b/src/main/java/me/melontini/commander/data/types/EventTypes.java new file mode 100644 index 0000000..7c6009f --- /dev/null +++ b/src/main/java/me/melontini/commander/data/types/EventTypes.java @@ -0,0 +1,37 @@ +package me.melontini.commander.data.types; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.mojang.serialization.Codec; +import lombok.experimental.UtilityClass; +import me.melontini.commander.event.EventType; +import me.melontini.commander.util.MagicCodecs; +import net.minecraft.util.Identifier; + +import java.util.Collections; +import java.util.Set; + +@UtilityClass +public final class EventTypes { + + private static final BiMap EVENTS = HashBiMap.create(); + public static final Codec CODEC = MagicCodecs.mapLookup(EVENTS); + + public static Identifier getId(EventType type) { + return EVENTS.inverse().get(type); + } + + public static EventType getType(Identifier identifier) { + return EVENTS.get(identifier); + } + + public static Set types() { + return Collections.unmodifiableSet(EVENTS.values()); + } + + public static EventType register(Identifier identifier, EventType type) { + var old = EVENTS.put(identifier, type); + if (old != null) throw new IllegalStateException("Already registered event %s".formatted(identifier)); + return type; + } +} diff --git a/src/main/java/me/melontini/commander/data/types/SelectorTypes.java b/src/main/java/me/melontini/commander/data/types/SelectorTypes.java new file mode 100644 index 0000000..e761cf7 --- /dev/null +++ b/src/main/java/me/melontini/commander/data/types/SelectorTypes.java @@ -0,0 +1,22 @@ +package me.melontini.commander.data.types; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.mojang.serialization.Codec; +import lombok.experimental.UtilityClass; +import me.melontini.commander.command.selector.Selector; +import me.melontini.commander.util.MagicCodecs; +import net.minecraft.util.Identifier; + +@UtilityClass +public final class SelectorTypes { + + private static final BiMap SELECTORS = HashBiMap.create(); + public static final Codec CODEC = MagicCodecs.mapLookup(SELECTORS); + + public static Selector register(Identifier identifier, Selector selector) { + var old = SELECTORS.put(identifier, selector); + if (old != null) throw new IllegalStateException("Already registered selector %s".formatted(identifier)); + return selector; + } +} diff --git a/src/main/java/me/melontini/commander/event/EventContext.java b/src/main/java/me/melontini/commander/event/EventContext.java new file mode 100644 index 0000000..1a7477b --- /dev/null +++ b/src/main/java/me/melontini/commander/event/EventContext.java @@ -0,0 +1,38 @@ +package me.melontini.commander.event; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.With; +import lombok.experimental.Accessors; +import me.melontini.commander.util.DataType; +import net.minecraft.loot.context.LootContext; + +import java.util.concurrent.atomic.AtomicReference; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Accessors(fluent = true) +public final class EventContext { + @Getter + @With + private final LootContext lootContext; + @Getter + private final EventType type; + private final AtomicReference returnValue; + + public EventContext(LootContext lootContext, EventType type) { + this.lootContext = lootContext; + this.type = type; + this.returnValue = new AtomicReference<>(); + } + + public void setReturnValue(Object value) { + returnValue.set(value); + } + + public T getReturnValue(DataType type, T def) { + var r = returnValue.get(); + if (r == null) return def; + return (T) r; + } +} diff --git a/src/main/java/me/melontini/commander/event/EventType.java b/src/main/java/me/melontini/commander/event/EventType.java new file mode 100644 index 0000000..d16d15b --- /dev/null +++ b/src/main/java/me/melontini/commander/event/EventType.java @@ -0,0 +1,48 @@ +package me.melontini.commander.event; + +import com.mojang.serialization.Codec; +import me.melontini.commander.data.Subscription; +import me.melontini.commander.util.DataType; +import me.melontini.dark_matter.api.base.util.Context; + +import java.util.List; +import java.util.function.Function; + +public record EventType(Context context) { + + public static final Context.Key> EXTENSION = Context.key("extension"); + public static final Context.Key, ?>> FINALIZER = Context.key("finalizer"); + public static final Context.Key> CANCEL_TERM = Context.key("cancel_term"); + + @Override + public boolean equals(Object obj) { + return obj == this; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final Context.Builder builder = Context.builder(); + + public Builder extension(Codec extension) { + builder.put(EXTENSION, extension); + return this; + } + + public Builder finalizer(DataType type, Function, C> finalizer) { + builder.put(FINALIZER, finalizer); + return this; + } + + public Builder cancelTerm(Codec returnCodec) { + builder.put(CANCEL_TERM, returnCodec); + return this; + } + + public EventType build() { + return new EventType(this.builder.build()); + } + } +} diff --git a/src/main/java/me/melontini/commander/event/builtin/BuiltInEvents.java b/src/main/java/me/melontini/commander/event/builtin/BuiltInEvents.java new file mode 100644 index 0000000..96ba667 --- /dev/null +++ b/src/main/java/me/melontini/commander/event/builtin/BuiltInEvents.java @@ -0,0 +1,24 @@ +package me.melontini.commander.event.builtin; + +import lombok.experimental.UtilityClass; +import net.minecraft.entity.Entity; +import net.minecraft.loot.context.LootContextParameterSet; +import net.minecraft.loot.context.LootContextParameters; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.Vec3d; + +@UtilityClass +public class BuiltInEvents { + public static void init() { + ServerTick.init(); + EntityEvents.init(); + PlayerEvents.init(); + } + + public static LootContextParameterSet.Builder builder(Entity entity, ServerWorld world, Vec3d pos) { + LootContextParameterSet.Builder builder = new LootContextParameterSet.Builder(world); + builder.add(LootContextParameters.THIS_ENTITY, entity); + builder.add(LootContextParameters.ORIGIN, pos); + return builder; + } +} diff --git a/src/main/java/me/melontini/commander/event/builtin/EntityEvents.java b/src/main/java/me/melontini/commander/event/builtin/EntityEvents.java new file mode 100644 index 0000000..6adc8f6 --- /dev/null +++ b/src/main/java/me/melontini/commander/event/builtin/EntityEvents.java @@ -0,0 +1,84 @@ +package me.melontini.commander.event.builtin; + +import com.mojang.serialization.Codec; +import lombok.experimental.UtilityClass; +import me.melontini.commander.command.ConditionedCommand; +import me.melontini.commander.data.DynamicEventManager; +import me.melontini.commander.data.types.EventTypes; +import me.melontini.commander.event.EventContext; +import me.melontini.commander.event.EventType; +import me.melontini.dark_matter.api.base.util.MakeSure; +import net.fabricmc.fabric.api.entity.event.v1.EntityElytraEvents; +import net.fabricmc.fabric.api.entity.event.v1.EntitySleepEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; +import net.minecraft.entity.Entity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.loot.context.LootContext; +import net.minecraft.loot.context.LootContextParameterSet; +import net.minecraft.loot.context.LootContextParameters; +import net.minecraft.loot.context.LootContextTypes; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.Vec3d; + +import static me.melontini.commander.Commander.id; + +@UtilityClass +public class EntityEvents { + + public static final EventType ALLOW_DAMAGE = EventTypes.register(id("allow_damage"), EventType.builder().cancelTerm(Codec.BOOL).build()); + public static final EventType ALLOW_DEATH = EventTypes.register(id("allow_death"), EventType.builder().cancelTerm(Codec.BOOL).build()); + public static final EventType AFTER_DEATH = EventTypes.register(id("after_death"), EventType.builder().build()); + + //public static final EventType ALLOW_SLEEPING = EventTypes.register(id("allow_sleeping"), EventType.builder().cancelTerm(Codec.BOOL).build()); + public static final EventType START_SLEEPING = EventTypes.register(id("start_sleeping"), EventType.builder().build()); + public static final EventType STOP_SLEEPING = EventTypes.register(id("stop_sleeping"), EventType.builder().build()); + + public static final EventType ALLOW_ELYTRA = EventTypes.register(id("allow_elytra_flight"), EventType.builder().cancelTerm(Codec.BOOL).build()); + + public static void init() { + ServerLivingEntityEvents.ALLOW_DAMAGE.register((entity, source, amount) -> executeReturn(ALLOW_DAMAGE, entity, source.getPosition(), source)); + ServerLivingEntityEvents.ALLOW_DEATH.register((entity, source, damageAmount) -> executeReturn(ALLOW_DEATH, entity, source.getPosition(), source)); + + ServerLivingEntityEvents.AFTER_DEATH.register((entity, source) -> execute(AFTER_DEATH, entity, source.getPosition(), source)); + + EntitySleepEvents.START_SLEEPING.register((entity, sleepingPos) -> execute(START_SLEEPING, entity, Vec3d.ofCenter(sleepingPos), null)); + EntitySleepEvents.STOP_SLEEPING.register((entity, sleepingPos) -> execute(STOP_SLEEPING, entity, Vec3d.ofCenter(sleepingPos), null)); + + EntityElytraEvents.ALLOW.register(entity -> executeReturn(ALLOW_ELYTRA, entity, entity.getPos(), null)); + } + + private static void execute(EventType type, Entity entity, Vec3d origin, DamageSource source) { + if (entity.getWorld().isClient()) return; + var subscribers = DynamicEventManager.getData(MakeSure.notNull(entity.getServer()), type, DynamicEventManager.DEFAULT); + if (subscribers.isEmpty()) return; + + var eventContext = makeContext(type, entity, origin, source); + for (ConditionedCommand subscriber : subscribers) subscriber.execute(eventContext); + } + + private static boolean executeReturn(EventType type, Entity entity, Vec3d origin, DamageSource source) { + if (entity.getWorld().isClient()) return true; + var subscribers = DynamicEventManager.getData(MakeSure.notNull(entity.getServer()), type, DynamicEventManager.DEFAULT); + if (subscribers.isEmpty()) return true; + + var eventContext = makeContext(type, entity, origin, source); + for (ConditionedCommand subscriber : subscribers) { + subscriber.execute(eventContext); + if (!eventContext.getReturnValue(null, true)) return false; + } + return true; + } + + private static EventContext makeContext(EventType type, Entity entity, Vec3d origin, DamageSource source) { + LootContextParameterSet.Builder builder = new LootContextParameterSet.Builder((ServerWorld) entity.getWorld()); + builder.add(LootContextParameters.THIS_ENTITY, entity); + builder.add(LootContextParameters.ORIGIN, origin); + if (source != null) { + builder.add(LootContextParameters.DAMAGE_SOURCE, source); + builder.addOptional(LootContextParameters.DIRECT_KILLER_ENTITY, source.getAttacker()); + builder.addOptional(LootContextParameters.KILLER_ENTITY, source.getSource()); + } + LootContext context = new LootContext.Builder(builder.build(source == null ? LootContextTypes.COMMAND : LootContextTypes.ENTITY)).build(null); + return new EventContext(context, type); + } +} diff --git a/src/main/java/me/melontini/commander/event/builtin/PlayerEvents.java b/src/main/java/me/melontini/commander/event/builtin/PlayerEvents.java new file mode 100644 index 0000000..7e7636e --- /dev/null +++ b/src/main/java/me/melontini/commander/event/builtin/PlayerEvents.java @@ -0,0 +1,70 @@ +package me.melontini.commander.event.builtin; + +import lombok.experimental.UtilityClass; +import me.melontini.commander.command.ConditionedCommand; +import me.melontini.commander.data.DynamicEventManager; +import me.melontini.commander.data.types.EventTypes; +import me.melontini.commander.event.EventContext; +import me.melontini.commander.event.EventType; +import me.melontini.commander.util.MagicCodecs; +import me.melontini.dark_matter.api.base.util.MakeSure; +import net.fabricmc.fabric.api.event.player.AttackBlockCallback; +import net.fabricmc.fabric.api.event.player.UseBlockCallback; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.loot.context.LootContext; +import net.minecraft.loot.context.LootContextParameterSet; +import net.minecraft.loot.context.LootContextParameters; +import net.minecraft.loot.context.LootContextTypes; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import java.util.function.Supplier; + +import static me.melontini.commander.Commander.id; + +@UtilityClass +public class PlayerEvents { + + public static final EventType ATTACK_BLOCK = EventTypes.register(id("player_attack/block"), EventType.builder().cancelTerm(MagicCodecs.enumCodec(ActionResult.class)).build()); + public static final EventType USE_BLOCK = EventTypes.register(id("player_use/block"), EventType.builder().cancelTerm(MagicCodecs.enumCodec(ActionResult.class)).build()); + + static void init() { + AttackBlockCallback.EVENT.register((player, world, hand, pos, direction) -> blockCallback(ATTACK_BLOCK, world, player, hand, Vec3d.ofCenter(pos), pos)); + UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> blockCallback(USE_BLOCK, world, player, hand, hitResult.getPos(), hitResult.getBlockPos())); + } + + private static ActionResult blockCallback(EventType type, World world, PlayerEntity player, Hand hand, Vec3d origin, BlockPos pos) { + if (world.isClient()) return ActionResult.PASS; + return executeReturn(type, world, () -> { + ItemStack tool = player.getStackInHand(hand); + BlockState state = world.getBlockState(pos); + BlockEntity blockEntity = world.getBlockEntity(pos); + + LootContextParameterSet.Builder builder = BuiltInEvents.builder(player, (ServerWorld) world, origin); + builder.add(LootContextParameters.BLOCK_STATE, state); + builder.add(LootContextParameters.TOOL, tool); + builder.addOptional(LootContextParameters.BLOCK_ENTITY, blockEntity); + return new LootContext.Builder(builder.build(LootContextTypes.BLOCK)).build(null); + }); + } + + private static ActionResult executeReturn(EventType type, World world, Supplier context) { + var subscribers = DynamicEventManager.getData(MakeSure.notNull(world.getServer()), type, DynamicEventManager.DEFAULT); + if (subscribers.isEmpty()) return ActionResult.PASS; + + var eventContext = new EventContext(context.get(), type); + for (ConditionedCommand subscriber : subscribers) { + subscriber.execute(eventContext); + ActionResult r = eventContext.getReturnValue(null, null); + if (r != null && r != ActionResult.PASS) return r; + } + return ActionResult.PASS; + } +} diff --git a/src/main/java/me/melontini/commander/event/builtin/ServerTick.java b/src/main/java/me/melontini/commander/event/builtin/ServerTick.java new file mode 100644 index 0000000..3ec30c8 --- /dev/null +++ b/src/main/java/me/melontini/commander/event/builtin/ServerTick.java @@ -0,0 +1,47 @@ +package me.melontini.commander.event.builtin; + +import lombok.experimental.UtilityClass; +import me.melontini.commander.command.ConditionedCommand; +import me.melontini.commander.data.DynamicEventManager; +import me.melontini.commander.data.types.EventTypes; +import me.melontini.commander.event.EventContext; +import me.melontini.commander.event.EventType; +import me.melontini.dark_matter.api.base.util.MakeSure; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.minecraft.loot.context.LootContext; +import net.minecraft.loot.context.LootContextParameterSet; +import net.minecraft.loot.context.LootContextParameters; +import net.minecraft.loot.context.LootContextTypes; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.Vec3d; + +import static me.melontini.commander.Commander.id; + +@UtilityClass +public class ServerTick { + + public static final EventType START_TICK = EventTypes.register(id("server_tick/start"), EventType.builder().build()); + public static final EventType END_TICK = EventTypes.register(id("server_tick/end"), EventType.builder().build()); + public static final EventType START_WORLD_TICK = EventTypes.register(id("world_tick/start"), EventType.builder().build()); + public static final EventType END_WORLD_TICK = EventTypes.register(id("world_tick/end"), EventType.builder().build()); + + static void init() { + ServerTickEvents.START_SERVER_TICK.register(server -> tick(server.getOverworld(), START_TICK)); + ServerTickEvents.END_SERVER_TICK.register(server -> tick(server.getOverworld(), END_TICK)); + + ServerTickEvents.START_WORLD_TICK.register((world) -> tick(world, START_WORLD_TICK)); + ServerTickEvents.END_WORLD_TICK.register((world) -> tick(world, END_WORLD_TICK)); + } + + private static void tick(ServerWorld world, EventType type) { + var subscribers = DynamicEventManager.getData(MakeSure.notNull(world.getServer()), type, DynamicEventManager.DEFAULT); + if (subscribers.isEmpty()) return; + + LootContextParameterSet.Builder builder = new LootContextParameterSet.Builder(world); + builder.add(LootContextParameters.ORIGIN, Vec3d.ZERO); + LootContext context = new LootContext.Builder(builder.build(LootContextTypes.COMMAND)).build(null); + EventContext eventContext = new EventContext(context, type); + + for (ConditionedCommand subscriber : subscribers) subscriber.execute(eventContext); + } +} diff --git a/src/main/java/me/melontini/commander/util/DataType.java b/src/main/java/me/melontini/commander/util/DataType.java new file mode 100644 index 0000000..2c20ce3 --- /dev/null +++ b/src/main/java/me/melontini/commander/util/DataType.java @@ -0,0 +1,7 @@ +package me.melontini.commander.util; + +public final class DataType { + public T cast(Object o) { + return (T) o; + } +} diff --git a/src/main/java/me/melontini/commander/util/MagicCodecs.java b/src/main/java/me/melontini/commander/util/MagicCodecs.java new file mode 100644 index 0000000..a64ffde --- /dev/null +++ b/src/main/java/me/melontini/commander/util/MagicCodecs.java @@ -0,0 +1,83 @@ +package me.melontini.commander.util; + +import com.google.common.collect.BiMap; +import com.google.gson.*; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import lombok.experimental.UtilityClass; +import me.melontini.dark_matter.api.base.util.Utilities; +import net.minecraft.loot.LootGsons; +import net.minecraft.loot.condition.LootCondition; +import net.minecraft.loot.condition.LootConditionType; +import net.minecraft.registry.Registries; +import net.minecraft.util.Identifier; +import net.minecraft.util.dynamic.Codecs; + +import java.lang.reflect.Type; +import java.util.Locale; +import java.util.Optional; + +@UtilityClass +public class MagicCodecs { + + public static final GsonContextImpl lootContext = new GsonContextImpl(LootGsons.getConditionGsonBuilder().create()); + + public static final Codec LOOT_CONDITION = Codecs.exceptionCatching(Codecs.JSON_ELEMENT.flatXmap(element -> { + if (!element.isJsonObject()) return DataResult.error(() -> "'%s' not a JsonObject".formatted(element)); + JsonObject object = element.getAsJsonObject(); + if (object.get("condition") == null) return DataResult.error(() -> "Missing required 'condition' field!"); + + LootConditionType type = Registries.LOOT_CONDITION_TYPE.get(Identifier.tryParse(object.get("condition").getAsString())); + if (type == null) + return DataResult.error(() -> "No such condition type '%s'".formatted(object.get("condition").getAsString())); + return DataResult.success((LootCondition) type.getJsonSerializer().fromJson(object, lootContext)); + }, condition -> { + JsonObject object = new JsonObject(); + condition.getType().getJsonSerializer().toJson(object, Utilities.cast(condition), lootContext); + return DataResult.success(object); + })); + + public static > Codec enumCodec(Class cls) { + return Codec.STRING.comapFlatMap(string -> { + try { + return DataResult.success(Enum.valueOf(cls, string.toUpperCase(Locale.ROOT))); + } catch (IllegalArgumentException e) { + return DataResult.error(() -> "No such enum constant %s!".formatted(string)); + } + }, t -> t.name().toLowerCase(Locale.ROOT)); + } + + public static Codec mapLookup(BiMap lookup) { + return Identifier.CODEC.flatXmap( + identifier -> Optional.ofNullable(lookup.get(identifier)) + .map(DataResult::success) + .orElseGet(() -> DataResult.error(() -> "Unknown type: %s".formatted(identifier))), + eventType -> Optional.ofNullable(lookup.inverse().get(eventType)) + .map(DataResult::success) + .orElseGet(() -> DataResult.error(() -> "Unknown type: %s".formatted(eventType)))); + } + + public static final class GsonContextImpl implements JsonSerializationContext, JsonDeserializationContext { + + private final Gson gson; + + public GsonContextImpl(Gson gson) { + this.gson = gson; + } + + @Override + public JsonElement serialize(Object src) { + return gson.toJsonTree(src); + } + + @Override + public JsonElement serialize(Object src, Type typeOfSrc) { + return gson.toJsonTree(src, typeOfSrc); + } + + @Override + public R deserialize(JsonElement json, Type typeOfT) throws JsonParseException { + return gson.fromJson(json, typeOfT); + } + } +}