From 52f523288250c70598d8e1b6a8e463a0a1aaf96e Mon Sep 17 00:00:00 2001 From: mworzala Date: Tue, 16 Aug 2022 01:00:55 +0300 Subject: [PATCH 01/25] initial behavior entries & sanity test of enodia --- .../hollowcube/registry/ResourceFactory.java | 6 ++ modules/entity/build.gradle.kts | 8 ++ modules/entity/libs/enodia-pf-1.0.1.jar | Bin 0 -> 74586 bytes .../unnamed/mmo/entity/UnnamedEntity.java | 4 + .../java/unnamed/mmo/entity/brain/Brain.java | 7 ++ .../mmo/entity/brain/SingleTaskBrain.java | 5 ++ .../mmo/entity/brain/task/AbstractTask.java | 23 +++++ .../mmo/entity/brain/task/IdleTask.java | 58 ++++++++++++ .../mmo/entity/brain/task/SequenceTask.java | 84 ++++++++++++++++++ .../unnamed/mmo/entity/brain/task/Task.java | 39 ++++++++ .../mmo/entity/brain/TestEnodiaPF.java | 73 +++++++++++++++ .../mmo/entity/brain/task/TestIdleTask.java | 26 ++++++ .../entity/brain/task/TestSequenceTask.java | 55 ++++++++++++ .../mmo/entity/brain/task/test/MockBrain.java | 7 ++ .../mmo/entity/brain/task/test/MockTask.java | 24 +++++ .../entity/brain/task/test/TaskSubject.java | 33 +++++++ settings.gradle.kts | 1 + 17 files changed, 453 insertions(+) create mode 100644 modules/entity/build.gradle.kts create mode 100644 modules/entity/libs/enodia-pf-1.0.1.jar create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/task/AbstractTask.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/task/IdleTask.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/TaskSubject.java diff --git a/modules/common/src/main/java/net/hollowcube/registry/ResourceFactory.java b/modules/common/src/main/java/net/hollowcube/registry/ResourceFactory.java index a1eb5709..1a5dd746 100644 --- a/modules/common/src/main/java/net/hollowcube/registry/ResourceFactory.java +++ b/modules/common/src/main/java/net/hollowcube/registry/ResourceFactory.java @@ -15,6 +15,12 @@ public ResourceFactory(NamespaceID namespace, Class type, Codec type, Codec codec) { + this.namespace = NamespaceID.from(namespace); + this.type = type; + this.codec = codec; + } + @Override public @NotNull NamespaceID namespace() { return this.namespace; diff --git a/modules/entity/build.gradle.kts b/modules/entity/build.gradle.kts new file mode 100644 index 00000000..df480752 --- /dev/null +++ b/modules/entity/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + `java-library` +} + +dependencies { + implementation(project(":modules:common")) + implementation(files("libs/enodia-pf-1.0.1.jar")) +} diff --git a/modules/entity/libs/enodia-pf-1.0.1.jar b/modules/entity/libs/enodia-pf-1.0.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..b1b2d5b114b6c786db7b5e1bf537a11114ab986f GIT binary patch literal 74586 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

  • --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 CODEC = RecordCodecBuilder.create(i -> i.group( + NumberProvider.CODEC.fieldOf("time").forGetter(IdleTask::time) + ).apply(i, IdleTask::new)); + + private final NumberProvider time; + + // Running state + private int sleepTime = 0; + + public IdleTask(@NotNull NumberProvider time) { + this.time = time; + } + + public NumberProvider time() { + return time; + } + + @Override + public void start(@NotNull Brain brain) { + super.start(brain); + + //todo get number source from entity or something, this is inconvenient to test + sleepTime = (int) time.nextLong(NumberSource.threadLocalRandom()); + } + + @Override + public void tick(@NotNull Brain brain) { + sleepTime -= 1; + if (sleepTime < 1) { + end(true); + } + } + + @Override + public @NotNull Task deepCopy() { + return new IdleTask(time); + } + + + @AutoService(Task.Factory.class) + public static final class Factory extends Task.Factory { + public Factory() { + super("unnamed:idle", IdleTask.class, IdleTask.CODEC); + } + } + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java new file mode 100644 index 00000000..a5ab3e04 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java @@ -0,0 +1,84 @@ +package unnamed.mmo.entity.brain.task; + +import com.google.auto.service.AutoService; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.brain.Brain; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import static unnamed.mmo.util.ExtraCodecs.lazy; + +public class SequenceTask extends AbstractTask { + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + lazy(() -> Task.CODEC).listOf().fieldOf("children").forGetter(SequenceTask::children) + ).apply(i, SequenceTask::new)); + + private final List children; + + // Running state + private Iterator taskIter = Collections.emptyIterator(); + private Task current = null; + + public SequenceTask(@NotNull List children) { + this.children = List.copyOf(children); + } + + public @NotNull List children() { + return children; + } + + @Override + public void start(@NotNull Brain brain) { + super.start(brain); + + taskIter = children().iterator(); + if (!taskIter.hasNext()) { + end(false); + return; + } + + current = taskIter.next(); + current.start(brain); + } + + @Override + public void tick(@NotNull Brain brain) { + current.tick(brain); + + // Do nothing if current task is still running + if (current.getState() == State.RUNNING) return; + + // If current task failed, fail the sequence + if (current.getState() == State.FAILED) { + end(false); + return; + } + + // If there are no more tasks, exit + if (!taskIter.hasNext()) { + end(true); + return; + } + + // Next task + current = taskIter.next(); + current.start(brain); + } + + @Override + public @NotNull Task deepCopy() { + return new SequenceTask(children().stream().map(Task::deepCopy).toList()); + } + + + @AutoService(Task.Factory.class) + public static final class Factory extends Task.Factory { + public Factory() { + super("unnamed:sequence", SequenceTask.class, SequenceTask.CODEC); + } + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java new file mode 100644 index 00000000..a19c2a96 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java @@ -0,0 +1,39 @@ +package unnamed.mmo.entity.brain.task; + +import com.mojang.serialization.Codec; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.brain.Brain; +import unnamed.mmo.registry.Registry; +import unnamed.mmo.registry.ResourceFactory; + +public sealed interface Task permits AbstractTask { + Codec CODEC = Factory.CODEC.dispatch(Factory::from, Factory::codec); + + @NotNull State getState(); + + void start(@NotNull Brain brain); + + void tick(@NotNull Brain brain); + + @NotNull Task deepCopy(); + + enum State { + INIT, RUNNING, COMPLETE, FAILED + } + + class Factory extends ResourceFactory { + static final Registry REGISTRY = Registry.service("task_factory", Factory.class); + static final Registry.Index, Factory> TYPE_REGISTRY = REGISTRY.index(Factory::type); + + @SuppressWarnings("Convert2MethodRef") + public static final Codec CODEC = Codec.STRING.xmap(ns -> REGISTRY.get(ns), Factory::name); + + public Factory(String namespace, Class type, Codec codec) { + super(namespace, type, codec); + } + + static @NotNull Factory from(@NotNull Task task) { + return TYPE_REGISTRY.get(task.getClass()); + } + } +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java new file mode 100644 index 00000000..c299ca23 --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java @@ -0,0 +1,73 @@ +package unnamed.mmo.entity.brain; + +import net.minestom.server.attribute.Attribute; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.test.Env; +import net.minestom.server.test.EnvTest; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import sexy.kostya.enodia.EnodiaPF; +import sexy.kostya.enodia.movement.MovementProcessor; +import sexy.kostya.enodia.movement.importance.MovementImportance; +import sexy.kostya.enodia.pathfinding.PathfindingCapabilities; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnvTest +public class TestEnodiaPF { + + @Test + public void test(Env env) { + var enodia = EnodiaPF.Companion.forImmutableWorlds(); + var hub = enodia.initializeMovementProcessingHub( + 2, + 5, + ent -> Attribute.MOVEMENT_SPEED.defaultValue(), + (a, b, c) -> true + ); + + var instance = env.createFlatInstance(); + var player = env.createPlayer(instance, new Pos(0, 42, 0)); + + var capabilities = PathfindingCapabilities.Companion.getDefault(); + var entity = new EnodiaEntity(); + entity.setInstance(instance, new Pos(5, 42, 5)) + .thenAccept(unused -> { + entity.movementProcessor = hub.createMovementProcessor(entity, capabilities); + + entity.movementProcessor.goTo(player, MovementImportance.Companion.getUNIMPORTANT(), 2); + }) + .join(); + + boolean result = env.tickWhile(() -> { + System.out.println(entity.getPosition()); + return !entity.getPosition().sameBlock(new Vec(0, 40, 0)); + }, Duration.ofMillis(100)); + + assertTrue(result); + + } + + static class EnodiaEntity extends Entity { + + public EnodiaEntity() { + super(EntityType.ZOMBIE); + } + + MovementProcessor movementProcessor = null; + + @Override + public void update(long time) { + super.update(time); + if (movementProcessor != null) { + movementProcessor.tick(time); + } + } + } +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java new file mode 100644 index 00000000..a8a42f1a --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java @@ -0,0 +1,26 @@ +package unnamed.mmo.entity.brain.task; + +import org.junit.jupiter.api.Test; +import unnamed.mmo.data.number.NumberProvider; +import unnamed.mmo.entity.brain.task.test.MockBrain; + +import static com.google.common.truth.Truth.assertThat; + +public class TestIdleTask { + + @Test + public void testHappyCase() { + var task = new IdleTask(NumberProvider.constant(5)); + // Entity should do nothing, so we can happily use any old entity. + var brain = new MockBrain(); + + assertThat(task.getState()).isEqualTo(Task.State.INIT); + task.start(brain); + for (int i = 0; i < 5; i++) { + assertThat(task.getState()).isEqualTo(Task.State.RUNNING); + task.tick(brain); + } + assertThat(task.getState()).isEqualTo(Task.State.COMPLETE); + } + +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java new file mode 100644 index 00000000..16115a88 --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java @@ -0,0 +1,55 @@ +package unnamed.mmo.entity.brain.task; + +import org.junit.jupiter.api.Test; +import unnamed.mmo.entity.brain.task.test.MockBrain; +import unnamed.mmo.entity.brain.task.test.MockTask; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static unnamed.mmo.entity.brain.task.test.TaskSubject.assertThat; + +public class TestSequenceTask { + @Test + public void testEmptySequence() { + Task task = new SequenceTask(List.of()); + var brain = new MockBrain(); + + task.start(brain); + + //todo not really sure this should fail on empty, but the problem with passing is that it may be instantly + // started again, pass again, start again, etc. Which would not be good. + assertEquals(Task.State.FAILED, task.getState()); + } + + @Test + public void testSingleTaskSuccess() { + var brain = new MockBrain(); + var mock1 = new MockTask(true); + var task = new SequenceTask(List.of(mock1)); + + task.start(brain); + task.tick(brain); + + // Should have passed and mock1 should also have been run (passed) + assertThat(mock1).isComplete(); + assertThat(task).isComplete(); + } + + @Test + public void testMultiTaskSuccess() { + var brain = new MockBrain(); + var mock1 = new MockTask(true); + var mock2 = new MockTask(true); + var task = new SequenceTask(List.of(mock1, mock2)); + task.start(brain); + + task.tick(brain); + assertThat(mock1).isComplete(); + + task.tick(brain); + assertThat(mock2).isComplete(); + + assertThat(task).isComplete(); + } +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java new file mode 100644 index 00000000..16473ef0 --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java @@ -0,0 +1,7 @@ +package unnamed.mmo.entity.brain.task.test; + +import unnamed.mmo.entity.brain.Brain; + +public class MockBrain implements Brain { + +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java new file mode 100644 index 00000000..991f4145 --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java @@ -0,0 +1,24 @@ +package unnamed.mmo.entity.brain.task.test; + +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.brain.Brain; +import unnamed.mmo.entity.brain.task.AbstractTask; +import unnamed.mmo.entity.brain.task.Task; + +public class MockTask extends AbstractTask { + private final boolean pass; + + public MockTask(boolean pass) { + this.pass = pass; + } + + @Override + public void tick(@NotNull Brain brain) { + end(pass); + } + + @Override + public @NotNull Task deepCopy() { + return new MockTask(pass); + } +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/TaskSubject.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/TaskSubject.java new file mode 100644 index 00000000..bb275ddc --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/TaskSubject.java @@ -0,0 +1,33 @@ +package unnamed.mmo.entity.brain.task.test; + +import com.google.common.truth.Fact; +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; +import com.google.common.truth.Truth; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import unnamed.mmo.entity.brain.task.Task; + +public class TaskSubject extends Subject { + private final Task actual; + + public static @NotNull TaskSubject assertThat(@Nullable Task actual) { + return Truth.assertAbout(tasks()).that(actual); + } + + protected TaskSubject(FailureMetadata metadata, @Nullable Task actual) { + super(metadata, actual); + this.actual = actual; + } + + public void isComplete() { + if (actual.getState() != Task.State.COMPLETE) { + failWithActual(Fact.simpleFact("Expected task to be complete, but it was " + actual.getState())); + } + } + + + private static final Factory tasks() { + return TaskSubject::new; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c5b0e5d5..7a0ef52e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,4 +9,5 @@ include(":modules:block-interactions") include(":modules:loot-table") include(":modules:player") include("modules:quest") +include(":modules:entity") include(":modules:development") From 175bd05f06fa1150beb808946551731b1f2efeba Mon Sep 17 00:00:00 2001 From: mworzala Date: Tue, 16 Aug 2022 22:13:33 +0300 Subject: [PATCH 02/25] more entity work --- .../java/unnamed/mmo/entity/brain/Brain.java | 4 ++ .../mmo/entity/brain/SingleTaskBrain.java | 7 ++ .../brain/navigator/EnodiaNavigator.java | 46 ++++++++++++ .../mmo/entity/brain/navigator/Navigator.java | 15 ++++ .../mmo/entity/brain/task/IdleTask.java | 37 +++++----- .../mmo/entity/brain/task/SequenceTask.java | 71 ++++++++++++------- .../unnamed/mmo/entity/brain/task/Task.java | 17 +++-- .../mmo/entity/brain/TestEnodiaPF.java | 7 +- .../mmo/entity/brain/task/TestIdleTask.java | 3 +- .../entity/brain/task/TestSequenceTask.java | 9 ++- .../mmo/entity/brain/task/test/MockBrain.java | 6 ++ .../mmo/entity/brain/task/test/MockTask.java | 9 ++- 12 files changed, 166 insertions(+), 65 deletions(-) create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java index 8267edf4..eba035bb 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java @@ -1,7 +1,11 @@ package unnamed.mmo.entity.brain; +import net.minestom.server.entity.Entity; +import org.jetbrains.annotations.NotNull; + public interface Brain { + @NotNull Entity entity(); } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java index a57e7cc7..dfb8429c 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java @@ -1,5 +1,12 @@ package unnamed.mmo.entity.brain; +import net.minestom.server.entity.Entity; +import org.jetbrains.annotations.NotNull; + class SingleTaskBrain implements Brain { + @Override + public @NotNull Entity entity() { + return null; + } } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java new file mode 100644 index 00000000..5c7d13f7 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java @@ -0,0 +1,46 @@ +package unnamed.mmo.entity.brain.navigator; + +import net.minestom.server.attribute.Attribute; +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.LivingEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import sexy.kostya.enodia.EnodiaPF; +import sexy.kostya.enodia.movement.MovementProcessingHub; +import sexy.kostya.enodia.movement.MovementProcessor; +import sexy.kostya.enodia.movement.importance.MovementImportance; +import sexy.kostya.enodia.pathfinding.PathfindingCapabilities; +import unnamed.mmo.entity.brain.Brain; + +final class EnodiaNavigator implements Navigator { + private static final EnodiaPF INSTANCE = EnodiaPF.Companion.forImmutableWorlds(); + private static final MovementProcessingHub MOVEMENT_HUB = INSTANCE.initializeMovementProcessingHub( + 2, 5, + ent -> ent instanceof LivingEntity ? + ((LivingEntity) ent).getAttributeValue(Attribute.MOVEMENT_SPEED) : + Attribute.MOVEMENT_SPEED.defaultValue(), + (unused1, unused2, unused3) -> true + ); + + private static final PathfindingCapabilities DEFAULT_CAPABILITIES = PathfindingCapabilities.Companion.getDefault(); + private static final MovementImportance DEFAULT_IMPORTANCE = MovementImportance.Companion.getUNIMPORTANT(); + + private final MovementProcessor enodia; + + EnodiaNavigator(@NotNull Brain brain) { + //todo other capabilities + this.enodia = MOVEMENT_HUB.createMovementProcessor(brain.entity(), DEFAULT_CAPABILITIES); + } + + @Override + public boolean setPathTo(@Nullable Point point) { + if (point == null) { + this.enodia.stop(true); + return true; + } + + //todo what is the last parameter here? + return enodia.goTo(point, DEFAULT_IMPORTANCE, 2); + } + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java new file mode 100644 index 00000000..145fb260 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java @@ -0,0 +1,15 @@ +package unnamed.mmo.entity.brain.navigator; + +import net.minestom.server.coordinate.Point; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.brain.Brain; + +public sealed interface Navigator permits EnodiaNavigator { + + static @NotNull Navigator enodia(@NotNull Brain brain) { + return new EnodiaNavigator(brain); + } + + boolean setPathTo(@NotNull Point point); + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/IdleTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/IdleTask.java index 201a7531..8ec41028 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/IdleTask.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/IdleTask.java @@ -9,21 +9,11 @@ import unnamed.mmo.entity.brain.Brain; public class IdleTask extends AbstractTask { - public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( - NumberProvider.CODEC.fieldOf("time").forGetter(IdleTask::time) - ).apply(i, IdleTask::new)); - - private final NumberProvider time; - - // Running state + private final Spec spec; private int sleepTime = 0; - public IdleTask(@NotNull NumberProvider time) { - this.time = time; - } - - public NumberProvider time() { - return time; + public IdleTask(@NotNull Spec spec) { + this.spec = spec; } @Override @@ -31,7 +21,7 @@ public void start(@NotNull Brain brain) { super.start(brain); //todo get number source from entity or something, this is inconvenient to test - sleepTime = (int) time.nextLong(NumberSource.threadLocalRandom()); + sleepTime = (int) spec.time().nextLong(NumberSource.threadLocalRandom()); } @Override @@ -42,16 +32,25 @@ public void tick(@NotNull Brain brain) { } } - @Override - public @NotNull Task deepCopy() { - return new IdleTask(time); - } + public record Spec( + @NotNull NumberProvider time + ) implements Task.Spec { + + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + NumberProvider.CODEC.fieldOf("time").forGetter(Spec::time) + ).apply(i, Spec::new)); + + @Override + public @NotNull Task create() { + return new IdleTask(this); + } + } @AutoService(Task.Factory.class) public static final class Factory extends Task.Factory { public Factory() { - super("unnamed:idle", IdleTask.class, IdleTask.CODEC); + super("unnamed:idle", Spec.class, Spec.CODEC); } } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java index a5ab3e04..7a262b91 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java @@ -6,47 +6,40 @@ import org.jetbrains.annotations.NotNull; import unnamed.mmo.entity.brain.Brain; -import java.util.Collections; -import java.util.Iterator; import java.util.List; import static unnamed.mmo.util.ExtraCodecs.lazy; public class SequenceTask extends AbstractTask { - public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( - lazy(() -> Task.CODEC).listOf().fieldOf("children").forGetter(SequenceTask::children) - ).apply(i, SequenceTask::new)); + private final Spec spec; private final List children; - - // Running state - private Iterator taskIter = Collections.emptyIterator(); - private Task current = null; - - public SequenceTask(@NotNull List children) { - this.children = List.copyOf(children); - } - - public @NotNull List children() { - return children; + private int current = 0; + + public SequenceTask(@NotNull Spec spec) { + this.spec = spec; + this.children = spec.children() + .stream() + .map(Task.Spec::create) + .toList(); } @Override public void start(@NotNull Brain brain) { super.start(brain); - taskIter = children().iterator(); - if (!taskIter.hasNext()) { + reset(); + if (!hasNext()) { end(false); return; } - current = taskIter.next(); - current.start(brain); + current().start(brain); } @Override public void tick(@NotNull Brain brain) { + final Task current = current(); current.tick(brain); // Do nothing if current task is still running @@ -59,26 +52,50 @@ public void tick(@NotNull Brain brain) { } // If there are no more tasks, exit - if (!taskIter.hasNext()) { + if (!hasNext()) { end(true); return; } // Next task - current = taskIter.next(); - current.start(brain); + next().start(brain); } - @Override - public @NotNull Task deepCopy() { - return new SequenceTask(children().stream().map(Task::deepCopy).toList()); + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean hasNext() { + return current < children.size() - 1; + } + + private Task current() { + return children.get(current); } + private Task next() { + return children.get(++current); + } + + private void reset() { + this.current = 0; + } + + + public record Spec( + List children + ) implements Task.Spec { + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + lazy(() -> Task.Spec.CODEC).listOf().fieldOf("children").forGetter(Spec::children) + ).apply(i, Spec::new)); + + @Override + public @NotNull Task create() { + return new SequenceTask(this); + } + } @AutoService(Task.Factory.class) public static final class Factory extends Task.Factory { public Factory() { - super("unnamed:sequence", SequenceTask.class, SequenceTask.CODEC); + super("unnamed:sequence", Spec.class, Spec.CODEC); } } } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java index a19c2a96..937980f8 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java @@ -7,7 +7,6 @@ import unnamed.mmo.registry.ResourceFactory; public sealed interface Task permits AbstractTask { - Codec CODEC = Factory.CODEC.dispatch(Factory::from, Factory::codec); @NotNull State getState(); @@ -15,25 +14,29 @@ public sealed interface Task permits AbstractTask { void tick(@NotNull Brain brain); - @NotNull Task deepCopy(); - enum State { INIT, RUNNING, COMPLETE, FAILED } - class Factory extends ResourceFactory { + interface Spec { + Codec CODEC = Factory.CODEC.dispatch(Factory::from, Factory::codec); + + @NotNull Task create(); + } + + class Factory extends ResourceFactory { static final Registry REGISTRY = Registry.service("task_factory", Factory.class); static final Registry.Index, Factory> TYPE_REGISTRY = REGISTRY.index(Factory::type); @SuppressWarnings("Convert2MethodRef") public static final Codec CODEC = Codec.STRING.xmap(ns -> REGISTRY.get(ns), Factory::name); - public Factory(String namespace, Class type, Codec codec) { + public Factory(String namespace, Class type, Codec codec) { super(namespace, type, codec); } - static @NotNull Factory from(@NotNull Task task) { - return TYPE_REGISTRY.get(task.getClass()); + static @NotNull Factory from(@NotNull Spec spec) { + return TYPE_REGISTRY.get(spec.getClass()); } } } diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java index c299ca23..b905a311 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java @@ -7,7 +7,6 @@ import net.minestom.server.entity.EntityType; import net.minestom.server.test.Env; import net.minestom.server.test.EnvTest; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import sexy.kostya.enodia.EnodiaPF; import sexy.kostya.enodia.movement.MovementProcessor; @@ -16,8 +15,7 @@ import java.time.Duration; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static com.google.common.truth.Truth.assertThat; @EnvTest public class TestEnodiaPF { @@ -50,8 +48,7 @@ public void test(Env env) { return !entity.getPosition().sameBlock(new Vec(0, 40, 0)); }, Duration.ofMillis(100)); - assertTrue(result); - + assertThat(result).isTrue(); } static class EnodiaEntity extends Entity { diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java index a8a42f1a..7011af8e 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java @@ -10,7 +10,8 @@ public class TestIdleTask { @Test public void testHappyCase() { - var task = new IdleTask(NumberProvider.constant(5)); + var spec = new IdleTask.Spec(NumberProvider.constant(5)); + var task = new IdleTask(spec); // Entity should do nothing, so we can happily use any old entity. var brain = new MockBrain(); diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java index 16115a88..01b1e0c5 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java @@ -12,7 +12,8 @@ public class TestSequenceTask { @Test public void testEmptySequence() { - Task task = new SequenceTask(List.of()); + var spec = new SequenceTask.Spec(List.of()); + var task = new SequenceTask(spec); var brain = new MockBrain(); task.start(brain); @@ -26,7 +27,8 @@ public void testEmptySequence() { public void testSingleTaskSuccess() { var brain = new MockBrain(); var mock1 = new MockTask(true); - var task = new SequenceTask(List.of(mock1)); + var spec = new SequenceTask.Spec(List.of(mock1.spec())); + var task = new SequenceTask(spec); task.start(brain); task.tick(brain); @@ -41,7 +43,8 @@ public void testMultiTaskSuccess() { var brain = new MockBrain(); var mock1 = new MockTask(true); var mock2 = new MockTask(true); - var task = new SequenceTask(List.of(mock1, mock2)); + var spec = new SequenceTask.Spec(List.of(mock1.spec(), mock2.spec())); + var task = new SequenceTask(spec); task.start(brain); task.tick(brain); diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java index 16473ef0..b32adf77 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java @@ -1,7 +1,13 @@ package unnamed.mmo.entity.brain.task.test; +import net.minestom.server.entity.Entity; +import org.jetbrains.annotations.NotNull; import unnamed.mmo.entity.brain.Brain; public class MockBrain implements Brain { + @Override + public @NotNull Entity entity() { + return null; + } } diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java index 991f4145..daed6f3b 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java @@ -17,8 +17,11 @@ public void tick(@NotNull Brain brain) { end(pass); } - @Override - public @NotNull Task deepCopy() { - return new MockTask(pass); + public @NotNull Spec spec() { + return () -> MockTask.this; } + + + + } From ef7eb430ab423ad69c805b293e4bd891e9192908 Mon Sep 17 00:00:00 2001 From: mworzala Date: Thu, 18 Aug 2022 23:08:43 +0300 Subject: [PATCH 03/25] more tests --- modules/development/build.gradle.kts | 1 + .../java/net/hollowcube/server/dev/Main.java | 21 ++++++ .../unnamed/mmo/entity/UnnamedEntity.java | 43 +++++++++++- .../java/unnamed/mmo/entity/brain/Brain.java | 11 +++ .../mmo/entity/brain/SingleTaskBrain.java | 54 ++++++++++++++- .../brain/navigator/EnodiaNavigator.java | 30 ++++++-- .../brain/navigator/HydrazineNavigator.java | 37 ++++++++++ .../mmo/entity/brain/navigator/Navigator.java | 18 ++++- .../entity/brain/task/WanderInRegionTask.java | 57 +++++++++++++++ .../mmo/entity/brain/TestEnodiaPF.java | 4 +- .../TestNavigatorBasicIntegration.java | 69 +++++++++++++++++++ .../mmo/entity/brain/navigator/TestUtil.java | 17 +++++ .../mmo/entity/brain/task/test/MockBrain.java | 17 +++++ .../mmo/entity/brain/task/test/MockTask.java | 7 +- quest_data.json5 | 20 ++++++ 15 files changed, 391 insertions(+), 15 deletions(-) create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/HydrazineNavigator.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java create mode 100644 quest_data.json5 diff --git a/modules/development/build.gradle.kts b/modules/development/build.gradle.kts index 61c8c024..b65ab221 100644 --- a/modules/development/build.gradle.kts +++ b/modules/development/build.gradle.kts @@ -9,6 +9,7 @@ dependencies { implementation(project(":modules:item")) implementation(project(":modules:player")) implementation(project(":modules:quest")) + implementation(project(":modules:entity")) implementation("org.mongodb:mongodb-driver-sync:4.7.1") } diff --git a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java index 0eef7c93..92ef449c 100644 --- a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java +++ b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java @@ -31,6 +31,21 @@ import net.hollowcube.server.Facet; import net.hollowcube.server.ServerWrapper; import net.hollowcube.server.dev.command.BaseCommandRegister; +import unnamed.mmo.blocks.BlockInteracter; +import unnamed.mmo.blocks.ore.Ore; +import unnamed.mmo.chat.ChatManager; +import unnamed.mmo.chat.storage.ChatStorage; +import unnamed.mmo.command.BaseCommandRegister; +import unnamed.mmo.entity.UnnamedEntity; +import unnamed.mmo.item.Item; +import unnamed.mmo.damage.DamageProcessor; +import unnamed.mmo.item.Item; +import unnamed.mmo.item.ItemManager; +import unnamed.mmo.item.entity.OwnedItemEntity; +import unnamed.mmo.player.PlayerImpl; +import unnamed.mmo.quest.QuestFacet; +import unnamed.mmo.server.dev.tool.DebugToolManager; +import unnamed.mmo.server.instance.TickTrackingInstance; import java.util.HashMap; import java.util.Map; @@ -78,6 +93,12 @@ public static void main(String[] args) { //todo a command for this player.getInventory().addItemStack(DebugToolManager.createTool("unnamed:hello")); + + + //todo test entity + UnnamedEntity entity = new UnnamedEntity(); + entity.setInstance(instance, new Pos(0, 40, 0)) + .thenAccept(unused -> System.out.println("Spawned")); }); BaseCommandRegister.registerCommands(); //todo this should be in a facet? diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java b/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java index eb318d59..a06e8cc9 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java @@ -1,4 +1,43 @@ package unnamed.mmo.entity; -public class UnnamedEntity { -} +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.instance.Instance; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.brain.Brain; +import unnamed.mmo.entity.brain.SingleTaskBrain; +import unnamed.mmo.entity.brain.task.Task; +import unnamed.mmo.entity.brain.task.WanderInRegionTask; + +import java.util.concurrent.CompletableFuture; + +public class UnnamedEntity extends Entity { + private final Brain brain; + + public UnnamedEntity(Task task) { + super(EntityType.ZOMBIE); + brain = new SingleTaskBrain(this, task); + } + + @Override + public CompletableFuture setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) { + return super.setInstance(instance, spawnPosition).thenAccept(unused -> { + brain.setInstance(instance); + }); + } + + @Override + public void update(long time) { + super.update(time); + + if (instance != null) { + brain.tick(time); + } + + } + + public @NotNull Brain brain() { + return brain; + } +} \ No newline at end of file diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java index eba035bb..870fab01 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java @@ -1,11 +1,22 @@ package unnamed.mmo.entity.brain; +import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; +import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.brain.navigator.Navigator; public interface Brain { @NotNull Entity entity(); + @NotNull Navigator navigator(); + + boolean setPathTo(@NotNull Point point); + + default void setInstance(Instance instance) {} + + void tick(long time); + } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java index dfb8429c..29fdca29 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java @@ -1,12 +1,62 @@ package unnamed.mmo.entity.brain; +import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; +import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.brain.navigator.Navigator; +import unnamed.mmo.entity.brain.task.Task; + +public class SingleTaskBrain implements Brain { + + private final Entity entity; + private final Navigator navigator; + private final Task task; + + public SingleTaskBrain(@NotNull Entity entity, @NotNull Task task) { + this.entity = entity; + this.navigator = Navigator.enodia(entity); + this.task = task; + } -class SingleTaskBrain implements Brain { @Override public @NotNull Entity entity() { - return null; + return entity; + } + + @Override + public @NotNull Navigator navigator() { + return navigator; + } + + @Override + public boolean setPathTo(@NotNull Point point) { + return navigator.setPathTo(point); + } + + private boolean failed = false; + private Instance lastInstance = null; + + @Override + public void setInstance(Instance instance) { + navigator.setInstance(instance); + lastInstance = instance; + } + + @Override + public void tick(long time) { + if (lastInstance == null) return; + + navigator.tick(time); + switch (task.getState()) { + case INIT, COMPLETE -> task.start(this); + case RUNNING -> task.tick(this); + case FAILED -> { + if (!failed) + System.out.println("Failed"); + failed = true; + } + } } } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java index 5c7d13f7..07311bd7 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java @@ -2,7 +2,9 @@ import net.minestom.server.attribute.Attribute; import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Entity; import net.minestom.server.entity.LivingEntity; +import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import sexy.kostya.enodia.EnodiaPF; @@ -25,11 +27,17 @@ final class EnodiaNavigator implements Navigator { private static final PathfindingCapabilities DEFAULT_CAPABILITIES = PathfindingCapabilities.Companion.getDefault(); private static final MovementImportance DEFAULT_IMPORTANCE = MovementImportance.Companion.getUNIMPORTANT(); - private final MovementProcessor enodia; + private final Entity entity; + private MovementProcessor enodia; - EnodiaNavigator(@NotNull Brain brain) { - //todo other capabilities - this.enodia = MOVEMENT_HUB.createMovementProcessor(brain.entity(), DEFAULT_CAPABILITIES); + EnodiaNavigator(@NotNull Entity entity) { + this.entity = entity; + this.enodia = null; + } + + @Override + public void setInstance(@NotNull Instance instance) { + enodia = MOVEMENT_HUB.createMovementProcessor(entity, DEFAULT_CAPABILITIES); } @Override @@ -43,4 +51,18 @@ public boolean setPathTo(@Nullable Point point) { return enodia.goTo(point, DEFAULT_IMPORTANCE, 2); } + @Override + public boolean isActive() { + if (enodia == null) { + return false; + } + return enodia.isActive(); + } + + @Override + public void tick(long time) { + if (enodia != null) { + enodia.tick(time); + } + } } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/HydrazineNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/HydrazineNavigator.java new file mode 100644 index 00000000..ed8aa3c7 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/HydrazineNavigator.java @@ -0,0 +1,37 @@ +package unnamed.mmo.entity.brain.navigator; + +import com.extollit.gaming.ai.path.HydrazinePathFinder; +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.pathfinding.NavigableEntity; +import net.minestom.server.instance.Instance; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.brain.Brain; + +final class HydrazineNavigator implements Navigator { + private final net.minestom.server.entity.pathfinding.Navigator hydrazine; + + HydrazineNavigator(@NotNull Entity entity) { + this.hydrazine = new net.minestom.server.entity.pathfinding.Navigator(entity); + } + + @Override + public void setInstance(@NotNull Instance instance) { + hydrazine.setPathFinder(new HydrazinePathFinder(hydrazine.getPathingEntity(), instance.getInstanceSpace())); + } + + @Override + public boolean setPathTo(@NotNull Point point) { + return hydrazine.setPathTo(point); + } + + @Override + public boolean isActive() { + return hydrazine.getPathPosition() != null; + } + + @Override + public void tick(long time) { + hydrazine.tick(); + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java index 145fb260..4a9005a0 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java @@ -1,15 +1,27 @@ package unnamed.mmo.entity.brain.navigator; import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Entity; +import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import unnamed.mmo.entity.brain.Brain; -public sealed interface Navigator permits EnodiaNavigator { +public sealed interface Navigator permits EnodiaNavigator, HydrazineNavigator { - static @NotNull Navigator enodia(@NotNull Brain brain) { - return new EnodiaNavigator(brain); + static @NotNull Navigator enodia(@NotNull Entity entity) { + return new EnodiaNavigator(entity); } + static @NotNull Navigator hydrazine(@NotNull Entity entity) { + return new HydrazineNavigator(entity); + } + + default void setInstance(@NotNull Instance instance) {} + boolean setPathTo(@NotNull Point point); + boolean isActive(); + + void tick(long time); + } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java new file mode 100644 index 00000000..f3c78cc2 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java @@ -0,0 +1,57 @@ +package unnamed.mmo.entity.brain.task; + +import com.google.auto.service.AutoService; +import com.mojang.serialization.Codec; +import net.minestom.server.coordinate.Vec; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.brain.Brain; + +import java.util.concurrent.ThreadLocalRandom; + +public class WanderInRegionTask extends AbstractTask { + private final Spec spec; + + public WanderInRegionTask(@NotNull Spec spec) { + this.spec = spec; + } + + @Override + public void start(@NotNull Brain brain) { + super.start(brain); + + var target = new Vec( + ThreadLocalRandom.current().nextInt(-10, 10), + 40, + ThreadLocalRandom.current().nextInt(-10, 10) + ); + System.out.println("PF to " + target); + + boolean result = brain.setPathTo(target); + System.out.println("Result: " + result); + + if (!result) end(false); + } + + @Override + public void tick(@NotNull Brain brain) { + if (brain.navigator().isActive()) return; + end(true); + } + + + public record Spec() implements Task.Spec { + public static final Codec CODEC = Codec.unit(new Spec()); + + @Override + public @NotNull Task create() { + return new WanderInRegionTask(this); + } + } + + @AutoService(WanderInRegionTask.class) + public static class Factory extends Task.Factory { + public Factory() { + super("unnamed:wander_in_region", Spec.class, Spec.CODEC); + } + } +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java index b905a311..5f1d4fbe 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java @@ -45,10 +45,12 @@ public void test(Env env) { boolean result = env.tickWhile(() -> { System.out.println(entity.getPosition()); - return !entity.getPosition().sameBlock(new Vec(0, 40, 0)); +// return !entity.getPosition().sameBlock(new Vec(0, 40, 0)); + return entity.movementProcessor.isActive(); }, Duration.ofMillis(100)); assertThat(result).isTrue(); + assertThat(entity.movementProcessor.isActive()).isFalse(); } static class EnodiaEntity extends Entity { diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java new file mode 100644 index 00000000..d10ca01c --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java @@ -0,0 +1,69 @@ +package unnamed.mmo.entity.brain.navigator; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.instance.block.Block; +import net.minestom.server.test.Env; +import net.minestom.server.test.EnvTest; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.time.Duration; +import java.util.function.Function; + +import static com.google.common.truth.Truth.assertThat; + +@EnvTest +public class TestNavigatorBasicIntegration { + + @ParameterizedTest(name = "{0}") + @MethodSource("unnamed.mmo.entity.brain.navigator.TestUtil#navigators") + public void testBasicMovement(String name, Function newNavigator, Env env) { + var entity = new Entity(EntityType.ZOMBIE); + var navigator = newNavigator.apply(entity); + + var instance = env.createFlatInstance(); + instance.loadChunk(0, 0).join(); + + entity.setInstance(instance, new Pos(5, 42, 5)).join(); + navigator.setInstance(instance); + + navigator.setPathTo(new Pos(0, 42, 0)); + var result = env.tickWhile(() -> { + System.out.println(entity.getPosition()); + navigator.tick(System.currentTimeMillis()); + return navigator.isActive(); + }, Duration.ofMillis(100)); + + assertThat(result).isTrue(); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("unnamed.mmo.entity.brain.navigator.TestUtil#navigators") + public void testBasicMovementAroundBlock(String name, Function newNavigator, Env env) { + var entity = new Entity(EntityType.ZOMBIE); + var navigator = newNavigator.apply(entity); + + var instance = env.createFlatInstance(); + instance.loadChunk(0, 0).join(); + + // Block the direct path + instance.setBlock(3, 41, 0, Block.STONE); + instance.setBlock(3, 42, 0, Block.STONE); + + entity.setInstance(instance, new Pos(5, 42, 0)).join(); + navigator.setInstance(instance); + + navigator.setPathTo(new Pos(0, 42, 0)); + var result = env.tickWhile(() -> { + System.out.println(entity.getPosition()); + navigator.tick(System.currentTimeMillis()); + return navigator.isActive(); + }, Duration.ofMillis(100)); + + assertThat(result).isTrue(); + assertThat(entity.getPosition().sameBlock(new Vec(0, 40, 0))).isTrue(); + } +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java new file mode 100644 index 00000000..f056bcd4 --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java @@ -0,0 +1,17 @@ +package unnamed.mmo.entity.brain.navigator; + +import net.minestom.server.entity.Entity; +import org.junit.jupiter.params.provider.Arguments; + +import java.util.function.Function; +import java.util.stream.Stream; + +public class TestUtil { + + public static Stream navigators() { + return Stream.of( + Arguments.of("enodia", (Function) EnodiaNavigator::new) +// Arguments.of("hydrazine", (Function) HydrazineNavigator::new) + ); + } +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java index b32adf77..212864f8 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java @@ -1,8 +1,10 @@ package unnamed.mmo.entity.brain.task.test; +import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; import unnamed.mmo.entity.brain.Brain; +import unnamed.mmo.entity.brain.navigator.Navigator; public class MockBrain implements Brain { @@ -10,4 +12,19 @@ public class MockBrain implements Brain { public @NotNull Entity entity() { return null; } + + @Override + public @NotNull Navigator navigator() { + return null; + } + + @Override + public boolean setPathTo(@NotNull Point point) { + return false; + } + + @Override + public void tick(long time) { + + } } diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java index daed6f3b..8bb6e7fa 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java @@ -6,15 +6,16 @@ import unnamed.mmo.entity.brain.task.Task; public class MockTask extends AbstractTask { - private final boolean pass; + private final Boolean pass; - public MockTask(boolean pass) { + public MockTask(Boolean pass) { this.pass = pass; } @Override public void tick(@NotNull Brain brain) { - end(pass); + if (pass != null) + end(pass); } public @NotNull Spec spec() { diff --git a/quest_data.json5 b/quest_data.json5 new file mode 100644 index 00000000..758de996 --- /dev/null +++ b/quest_data.json5 @@ -0,0 +1,20 @@ +[ + // Single objective, kill mob + { + "type": "kill_mob", + "data": 5, + }, + + // Sequence objective + { + "data": {}, + "children": { + "0": { + "data": 5, + }, + "1": { + "data": 2, + }, + } + } +] \ No newline at end of file From 1ad4d145cbaa0081bb5735ebf8944f587a5ccfd8 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 21 Aug 2022 12:17:15 +0300 Subject: [PATCH 04/25] add my dumb pathfinding impl, it needs a lot more testing --- .../brain/navigator/CustomNavigator.java | 30 +++ .../brain/navigator/EnodiaNavigator.java | 4 +- .../mmo/entity/brain/navigator/Navigator.java | 6 +- .../mmo/entity/pathfinding/PFNavigator.java | 192 ++++++++++++++++++ .../pathfinding/PFNeighborSupplier.java | 76 +++++++ .../mmo/entity/pathfinding/PFNode.java | 46 +++++ .../mmo/entity/pathfinding/PFPath.java | 37 ++++ .../entity/pathfinding/PFPathGenerator.java | 118 +++++++++++ .../TestNavigatorBasicIntegration.java | 19 +- .../mmo/entity/brain/navigator/TestUtil.java | 5 +- quest_data.json5 | 20 -- 11 files changed, 521 insertions(+), 32 deletions(-) create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/CustomNavigator.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNeighborSupplier.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNode.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPath.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java delete mode 100644 quest_data.json5 diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/CustomNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/CustomNavigator.java new file mode 100644 index 00000000..9a1b4e08 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/CustomNavigator.java @@ -0,0 +1,30 @@ +package unnamed.mmo.entity.brain.navigator; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Entity; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.pathfinding.PFNavigator; + +final class CustomNavigator implements Navigator { + private final PFNavigator navigator; + + public CustomNavigator(@NotNull Entity entity) { + this.navigator = new PFNavigator(entity); + } + + + @Override + public boolean setPathTo(@NotNull Point point) { + return navigator.setPathTo(point, 0.5f); + } + + @Override + public boolean isActive() { + return !navigator.isComplete(); + } + + @Override + public void tick(long time) { + navigator.tick(time); + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java index 07311bd7..bda0caa4 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java @@ -47,8 +47,8 @@ public boolean setPathTo(@Nullable Point point) { return true; } - //todo what is the last parameter here? - return enodia.goTo(point, DEFAULT_IMPORTANCE, 2); + // Last parameter here is the max distance from the target. + return enodia.goTo(point, DEFAULT_IMPORTANCE, 0.5f); } @Override diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java index 4a9005a0..6fa449a4 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java @@ -6,7 +6,7 @@ import org.jetbrains.annotations.NotNull; import unnamed.mmo.entity.brain.Brain; -public sealed interface Navigator permits EnodiaNavigator, HydrazineNavigator { +public sealed interface Navigator permits CustomNavigator, EnodiaNavigator, HydrazineNavigator { static @NotNull Navigator enodia(@NotNull Entity entity) { return new EnodiaNavigator(entity); @@ -16,6 +16,10 @@ public sealed interface Navigator permits EnodiaNavigator, HydrazineNavigator { return new HydrazineNavigator(entity); } + static @NotNull Navigator custom(@NotNull Entity entity) { + return new CustomNavigator(entity); + } + default void setInstance(@NotNull Instance instance) {} boolean setPathTo(@NotNull Point point); diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java new file mode 100644 index 00000000..8fa35108 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java @@ -0,0 +1,192 @@ +package unnamed.mmo.entity.pathfinding; + +import net.minestom.server.attribute.Attribute; +import net.minestom.server.collision.CollisionUtils; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.LivingEntity; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.WorldBorder; +import net.minestom.server.utils.chunk.ChunkUtils; +import net.minestom.server.utils.position.PositionUtils; +import net.minestom.server.utils.time.Cooldown; +import net.minestom.server.utils.time.TimeUnit; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Duration; + +// TODO all pathfinding requests could be processed in another thread + +public final class PFNavigator { + private Point goalPosition; + private final Entity entity; + private PFPath path; + private final Cooldown jumpCooldown = new Cooldown(Duration.of(40, TimeUnit.SERVER_TICK)); + private double minimumDistance; + + public PFNavigator(@NotNull Entity entity) { + this.entity = entity; + } + + /** + * Used to move the entity toward {@code direction} in the X and Z axis + * Gravity is still applied but the entity will not attempt to jump + * Also update the yaw/pitch of the entity to look along 'direction' + * + * @param direction the targeted position + * @param speed define how far the entity will move + */ + public boolean moveTowards(@NotNull Point direction, double speed) { + final Pos position = entity.getPosition(); + final double dx = direction.x() - position.x(); + final double dy = direction.y() - position.y(); + final double dz = direction.z() - position.z(); + // the purpose of these few lines is to slow down entities when they reach their destination + final double distSquared = dx * dx + dy * dy + dz * dz; + if (speed > distSquared) { + speed = distSquared; + } + final double radians = Math.atan2(dz, dx); + final double speedX = Math.cos(radians) * speed; + final double speedY = dy * speed; + final double speedZ = Math.sin(radians) * speed; + final float yaw = PositionUtils.getLookYaw(dx, dz); + final float pitch = PositionUtils.getLookPitch(dx, dy, dz); + // Prevent ghosting + final var physicsResult = CollisionUtils.handlePhysics(entity, new Vec(speedX, speedY, speedZ)); + this.entity.refreshPosition(Pos.fromPoint(physicsResult.newPosition()).withView(yaw, pitch)); + return physicsResult.collisionX() || physicsResult.collisionY() | physicsResult.collisionZ(); + } + + public void jump(float height) { + // FIXME magic value + this.entity.setVelocity(new Vec(0, height * 2.5f, 0)); + } + + /** + * Retrieves the path to {@code position} and ask the entity to follow the path. + *

    + * Can be set to null to reset the pathfinder. + *

    + * The position is cloned, if you want the entity to continually follow this position object + * you need to call this when you want the path to update. + * + * @param point the position to find the path to, null to reset the pathfinder + * @param minimumDistance + * @return true if a path has been found + */ + public synchronized boolean setPathTo(@Nullable Point point, double minimumDistance) { + if (point != null && goalPosition != null && point.samePoint(goalPosition) && this.path != null) { + // Tried to set path to the same target position + return false; + } + final Instance instance = entity.getInstance(); + if (point == null) { + this.path = null; + return false; + } + // Can't path with a null instance. + if (instance == null) { + this.path = null; + return false; + } + // Can't path outside the world border + final WorldBorder worldBorder = instance.getWorldBorder(); + if (!worldBorder.isInside(point)) { + return false; + } + // Can't path in an unloaded chunk + final Chunk chunk = instance.getChunkAt(point); + if (!ChunkUtils.isLoaded(chunk)) { + return false; + } + + this.minimumDistance = minimumDistance; + if (this.entity.getPosition().distance(point) < minimumDistance) return false; + if (goalPosition != null && point.samePoint(goalPosition)) return false; + + this.path = PFPathGenerator.generate(instance, + this.entity.getPosition(), + point, + 100, + this.entity.getBoundingBox().depth() * 2, + this.entity.getBoundingBox()); + + final boolean success = path != null; + this.goalPosition = success ? point : null; + return success; + } + + @ApiStatus.Internal + public synchronized void tick(long tick) { + if (goalPosition == null) return; // No path + if (entity instanceof LivingEntity && ((LivingEntity) entity).isDead()) return; // No pathfinding tick for dead entities + if (path == null) return; + + Point currentTarget = path.getCurrent(); + if (currentTarget == null) currentTarget = goalPosition; + float movementSpeed = 0.1f; + + if (this.entity.getDistance(goalPosition) < minimumDistance) return; + + if (entity instanceof LivingEntity living) { + movementSpeed = living.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue(); + } + + boolean isStuck = moveTowards(currentTarget, movementSpeed); + + if (isStuck) { + double heightChange = currentTarget.y() - entity.getPosition().y(); + if (heightChange > 0 && entity.getVelocity().y() <= 0) { + if (heightChange < 0.2) { + entity.setVelocity(new Vec(0, 2, 0)); + moveTowards(currentTarget, movementSpeed); + } else if (heightChange < 1) { + entity.setVelocity(new Vec(0, 4, 0)); + moveTowards(currentTarget, movementSpeed); + } else if (jumpCooldown.isReady(tick)) { + jumpCooldown.refreshLastUpdate(tick); + jump(3.5f); + moveTowards(currentTarget, movementSpeed); + } + } + } + + if (entity.getPosition().distanceSquared(currentTarget) < 0.4) { + path.next(); + } + } + + + /** + * Gets the target pathfinder position. + * + * @return the target pathfinder position, null if there is no one + */ + public @Nullable Point getGoalPosition() { + return goalPosition; + } + + public @NotNull Entity getEntity() { + return entity; + } + + public PFPath getPath() { + return path; + } + + private void reset() { + this.goalPosition = null; + this.path = null; + } + + public boolean isComplete() { + if (this.path == null) return true; + return goalPosition == null || entity.getPosition().distance(goalPosition) < 1; + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNeighborSupplier.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNeighborSupplier.java new file mode 100644 index 00000000..00340323 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNeighborSupplier.java @@ -0,0 +1,76 @@ +package unnamed.mmo.entity.pathfinding; + +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; + +public interface PFNeighborSupplier { + + //todo right now supplier cannot influence cost, that is probably not desirable behavior + @NotNull + Collection<@NotNull Point> getNeighbors(Block.Getter blockGetter, Point current, Point goal, @NotNull BoundingBox expandedBoundingBox); + + class Iam implements PFNeighborSupplier { + + @Override + public @NotNull Collection<@NotNull Point> getNeighbors(Block.Getter blockGetter, Point current, Point goal, @NotNull BoundingBox expandedBoundingBox) { + Collection nearby = new ArrayList<>(); + + int width = (int) Math.ceil(expandedBoundingBox.width() / 2); + + for (int x = -width; x <= width; x++) { + for (int z = -width; z <= width; z++) { + if (x == 0 && z == 0) continue; + + Point orgPoint = new Vec(current.blockX(), current.blockY(), current.blockZ()).add(x, 0, z).add(0.5, 0, 0.5); + Point point = PFNode.gravitySnap(blockGetter, orgPoint); +// Point point = orgPoint; + + if (point == null) continue; + + //todo may be worth adding something like this back later (more expensive to jump) +// float cost = orgPoint.blockY() == point.blockY() ? 0.9f : 4.5f; +// if (x != 0 && z != 0) cost -= 0.2; + + if (blockGetter.getBlock(point, Block.Getter.Condition.TYPE).isSolid()) continue; + if (!blockGetter.getBlock(point.sub(0, 1, 0), Block.Getter.Condition.TYPE).isSolid()) continue; + + //todo this stops my villager from finding a path between two blocks +// Collection overlapping = BoundingBoxUtilKt.getBlocks(expandedBoundingBox, point); +// +// boolean isInvalid = false; +// for (Point block : overlapping) { +// if (blockGetter.getBlock(block, Block.Getter.Condition.TYPE).isSolid()) { +// isInvalid = true; +// break; +// } +// } +// +// if (isInvalid) { +// // Check up 1 block +// boolean isInvalidUp = false; +// for (Point block : overlapping) { +// if (blockGetter.getBlock(block.add(0, 1, 0), Block.Getter.Condition.TYPE).isSolid()) { +// isInvalidUp = true; +// break; +// } +// } +// +// if (isInvalidUp) continue; +// point = point.add(0, 1, 0); +// } + + //todo a slight optimization would be to not add any visited points to the list + nearby.add(point); + } + } + + return nearby; + } + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNode.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNode.java new file mode 100644 index 00000000..d9cd2241 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNode.java @@ -0,0 +1,46 @@ +package unnamed.mmo.entity.pathfinding; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public record PFNode( + @NotNull Point point, + float cost +) { + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PFNode pfNode = (PFNode) o; + return point.equals(pfNode.point); + } + + @Override + public int hashCode() { + return Objects.hash(point); + } + + + + private static Point findGround(Block.Getter blockGetter, Point point) { + Point ground = point.sub(0, 1, 0); + while (!blockGetter.getBlock(ground, Block.Getter.Condition.TYPE).isSolid()) { + ground = ground.sub(0, 1, 0); + if (Math.abs(ground.blockY() - point.blockY()) > 30) return null; + } + Point newPoint = ground.add(0, 1, 0); + return newPoint.withY(newPoint.blockY()); + } + + public static Point gravitySnap(Block.Getter blockGetter, Point orgPoint) { + var above = blockGetter.getBlock(orgPoint, Block.Getter.Condition.TYPE).isSolid(); + + if (above) return orgPoint.add(0, 1, 0); + return findGround(blockGetter, orgPoint); + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPath.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPath.java new file mode 100644 index 00000000..41b2d439 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPath.java @@ -0,0 +1,37 @@ +package unnamed.mmo.entity.pathfinding; + +import net.minestom.server.coordinate.Point; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class PFPath { + final List nodes; + public int index = 0; + + public PFPath(List nodes) { + this.nodes = nodes; + } + + public List getNodes() { + return nodes; + } + + @Nullable + public Point getCurrent() { + if (index >= nodes.size()) return null; + var current = nodes.get(index); + return current; + } + + public void next() { + if (index >= nodes.size()) return; + index++; + } + + @Override + public String toString() { + return nodes.toString(); + } + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java new file mode 100644 index 00000000..d4b00cb4 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java @@ -0,0 +1,118 @@ +package unnamed.mmo.entity.pathfinding; + +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.Instance; + +import java.util.*; + +public class PFPathGenerator { + public static double heuristic (Point node, Point target) { + return node.distance(target); + } + + private static Collection getNeighborPositions(Point point) { + return List.of( + point.add(1, 0, 0), + point.add(-1, 0, 0), + point.add(0, 0, 1), + point.add(0, 0, -1) + ); + } + + static Comparator pNodeComparator = (a, b) -> Float.compare(a.cost(), b.cost()); + public static PFPath generate(Instance instance, Point orgStart, Point orgTarget, double maxDistance, double closeDistance, BoundingBox boundingBox) { + Point start = PFNode.gravitySnap(instance, orgStart); + Point target = PFNode.gravitySnap(instance, orgTarget); + if (start == null || target == null) return null; + + BoundingBox expandedBoundingBox = boundingBox.expand(0.5, 0, 0.5); + PFNeighborSupplier neighborSupplier = new PFNeighborSupplier.Iam(); + + int maxSize = (int) Math.floor(maxDistance * 7); + + PFNode startNode = new PFNode(start, 0); + + Queue open = new PriorityQueue<>(pNodeComparator); + open.add(startNode); + + Map cameFrom = new HashMap<>(); + + Map minCost = new HashMap<>(); + minCost.put(start, 0f); + + while (!open.isEmpty()) { + PFNode current = open.peek(); + if (current.point().distance(target) < closeDistance) + break; // Path is found + + if (cameFrom.size() >= maxSize) // Max search depth reached + throw new RuntimeException("Ran out of nodes"); + + open.poll(); + for (Point neighbor : neighborSupplier.getNeighbors(instance, current.point(), target, expandedBoundingBox)) { + float neighborCost = (float) (minCost.get(current.point()) + current.point().distance(neighbor)); + if (neighborCost < minCost.getOrDefault(neighbor, Float.POSITIVE_INFINITY)) { + // New cheapest path, update indices + float neighborTotalCost = (float) (neighborCost + heuristic(neighbor, target)); + PFNode neighborNode = new PFNode(neighbor, neighborTotalCost); + cameFrom.put(neighborNode, current); + minCost.put(neighbor, neighborCost); + + if (open.contains(neighborNode)) { + open.remove(neighborNode); + } + open.offer(neighborNode); + } + + } +// var nearbyPoints = current.getNearby(instance, closed, target, expandedBoundingBox) +// .stream().filter(p -> p.point.distance(target) <= maxDistance).collect(Collectors.toSet()); + + } + + List result = new ArrayList<>(); + PFNode current = open.peek(); + if (current == null) return null; + result.add(current.point()); + while (cameFrom.containsKey(current)) { + current = cameFrom.get(current); + result.add(0, current.point()); + } + +// Collections.reverse(result); +// if (open.isEmpty()) return null; +// +// PNode current = open.poll(); +// +// if (current.point.distance(target) > closeDistance) return null; +// +// while (current.parent != null) { +// path.getNodes().add(current); +// current = current.parent; +// } +// +// Collections.reverse(path.getNodes()); +// path.reduceNodes(instance, expandedBoundingBox); +// path.addNodes(instance, expandedBoundingBox); + +// if (path.getNodes().size() > 0) { +// PNode pEnd = new PNode(target, 0, 0, path.getNodes().get(path.getNodes().size() - 1)); +// path.getNodes().add(pEnd); +// } +// +// return path; + return new PFPath(result); + } + + public static void main(String[] args) { + Queue queue = new PriorityQueue<>(pNodeComparator); + PFNode a = new PFNode(Vec.ZERO, 2); + queue.offer(a); + queue.offer(a); + queue.remove(a); + + PFPathGenerator.generate(null, new Vec(0, 0, 0), new Vec(5, 0, 0), 10, 0.8, null); + } +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java index d10ca01c..223e57e4 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java @@ -27,10 +27,10 @@ public void testBasicMovement(String name, Function newNaviga var instance = env.createFlatInstance(); instance.loadChunk(0, 0).join(); - entity.setInstance(instance, new Pos(5, 42, 5)).join(); + entity.setInstance(instance, new Pos(5, 40, 5)).join(); navigator.setInstance(instance); - navigator.setPathTo(new Pos(0, 42, 0)); + navigator.setPathTo(new Pos(0, 40, 0)); var result = env.tickWhile(() -> { System.out.println(entity.getPosition()); navigator.tick(System.currentTimeMillis()); @@ -38,6 +38,7 @@ public void testBasicMovement(String name, Function newNaviga }, Duration.ofMillis(100)); assertThat(result).isTrue(); + assertThat(entity.getPosition().distance(new Vec(0, 40, 0))).isAtMost(1); } @ParameterizedTest(name = "{0}") @@ -47,23 +48,27 @@ public void testBasicMovementAroundBlock(String name, Function { System.out.println(entity.getPosition()); navigator.tick(System.currentTimeMillis()); return navigator.isActive(); - }, Duration.ofMillis(100)); + }, Duration.ofMillis(200)); assertThat(result).isTrue(); - assertThat(entity.getPosition().sameBlock(new Vec(0, 40, 0))).isTrue(); + assertThat(entity.getPosition().distance(new Vec(0, 40, 0))).isAtMost(1); } } diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java index f056bcd4..b9f02a7c 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java @@ -10,8 +10,9 @@ public class TestUtil { public static Stream navigators() { return Stream.of( - Arguments.of("enodia", (Function) EnodiaNavigator::new) -// Arguments.of("hydrazine", (Function) HydrazineNavigator::new) +// Arguments.of("enodia", (Function) EnodiaNavigator::new), +// Arguments.of("hydrazine", (Function) HydrazineNavigator::new), + Arguments.of("custom", (Function) CustomNavigator::new) ); } } diff --git a/quest_data.json5 b/quest_data.json5 deleted file mode 100644 index 758de996..00000000 --- a/quest_data.json5 +++ /dev/null @@ -1,20 +0,0 @@ -[ - // Single objective, kill mob - { - "type": "kill_mob", - "data": 5, - }, - - // Sequence objective - { - "data": {}, - "children": { - "0": { - "data": 5, - }, - "1": { - "data": 2, - }, - } - } -] \ No newline at end of file From 888956b42e7737c1c355c3f36f21d0323b39537f Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 21 Aug 2022 12:45:44 +0300 Subject: [PATCH 05/25] add debug renderer --- modules/common/build.gradle.kts | 2 + .../mattworzala/debug/shape/OutlineBox.java | 88 +++++++++++++++++++ .../java/net/hollowcube/server/dev/Main.java | 3 +- .../mmo/entity/brain/SingleTaskBrain.java | 2 +- .../mmo/entity/pathfinding/PFNavigator.java | 67 ++++++++++++++ 5 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 modules/common/src/main/java/com/mattworzala/debug/shape/OutlineBox.java diff --git a/modules/common/build.gradle.kts b/modules/common/build.gradle.kts index 1473429c..4a7f912e 100644 --- a/modules/common/build.gradle.kts +++ b/modules/common/build.gradle.kts @@ -8,6 +8,8 @@ dependencies { implementation("org.tinylog:tinylog-impl:2.4.1") + api("com.github.mworzala.mc_debug_renderer:minestom:3ed4c6e97536a19") + implementation("io.github.cdimascio:dotenv-java:2.2.4") // Optional components diff --git a/modules/common/src/main/java/com/mattworzala/debug/shape/OutlineBox.java b/modules/common/src/main/java/com/mattworzala/debug/shape/OutlineBox.java new file mode 100644 index 00000000..f3e545a6 --- /dev/null +++ b/modules/common/src/main/java/com/mattworzala/debug/shape/OutlineBox.java @@ -0,0 +1,88 @@ +package com.mattworzala.debug.shape; + +import com.mattworzala.debug.Layer; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.utils.binary.BinaryWriter; +import net.minestom.server.utils.validate.Check; +import org.jetbrains.annotations.NotNull; + +//todo remove me (this isn't present in mainline debug renderer currently) +public record OutlineBox( + Vec start, + Vec end, + int color, + Layer layer, + int colorLine, + Layer layerLine +) implements Shape { + private static final int ID = 3; + + @Override + public void write(@NotNull BinaryWriter buffer) { + buffer.writeVarInt(ID); + buffer.writeDouble(start.x()); + buffer.writeDouble(start.y()); + buffer.writeDouble(start.z()); + buffer.writeDouble(end.x()); + buffer.writeDouble(end.y()); + buffer.writeDouble(end.z()); + buffer.writeInt(color); + buffer.writeVarInt(layer.ordinal()); + buffer.writeInt(colorLine); + buffer.writeVarInt(layerLine.ordinal()); + } + + public static class Builder { + private Vec start; + private Vec end; + private int color = 0xFFFFFFFF; + private Layer layer = Layer.INLINE; + private int colorLine = 0xFFFFFFFF; + private Layer layerLine = Layer.INLINE; + + public Builder start(Vec start) { + this.start = start; + return this; + } + + public Builder end(Vec end) { + this.end = end; + return this; + } + + public Builder block(int x, int y, int z) { + return block(x, y, z, 0.05); + } + + public Builder block(int x, int y, int z, double expand) { + return start(new Vec(x - expand, y - expand, z - expand)) + .end(new Vec(x + 1 + expand, y + 1 + expand, z + 1 + expand)); + } + + public Builder color(int color) { + this.color = color; + return this; + } + + public Builder layer(Layer layer) { + this.layer = layer; + return this; + } + + public Builder colorLine(int color) { + this.colorLine = color; + return this; + } + + public Builder layerLine(Layer layer) { + this.layerLine = layer; + return this; + } + + public OutlineBox build() { + Check.notNull(start, "start"); + Check.notNull(end, "end"); + return new OutlineBox(start, end, color, layer, colorLine, layerLine); + } + } +} diff --git a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java index 92ef449c..33301cf3 100644 --- a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java +++ b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java @@ -37,6 +37,7 @@ import unnamed.mmo.chat.storage.ChatStorage; import unnamed.mmo.command.BaseCommandRegister; import unnamed.mmo.entity.UnnamedEntity; +import unnamed.mmo.entity.brain.task.WanderInRegionTask; import unnamed.mmo.item.Item; import unnamed.mmo.damage.DamageProcessor; import unnamed.mmo.item.Item; @@ -96,7 +97,7 @@ public static void main(String[] args) { //todo test entity - UnnamedEntity entity = new UnnamedEntity(); + UnnamedEntity entity = new UnnamedEntity(new WanderInRegionTask(new WanderInRegionTask.Spec())); entity.setInstance(instance, new Pos(0, 40, 0)) .thenAccept(unused -> System.out.println("Spawned")); }); diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java index 29fdca29..6e995c3b 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java @@ -15,7 +15,7 @@ public class SingleTaskBrain implements Brain { public SingleTaskBrain(@NotNull Entity entity, @NotNull Task task) { this.entity = entity; - this.navigator = Navigator.enodia(entity); + this.navigator = Navigator.custom(entity); this.task = task; } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java index 8fa35108..d1a6ef3d 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java @@ -1,5 +1,10 @@ package unnamed.mmo.entity.pathfinding; +import com.mattworzala.debug.DebugMessage; +import com.mattworzala.debug.Layer; +import com.mattworzala.debug.shape.Box; +import com.mattworzala.debug.shape.Line; +import com.mattworzala.debug.shape.OutlineBox; import net.minestom.server.attribute.Attribute; import net.minestom.server.collision.CollisionUtils; import net.minestom.server.coordinate.Point; @@ -19,6 +24,7 @@ import org.jetbrains.annotations.Nullable; import java.time.Duration; +import java.util.ArrayList; // TODO all pathfinding requests could be processed in another thread @@ -122,12 +128,19 @@ public synchronized boolean setPathTo(@Nullable Point point, double minimumDista return success; } + private long tick = 0; + @ApiStatus.Internal public synchronized void tick(long tick) { if (goalPosition == null) return; // No path if (entity instanceof LivingEntity && ((LivingEntity) entity).isDead()) return; // No pathfinding tick for dead entities if (path == null) return; + if (tick++ % 5 == 0) { + sendDebugData(); + } + + Point currentTarget = path.getCurrent(); if (currentTarget == null) currentTarget = goalPosition; float movementSpeed = 0.1f; @@ -189,4 +202,58 @@ public boolean isComplete() { if (this.path == null) return true; return goalPosition == null || entity.getPosition().distance(goalPosition) < 1; } + + + + // SECTION: Debug rendering + // Eventually this should be only in the dev server. Just don't currently have a way to do a "mixin" here. + // Probably will have some way to set the entity provider somewhere. + + private @NotNull String debugNamespace(){ + return "debug_" + entity.getUuid(); + } + + private void sendDebugData() { + var builder = DebugMessage.builder() + .clear(debugNamespace()); + + addPathfinderDebugData(builder); + addTargetPoint(builder); + + builder.build() + .sendTo(entity.getViewersAsAudience()); + } + + private void addPathfinderDebugData(DebugMessage.Builder builder) { + if (path == null) return; + var nodes = path.nodes; + var linePoints = new ArrayList(); + + for (int i = path.index; i < nodes.size(); i++) { + var pos = Vec.fromPoint(nodes.get(i)); + builder.set( + debugNamespace() + ":pf_node_" + i, + new Box(pos.sub(0.4, 0.0, 0.4), pos.add(0.4, 0.1, 0.4), 0x331CB2F5, Layer.TOP) + ); + linePoints.add(pos.withY(y -> y + 0.05)); + } + builder.set( + debugNamespace() + ":pf_path", + new Line(linePoints, 10f, 0xFF1CB2F5, Layer.TOP) + ); + } + + private void addTargetPoint(DebugMessage.Builder builder) { + if (goalPosition == null) return; + builder.set( + debugNamespace() + ":pf_target", + new OutlineBox.Builder() + .block(goalPosition.blockX(), goalPosition.blockY(), goalPosition.blockZ(), 0) + .color(0x55FF0000) + .layer(Layer.TOP) + .colorLine(0xFFFF0000) + .layerLine(Layer.TOP) + .build() + ); + } } From 92d1539c274d0868a92a5574d6db2f6a9159daa4 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 21 Aug 2022 16:26:12 +0300 Subject: [PATCH 06/25] start testing selectors, not very happy with the system at all --- .../java/unnamed/mmo/entity/brain/Brain.java | 9 +++ .../mmo/entity/brain/SingleTaskBrain.java | 13 ++++ .../mmo/entity/brain/task/SelectorTask.java | 62 +++++++++++++++++++ .../mmo/entity/pathfinding/PFNavigator.java | 2 +- .../src/main/resources/test_brain.json5 | 43 +++++++++++++ .../entity/src/main/resources/test_fish.json5 | 26 ++++++++ 6 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java create mode 100644 modules/entity/src/main/resources/test_brain.json5 create mode 100644 modules/entity/src/main/resources/test_fish.json5 diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java index 870fab01..03565723 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java @@ -4,6 +4,7 @@ import net.minestom.server.entity.Entity; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import unnamed.mmo.entity.brain.navigator.Navigator; public interface Brain { @@ -19,4 +20,12 @@ default void setInstance(Instance instance) {} void tick(long time); + + // Temp stuff im not in love with + + default @Nullable Entity getTarget() { + return null; + } + + } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java index 6e995c3b..8c26e3ca 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java @@ -4,6 +4,7 @@ import net.minestom.server.entity.Entity; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import unnamed.mmo.entity.brain.navigator.Navigator; import unnamed.mmo.entity.brain.task.Task; @@ -13,6 +14,8 @@ public class SingleTaskBrain implements Brain { private final Navigator navigator; private final Task task; + private Entity target = null; + public SingleTaskBrain(@NotNull Entity entity, @NotNull Task task) { this.entity = entity; this.navigator = Navigator.custom(entity); @@ -48,6 +51,11 @@ public void setInstance(Instance instance) { public void tick(long time) { if (lastInstance == null) return; + //todo also player death or anything else? + if (target.isRemoved()) { + target = null; + } + navigator.tick(time); switch (task.getState()) { case INIT, COMPLETE -> task.start(this); @@ -59,4 +67,9 @@ public void tick(long time) { } } } + + @Override + public @Nullable Entity getTarget() { + return target; + } } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java new file mode 100644 index 00000000..c33beae0 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java @@ -0,0 +1,62 @@ +package unnamed.mmo.entity.brain.task; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minestom.server.entity.Entity; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.brain.Brain; + +import static unnamed.mmo.util.ExtraCodecs.lazy; + +public class SelectorTask extends AbstractTask { + + + private final Spec spec; + private final Task targetTask; + private final Task otherwiseTask; + + private Task activeTask; + + public SelectorTask(Spec spec) { + this.spec = spec; + this.targetTask = spec.target().create(); + this.otherwiseTask = spec.otherwise().create(); + } + + @Override + public void tick(@NotNull Brain brain) { + final Entity target = brain.getTarget(); + // Check if there is a target, and we are not currently running the target task. + if (target != null && activeTask != targetTask) { + // End the active task + if (activeTask != null) { + //todo bad + ((AbstractTask) activeTask).end(true); + } + + // start the other task + activeTask = otherwiseTask; + activeTask.start(brain); + } else if (activeTask != otherwiseTask) + + activeTask.tick(brain); + } + + + public record Spec( + Task.Spec target, + Task.Spec otherwise + ) implements Task.Spec { + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + lazy(() -> Task.Spec.CODEC).fieldOf("target").forGetter(Spec::target), + lazy(() -> Task.Spec.CODEC).fieldOf("otherwise").forGetter(Spec::target) + ).apply(i, Spec::new)); + + @Override + public @NotNull Task create() { + return new SelectorTask(this); + } + } + + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java index d1a6ef3d..d935b641 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java @@ -136,7 +136,7 @@ public synchronized void tick(long tick) { if (entity instanceof LivingEntity && ((LivingEntity) entity).isDead()) return; // No pathfinding tick for dead entities if (path == null) return; - if (tick++ % 5 == 0) { + if (this.tick++ % 5 == 0) { sendDebugData(); } diff --git a/modules/entity/src/main/resources/test_brain.json5 b/modules/entity/src/main/resources/test_brain.json5 new file mode 100644 index 00000000..5e0a5aff --- /dev/null +++ b/modules/entity/src/main/resources/test_brain.json5 @@ -0,0 +1,43 @@ +{ + "namespace": "unnamed:attack_enemy", + "task": { + "type": "selector", + // Define the stimuli for this task. They can provide context such as target entities/positions + "stimuli": [ + { + "type": "target_closest_entity", + "entityType": "player", + "range": 25 + } + ], + // Children are evaluated in order. + "children": { + // If there is a target (from stimuli) + "target != null": { + "type": "parallel", + // Follow the target and attempt to attack it + children: [ + { + "type": "follow_target" + }, + { + "type": "attack_target" + } + ] + }, + // No expression always matches + "": { + "type": "sequence", + "children": [ + { + "type": "idle_time", + "time": 5 + }, + { + "type": "wander_in_region" + } + ] + } + } + } +} \ No newline at end of file diff --git a/modules/entity/src/main/resources/test_fish.json5 b/modules/entity/src/main/resources/test_fish.json5 new file mode 100644 index 00000000..443bc629 --- /dev/null +++ b/modules/entity/src/main/resources/test_fish.json5 @@ -0,0 +1,26 @@ +{ + "namespace": "unnamed:attack_enemy", + "task": { + "type": "selector", + // No stimuli, the fish will have its target set programmatically + "stimuli": [], + "children": { + //If we have a target, move towards it + "target != null": { + "type": "follow_target" + }, + "": { + "type": "sequence", + "children": [ + { + "type": "idle_time", + "time": 5 + }, + { + "type": "wander_in_region" + } + ] + } + } + } +} \ No newline at end of file From 2048b7e50639117df1f01040428b0d57d142f44f Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 21 Aug 2022 17:06:40 +0300 Subject: [PATCH 07/25] very basic working selection --- .../java/net/hollowcube/server/dev/Main.java | 13 ++++++- .../mmo/entity/brain/SingleTaskBrain.java | 6 ++- .../entity/brain/task/FollowTargetTask.java | 37 +++++++++++++++++++ .../mmo/entity/brain/task/SelectorTask.java | 33 ++++++++++++----- .../task/TestFollowTargetTaskIntegration.java | 20 ++++++++++ 5 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestFollowTargetTaskIntegration.java diff --git a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java index 33301cf3..3bfaf979 100644 --- a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java +++ b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java @@ -37,6 +37,9 @@ import unnamed.mmo.chat.storage.ChatStorage; import unnamed.mmo.command.BaseCommandRegister; import unnamed.mmo.entity.UnnamedEntity; +import unnamed.mmo.entity.brain.SingleTaskBrain; +import unnamed.mmo.entity.brain.task.FollowTargetTask; +import unnamed.mmo.entity.brain.task.SelectorTask; import unnamed.mmo.entity.brain.task.WanderInRegionTask; import unnamed.mmo.item.Item; import unnamed.mmo.damage.DamageProcessor; @@ -97,9 +100,17 @@ public static void main(String[] args) { //todo test entity - UnnamedEntity entity = new UnnamedEntity(new WanderInRegionTask(new WanderInRegionTask.Spec())); + UnnamedEntity entity = new UnnamedEntity(new SelectorTask(new SelectorTask.Spec( + new FollowTargetTask.Spec(), + new WanderInRegionTask.Spec() + ))); entity.setInstance(instance, new Pos(0, 40, 0)) .thenAccept(unused -> System.out.println("Spawned")); + + MinecraftServer.getSchedulerManager().buildTask(() -> { + System.out.println("SETTING TARGET"); + ((SingleTaskBrain) entity.brain()).setTarget(player); + }).delay(5, net.minestom.server.utils.time.TimeUnit.SECOND).schedule(); }); BaseCommandRegister.registerCommands(); //todo this should be in a facet? diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java index 8c26e3ca..be8761b0 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java @@ -52,7 +52,7 @@ public void tick(long time) { if (lastInstance == null) return; //todo also player death or anything else? - if (target.isRemoved()) { + if (target != null && target.isRemoved()) { target = null; } @@ -72,4 +72,8 @@ public void tick(long time) { public @Nullable Entity getTarget() { return target; } + + public void setTarget(@NotNull Entity target) { + this.target = target; + } } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java new file mode 100644 index 00000000..07b6d7dc --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java @@ -0,0 +1,37 @@ +package unnamed.mmo.entity.brain.task; + +import net.minestom.server.entity.Entity; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.brain.Brain; + +public class FollowTargetTask extends AbstractTask { + + private int tick = 0; + + @Override + public void start(@NotNull Brain brain) { + super.start(brain); + } + + @Override + public void tick(@NotNull Brain brain) { + final Entity target = brain.getTarget(); + if (target == null) { + end(true); + return; + } + + if (tick++ % 5 == 0) { + brain.setPathTo(target.getPosition()); + } + } + + + public record Spec() implements Task.Spec { + + @Override + public @NotNull Task create() { + return new FollowTargetTask(); + } + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java index c33beae0..d4058e68 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java @@ -10,7 +10,6 @@ public class SelectorTask extends AbstractTask { - private final Spec spec; private final Task targetTask; private final Task otherwiseTask; @@ -28,20 +27,34 @@ public void tick(@NotNull Brain brain) { final Entity target = brain.getTarget(); // Check if there is a target, and we are not currently running the target task. if (target != null && activeTask != targetTask) { - // End the active task - if (activeTask != null) { - //todo bad - ((AbstractTask) activeTask).end(true); - } - - // start the other task - activeTask = otherwiseTask; + selectAndStart(brain, targetTask); + return; + } + if (target == null && activeTask != otherwiseTask) { + selectAndStart(brain, otherwiseTask); + return; + } + + // Restart a task if it has finished + if (activeTask.getState() != State.RUNNING) { activeTask.start(brain); - } else if (activeTask != otherwiseTask) + } activeTask.tick(brain); } + private void selectAndStart(Brain brain, Task task) { + // End the active task + if (activeTask != null) { + //todo bad + ((AbstractTask) activeTask).end(true); + } + + // start the other task + activeTask = task; + activeTask.start(brain); + } + public record Spec( Task.Spec target, diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestFollowTargetTaskIntegration.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestFollowTargetTaskIntegration.java new file mode 100644 index 00000000..d3c580c4 --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestFollowTargetTaskIntegration.java @@ -0,0 +1,20 @@ +package unnamed.mmo.entity.brain.task; + +import net.minestom.server.test.Env; +import net.minestom.server.test.EnvTest; +import org.junit.jupiter.api.Test; + +@EnvTest +public class TestFollowTargetTaskIntegration { + + // No target + @Test + public void testNoTarget(Env env) { + + } + + // Static target + + // Moving target + +} From 7a54bd894a38e38d21a382d84521b482e7f6c019 Mon Sep 17 00:00:00 2001 From: mworzala Date: Tue, 30 Aug 2022 23:20:19 +0300 Subject: [PATCH 08/25] more entity planning, start on mql for entity selectors --- .../server/dev/tool/DebugToolManager.java | 2 +- .../brain/navigator/EnodiaNavigator.java | 2 +- .../mmo/entity/brain/task/SelectorTask.java | 2 +- .../mmo/entity/pathfinding/PFNavigator.java | 6 +- .../entity/pathfinding/PFPathGenerator.java | 8 +- .../src/main/java/unnamed/mmo/mql/README.md | 23 ++++ .../java/unnamed/mmo/mql/parser/MqlLexer.java | 125 ++++++++++++++++++ .../unnamed/mmo/mql/parser/MqlParseError.java | 11 ++ .../unnamed/mmo/mql/parser/MqlParser.java | 83 ++++++++++++ .../java/unnamed/mmo/mql/parser/MqlToken.java | 13 ++ .../mmo/mql/runtime/MqlRuntimeError.java | 9 ++ .../unnamed/mmo/mql/runtime/MqlScope.java | 12 ++ .../unnamed/mmo/mql/tree/MqlAccessExpr.java | 23 ++++ .../unnamed/mmo/mql/tree/MqlBinaryExpr.java | 31 +++++ .../java/unnamed/mmo/mql/tree/MqlExpr.java | 27 ++++ .../unnamed/mmo/mql/tree/MqlIdentExpr.java | 19 +++ .../unnamed/mmo/mql/tree/MqlNumberExpr.java | 23 ++++ .../java/unnamed/mmo/mql/tree/MqlPrinter.java | 42 ++++++ .../java/unnamed/mmo/mql/tree/MqlVisitor.java | 26 ++++ .../unnamed/mmo/mql/value/MqlCallable.java | 12 ++ .../java/unnamed/mmo/mql/value/MqlHolder.java | 9 ++ .../unnamed/mmo/mql/value/MqlIdentValue.java | 8 ++ .../java/unnamed/mmo/mql/value/MqlNull.java | 8 ++ .../unnamed/mmo/mql/value/MqlNumberValue.java | 4 + .../java/unnamed/mmo/mql/value/MqlValue.java | 19 +++ .../src/main/resources/fisherman_brain.json5 | 49 +++++++ .../entity/src/main/resources/sniffing.json5 | 93 +++++++++++++ .../src/main/resources/test_brain.json5 | 27 +++- .../entity/src/main/resources/test_fish.json5 | 4 +- .../task/TestFollowTargetTaskIntegration.java | 6 +- .../test/java/unnamed/mmo/mql/MqlTest.java | 38 ++++++ .../unnamed/mmo/mql/parser/TestMqlLexer.java | 41 ++++++ .../unnamed/mmo/mql/parser/TestMqlParser.java | 39 ++++++ 33 files changed, 825 insertions(+), 19 deletions(-) create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/README.md create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlLexer.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParseError.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParser.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlToken.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlRuntimeError.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlScope.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlAccessExpr.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlBinaryExpr.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlExpr.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlIdentExpr.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlNumberExpr.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlPrinter.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlVisitor.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/value/MqlCallable.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/value/MqlHolder.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/value/MqlIdentValue.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNull.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNumberValue.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/value/MqlValue.java create mode 100644 modules/entity/src/main/resources/fisherman_brain.json5 create mode 100644 modules/entity/src/main/resources/sniffing.json5 create mode 100644 modules/entity/src/test/java/unnamed/mmo/mql/MqlTest.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlLexer.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlParser.java diff --git a/modules/development/src/main/java/net/hollowcube/server/dev/tool/DebugToolManager.java b/modules/development/src/main/java/net/hollowcube/server/dev/tool/DebugToolManager.java index 50a3689e..ae95c6ba 100644 --- a/modules/development/src/main/java/net/hollowcube/server/dev/tool/DebugToolManager.java +++ b/modules/development/src/main/java/net/hollowcube/server/dev/tool/DebugToolManager.java @@ -110,7 +110,7 @@ private void handAnimation(@NotNull PlayerHandAnimationEvent event) { if (tool == null || debounce(player)) return; final Point targetBlock = player.getTargetBlockPosition(3); - //todo ray trace for target entity + //todo ray trace for value entity final ItemStack newItemStack = tool.leftClicked(player, itemStack, targetBlock, null); if (itemStack != newItemStack) { diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java index bda0caa4..ffaa6f76 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java @@ -47,7 +47,7 @@ public boolean setPathTo(@Nullable Point point) { return true; } - // Last parameter here is the max distance from the target. + // Last parameter here is the max distance from the value. return enodia.goTo(point, DEFAULT_IMPORTANCE, 0.5f); } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java index d4058e68..aa7bc282 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java @@ -25,7 +25,7 @@ public SelectorTask(Spec spec) { @Override public void tick(@NotNull Brain brain) { final Entity target = brain.getTarget(); - // Check if there is a target, and we are not currently running the target task. + // Check if there is a value, and we are not currently running the value task. if (target != null && activeTask != targetTask) { selectAndStart(brain, targetTask); return; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java index d935b641..42d86865 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java @@ -88,7 +88,7 @@ public void jump(float height) { */ public synchronized boolean setPathTo(@Nullable Point point, double minimumDistance) { if (point != null && goalPosition != null && point.samePoint(goalPosition) && this.path != null) { - // Tried to set path to the same target position + // Tried to set path to the same value position return false; } final Instance instance = entity.getInstance(); @@ -177,9 +177,9 @@ public synchronized void tick(long tick) { /** - * Gets the target pathfinder position. + * Gets the value pathfinder position. * - * @return the target pathfinder position, null if there is no one + * @return the value pathfinder position, null if there is no one */ public @Nullable Point getGoalPosition() { return goalPosition; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java index d4b00cb4..132bbc70 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java @@ -67,8 +67,8 @@ public static PFPath generate(Instance instance, Point orgStart, Point orgTarget } } -// var nearbyPoints = current.getNearby(instance, closed, target, expandedBoundingBox) -// .stream().filter(p -> p.point.distance(target) <= maxDistance).collect(Collectors.toSet()); +// var nearbyPoints = current.getNearby(instance, closed, value, expandedBoundingBox) +// .stream().filter(p -> p.point.distance(value) <= maxDistance).collect(Collectors.toSet()); } @@ -86,7 +86,7 @@ public static PFPath generate(Instance instance, Point orgStart, Point orgTarget // // PNode current = open.poll(); // -// if (current.point.distance(target) > closeDistance) return null; +// if (current.point.distance(value) > closeDistance) return null; // // while (current.parent != null) { // path.getNodes().add(current); @@ -98,7 +98,7 @@ public static PFPath generate(Instance instance, Point orgStart, Point orgTarget // path.addNodes(instance, expandedBoundingBox); // if (path.getNodes().size() > 0) { -// PNode pEnd = new PNode(target, 0, 0, path.getNodes().get(path.getNodes().size() - 1)); +// PNode pEnd = new PNode(value, 0, 0, path.getNodes().get(path.getNodes().size() - 1)); // path.getNodes().add(pEnd); // } // diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/README.md b/modules/entity/src/main/java/unnamed/mmo/mql/README.md new file mode 100644 index 00000000..1b5e1dc0 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/README.md @@ -0,0 +1,23 @@ +# Minecraft Query Language (mql) + +a.k.a MoLang Jr. + +A subset of MoLang (may eventually be a full implementation). Currently implemented as a basic tree-walk interpreter, +but may eventually be refactored to something more performant in the future. + +## Syntax + +`mql` supports the following syntax +* [Query functions](#query-functions) + +## Query Functions + +`mql` implements a subset of the query functions in MoLang. They are described below. + +| Function | Status | Description | +|---------------|-----------------|------------------------------------------------------------------------------------------------------------| +| q.time_of_day | NOT IMPLEMENTED | Gets the current time of day in the world as a decimal: midnight=0.0, sunrise=0.25, noon=0.5, sunset=0.75. | +| q.has_target | NOT IMPLEMENTED | Returns true if the entity has a target, false otherwise. | +| | | | +| | | | +| | | | diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlLexer.java b/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlLexer.java new file mode 100644 index 00000000..22359a01 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlLexer.java @@ -0,0 +1,125 @@ +package unnamed.mmo.mql.parser; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class MqlLexer { + private final String source; + + private int start = 0; + private int cursor = 0; + + public MqlLexer(@NotNull String source) { + this.source = source; + } + + /** + * Returns the next token in the input, or null if the end of file was reached. + * @throws MqlParseError if there is an unexpected token. + */ + public @Nullable MqlToken next() { + start = cursor; + + if (atEnd()) return null; + + consumeWhitespace(); + + char c = advance(); + if (isAlpha(c)) + return ident(); + if (isDigit(c)) + return number(); + + return symbol(c); + } + + /** + * Returns the next token without stepping to the next token in the input, + * or null if the end of file was reached. + * @throws MqlParseError if there is an unexpected token. + */ + public @Nullable MqlToken peek() { + var result = next(); + cursor = start; // Reset to where it was before the call to next. + return result; + } + + public @NotNull String span(@NotNull MqlToken token) { + return source.substring(start, cursor); + } + + private void consumeWhitespace() { + while (true) { + switch (peek0()) { + case ' ', '\t', '\r', '\n' -> advance(); + default -> { + return; + } + } + } + } + + private MqlToken ident() { + while (isAlpha(peek0()) || isDigit(peek0())) { + advance(); + } + + return new MqlToken(MqlToken.Type.IDENT, start, cursor); + } + + private MqlToken number() { + // Pre decimal + while (isDigit(peek0())) + advance(); + + // Decimal, if present + if (match('.')) { + while (isDigit(peek0())) + advance(); + } + + return new MqlToken(MqlToken.Type.NUMBER, start, cursor); + } + + private MqlToken symbol(char c) { + var tokenType = switch (c) { + // @formatter:off + case '+' -> MqlToken.Type.PLUS; + case '.' -> MqlToken.Type.DOT; + default -> throw new MqlParseError( + String.format("unexpected token '%s' at %d.", c, cursor)); + // @formatter:on + }; + return new MqlToken(tokenType, start, cursor); + } + + private boolean atEnd() { + return cursor >= source.length(); + } + + private char peek0() { + if (atEnd()) + return '\u0000'; + return source.charAt(cursor); + } + + private char advance() { + if (atEnd()) throw new MqlParseError("unexpected end of input"); + return source.charAt(cursor++); + } + + private boolean match(char c) { + if (atEnd()) return false; + if (peek0() != c) return false; + advance(); + return true; + } + + private boolean isAlpha(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; + } + + private boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParseError.java b/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParseError.java new file mode 100644 index 00000000..b4c1b286 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParseError.java @@ -0,0 +1,11 @@ +package unnamed.mmo.mql.parser; + +import org.jetbrains.annotations.NotNull; + +public class MqlParseError extends RuntimeException { + + public MqlParseError(@NotNull String message) { + super(message); + } + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParser.java b/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParser.java new file mode 100644 index 00000000..5fdf9b02 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParser.java @@ -0,0 +1,83 @@ +package unnamed.mmo.mql.parser; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import unnamed.mmo.mql.tree.*; + +public class MqlParser { + private final MqlLexer lexer; + + public MqlParser(@NotNull String source) { + this.lexer = new MqlLexer(source); + } + + public @NotNull MqlExpr parse() { + return expr(0); + } + + private @NotNull MqlExpr expr(int minBindingPower) { + MqlExpr lhs = lhs(); + + while (true) { + InfixOp op = infixOp(); + if (op == null) break; + + // Stop if operator left binding power is less than the current min + if (op.lbp < minBindingPower) break; + + lexer.next(); // Operator token + + // Parse right side expression + MqlExpr rhs = expr(op.rbp); + lhs = switch (op) { + case MEMBER_ACCESS -> { + if (!(rhs instanceof MqlIdentExpr ident)) + throw new MqlParseError("rhs of member access must be an ident, was " + rhs); + yield new MqlAccessExpr(lhs, ident.value()); + } + default -> new MqlBinaryExpr(op.op, lhs, rhs); + }; + } + + return lhs; + } + + /** Parses a possible left side expression. */ + private @NotNull MqlExpr lhs() { + MqlToken token = lexer.next(); + if (token == null) throw new MqlParseError("unexpected end of input"); + + return switch (token.type()) { + case NUMBER -> new MqlNumberExpr(Double.parseDouble(lexer.span(token))); + case IDENT -> new MqlIdentExpr(lexer.span(token)); + //todo better error handling + default -> throw new MqlParseError("unexpected token " + token); + }; + } + + private @Nullable InfixOp infixOp() { + MqlToken token = lexer.peek(); + if (token == null) return null; + return switch (token.type()) { + case PLUS -> InfixOp.PLUS; + case DOT -> InfixOp.MEMBER_ACCESS; + default -> null; + }; + } + + private enum InfixOp { + PLUS(25, 26, MqlBinaryExpr.Op.PLUS), + MEMBER_ACCESS(35, 36, null); + + private final int lbp; + private final int rbp; + private final MqlBinaryExpr.Op op; + + InfixOp(int lbp, int rbp, MqlBinaryExpr.Op op) { + this.lbp = lbp; + this.rbp = rbp; + this.op = op; + } + } + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlToken.java b/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlToken.java new file mode 100644 index 00000000..343a6fba --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlToken.java @@ -0,0 +1,13 @@ +package unnamed.mmo.mql.parser; + +import org.jetbrains.annotations.NotNull; + +record MqlToken(@NotNull Type type, int start, int end) { + + enum Type { + PLUS, + DOT, + NUMBER, IDENT; + } + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlRuntimeError.java b/modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlRuntimeError.java new file mode 100644 index 00000000..5eb023f9 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlRuntimeError.java @@ -0,0 +1,9 @@ +package unnamed.mmo.mql.runtime; + +import org.jetbrains.annotations.NotNull; + +public class MqlRuntimeError extends RuntimeException { + public MqlRuntimeError(@NotNull String message) { + super(message); + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlScope.java b/modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlScope.java new file mode 100644 index 00000000..146c949f --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlScope.java @@ -0,0 +1,12 @@ +package unnamed.mmo.mql.runtime; + +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.mql.value.MqlValue; + +public interface MqlScope { + + @NotNull MqlValue get(@NotNull String name); + + //todo setter as well i guess + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlAccessExpr.java b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlAccessExpr.java new file mode 100644 index 00000000..f868a3b1 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlAccessExpr.java @@ -0,0 +1,23 @@ +package unnamed.mmo.mql.tree; + +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.mql.runtime.MqlScope; +import unnamed.mmo.mql.value.MqlHolder; +import unnamed.mmo.mql.value.MqlValue; + +public record MqlAccessExpr( + @NotNull MqlExpr lhs, + @NotNull String target +) implements MqlExpr { + + @Override + public MqlValue evaluate(@NotNull MqlScope scope) { + var lhs = lhs().evaluate(scope).cast(MqlHolder.class); + return lhs.get(target()); + } + + @Override + public R visit(@NotNull MqlVisitor visitor, P p) { + return visitor.visitAccessExpr(this, p); + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlBinaryExpr.java b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlBinaryExpr.java new file mode 100644 index 00000000..c7e5ef79 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlBinaryExpr.java @@ -0,0 +1,31 @@ +package unnamed.mmo.mql.tree; + +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.mql.runtime.MqlScope; +import unnamed.mmo.mql.value.MqlNumberValue; +import unnamed.mmo.mql.value.MqlValue; + +public record MqlBinaryExpr( + @NotNull Op operator, + @NotNull MqlExpr lhs, + @NotNull MqlExpr rhs +) implements MqlExpr { + public enum Op { + PLUS + } + + @Override + public MqlValue evaluate(@NotNull MqlScope scope) { + var lhs = lhs().evaluate(scope).cast(MqlNumberValue.class); + var rhs = lhs().evaluate(scope).cast(MqlNumberValue.class); + + return switch (operator()) { + case PLUS -> new MqlNumberValue(lhs.value() + rhs.value()); + }; + } + + @Override + public R visit(@NotNull MqlVisitor visitor, P p) { + return visitor.visitBinaryExpr(this, p); + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlExpr.java b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlExpr.java new file mode 100644 index 00000000..80d9c056 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlExpr.java @@ -0,0 +1,27 @@ +package unnamed.mmo.mql.tree; + +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.mql.runtime.MqlScope; +import unnamed.mmo.mql.value.MqlNumberValue; +import unnamed.mmo.mql.value.MqlValue; + +public sealed interface MqlExpr permits MqlAccessExpr, MqlBinaryExpr, MqlNumberExpr, MqlIdentExpr { + + MqlValue evaluate(@NotNull MqlScope scope); + + default double evaluateToNumber(@NotNull MqlScope scope) { + MqlValue result = evaluate(scope); + if (result instanceof MqlNumberValue num) + return num.value(); + return 0.0; + } + + default boolean evaluateToBool(@NotNull MqlScope scope) { + MqlValue result = evaluate(scope); + if (result instanceof MqlNumberValue num) + return num.value() != 0; + return result != MqlValue.NULL; + } + + R visit(@NotNull MqlVisitor visitor, P p); +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlIdentExpr.java b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlIdentExpr.java new file mode 100644 index 00000000..d28da062 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlIdentExpr.java @@ -0,0 +1,19 @@ +package unnamed.mmo.mql.tree; + +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.mql.runtime.MqlScope; +import unnamed.mmo.mql.value.MqlIdentValue; +import unnamed.mmo.mql.value.MqlValue; + +public record MqlIdentExpr(@NotNull String value) implements MqlExpr { + + @Override + public MqlValue evaluate(@NotNull MqlScope scope) { + return scope.get(value); + } + + @Override + public R visit(@NotNull MqlVisitor visitor, P p) { + return visitor.visitRefExpr(this, p); + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlNumberExpr.java b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlNumberExpr.java new file mode 100644 index 00000000..05b0ab8f --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlNumberExpr.java @@ -0,0 +1,23 @@ +package unnamed.mmo.mql.tree; + +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.mql.runtime.MqlScope; +import unnamed.mmo.mql.value.MqlNumberValue; +import unnamed.mmo.mql.value.MqlValue; + +public record MqlNumberExpr(@NotNull MqlNumberValue value) implements MqlExpr { + + public MqlNumberExpr(double value) { + this(new MqlNumberValue(value)); + } + + @Override + public MqlValue evaluate(@NotNull MqlScope scope) { + return value; + } + + @Override + public R visit(@NotNull MqlVisitor visitor, P p) { + return visitor.visitNumberExpr(this, p); + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlPrinter.java b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlPrinter.java new file mode 100644 index 00000000..6b99dbb0 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlPrinter.java @@ -0,0 +1,42 @@ +package unnamed.mmo.mql.tree; + +import org.jetbrains.annotations.NotNull; + +public class MqlPrinter implements MqlVisitor { + + @Override + public String visitBinaryExpr(@NotNull MqlBinaryExpr expr, Void unused) { + return String.format( + "(%s %s %s)", + switch (expr.operator()) { + case PLUS -> "+"; + }, + visit(expr.lhs(), null), + visit(expr.rhs(), null) + ); + } + + @Override + public String visitAccessExpr(@NotNull MqlAccessExpr expr, Void unused) { + return String.format( + "(. %s %s)", + visit(expr.lhs(), null), + expr.target() + ); + } + + @Override + public String visitNumberExpr(@NotNull MqlNumberExpr expr, Void unused) { + return String.valueOf(expr.value()); + } + + @Override + public String visitRefExpr(@NotNull MqlIdentExpr expr, Void unused) { + return expr.value(); + } + + @Override + public String defaultValue() { + return "##Error"; + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlVisitor.java b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlVisitor.java new file mode 100644 index 00000000..625b44ae --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlVisitor.java @@ -0,0 +1,26 @@ +package unnamed.mmo.mql.tree; + +import org.jetbrains.annotations.NotNull; + +// @formatter:off +public interface MqlVisitor { + + default R visitBinaryExpr(@NotNull MqlBinaryExpr expr, P p) { return defaultValue(); } + + default R visitAccessExpr(@NotNull MqlAccessExpr expr, P p) { return defaultValue(); } + + default R visitNumberExpr(@NotNull MqlNumberExpr expr, P p) { return defaultValue(); } + + default R visitRefExpr(@NotNull MqlIdentExpr expr, P p) { return defaultValue(); } + + + + + default R visit(@NotNull MqlExpr expr, P p) { + return expr.visit(this, p); + } + + R defaultValue(); + +} +// @formatter:on diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlCallable.java b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlCallable.java new file mode 100644 index 00000000..b9552271 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlCallable.java @@ -0,0 +1,12 @@ +package unnamed.mmo.mql.value; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +@FunctionalInterface +public non-sealed interface MqlCallable extends MqlValue { + + @NotNull MqlValue call(@NotNull List args); + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlHolder.java b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlHolder.java new file mode 100644 index 00000000..b1e35aae --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlHolder.java @@ -0,0 +1,9 @@ +package unnamed.mmo.mql.value; + +import org.jetbrains.annotations.NotNull; + +public non-sealed interface MqlHolder extends MqlValue { + + @NotNull MqlValue get(@NotNull String name); + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlIdentValue.java b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlIdentValue.java new file mode 100644 index 00000000..9c2bb4b8 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlIdentValue.java @@ -0,0 +1,8 @@ +package unnamed.mmo.mql.value; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public record MqlIdentValue(@NotNull String value) implements MqlValue { +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNull.java b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNull.java new file mode 100644 index 00000000..150e8e62 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNull.java @@ -0,0 +1,8 @@ +package unnamed.mmo.mql.value; + +final class MqlNull implements MqlValue { + public static final MqlNull INSTANCE = new MqlNull(); + + private MqlNull() {} + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNumberValue.java b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNumberValue.java new file mode 100644 index 00000000..3b8d1dfc --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNumberValue.java @@ -0,0 +1,4 @@ +package unnamed.mmo.mql.value; + +public record MqlNumberValue(double value) implements MqlValue { +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlValue.java b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlValue.java new file mode 100644 index 00000000..eeb59ab6 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlValue.java @@ -0,0 +1,19 @@ +package unnamed.mmo.mql.value; + +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.mql.runtime.MqlRuntimeError; + +/** + * Mutable marker for any possible mql value. + */ +public sealed interface MqlValue permits MqlCallable, MqlHolder, MqlIdentValue, MqlNull, MqlNumberValue { + MqlValue NULL = MqlNull.INSTANCE; + + default Target cast(@NotNull Class targetType) { + if (targetType.isInstance(this)) + //noinspection unchecked + return (Target) this; + throw new MqlRuntimeError("cannot cast " + this.getClass().getSimpleName() + " to " + targetType.getSimpleName()); + } + +} diff --git a/modules/entity/src/main/resources/fisherman_brain.json5 b/modules/entity/src/main/resources/fisherman_brain.json5 new file mode 100644 index 00000000..be40be3d --- /dev/null +++ b/modules/entity/src/main/resources/fisherman_brain.json5 @@ -0,0 +1,49 @@ +{ + "namespace": "unnamed:fisherman_npc", + "task": { + "type": "selector", + "children": { + "world.time < 700": { + // sleep + "type": "sleep_in_bed", + "bed": [5, 5, 5], + + // Allow all to interrupt the other actions (since they should change when time changes) + "canInterrupt": true, + }, + "q.time_of_day < 0.25": { + // Idle walk around in house + "type": "sequence", + "children": [ + { + "type": "idle_time", + "time": {"min": 5, "max": 100} + }, + { + "type": "wander_in_region", + "region": [[1, 1, 1], [10, 10, 10]] + } + ], + + "canInterrupt": true + }, + "world.time < 2400": { + // Walk to fishing spot then play animation forever + "type": "sequence", + "children": [ + { + "type": "walk_to_fixed", + "point": [2, 2, 2] + }, + { + "type": "play_animation", + "animation": "fisherman_fishing", + "repeat": "forever", + } + ], + + "canInterrupt": true + }, + } + } +} \ No newline at end of file diff --git a/modules/entity/src/main/resources/sniffing.json5 b/modules/entity/src/main/resources/sniffing.json5 new file mode 100644 index 00000000..86131f2f --- /dev/null +++ b/modules/entity/src/main/resources/sniffing.json5 @@ -0,0 +1,93 @@ +{ + "namespace": "unnamed:sniffing", + "task": { + "type": "selector", + "stimuli": [ + { + // Target closest player that it has line of sight to + "type": "target_closest", + "range": 25 + }, + // Below is the stimuli for the sniffing value + { + // Probably not super reusable, also we dont want to track this for every region (would be expensive) + // so maybe it would be even more specific. + "type": "ticks_in_region", + "ref": "forest", + + // Not sure about this, but need some way to have multiple targets here + "into": "sniffTarget" + }, + { + // An alternative to above with some kind of weighting system + "type": "weighted_target", + "children": [ + { + "type": "target_closest_no_los", + "entityType": "player", + "range": { + "type": "region", + "ref": "forest" + }, + + "weight": 0.1 + }, + { + "type": "ticks_in_region", + "ref": "forest", + + "weight": 1 + } + ] + } + ], + "children": { + "target != null": { + // Move towards the closest player + "type": "follow_target", + + //todo attack when close enough + }, + "sniffTarget != null": { + // Move towards the player, stopping every once in a while for a bit + "type": "sequence", + "children": [ + { + "type": "timeout", + "length": 100, + "child": { + "type": "follow_target", + "navigator": { + "movementSpeed": 0.5 + } + }, + }, + { + "type": "play_animation", + "animation": "bear_sniff" + } + ], + + // This should be interruptable in case it gets line of sight to somebody + "canInterrupt": true + }, + // Default case, matches everything + "": { + // Idle wander in the center of the forest + "type": "sequence", + "children": [ + { + "type": "wander_in_region", + "ref": "forest_center" + }, + { + "type": "idle_time", + "time": 5 + } + ], + + "canInterrupt": true + }, + } + } +} \ No newline at end of file diff --git a/modules/entity/src/main/resources/test_brain.json5 b/modules/entity/src/main/resources/test_brain.json5 index 5e0a5aff..d76a6bf7 100644 --- a/modules/entity/src/main/resources/test_brain.json5 +++ b/modules/entity/src/main/resources/test_brain.json5 @@ -2,20 +2,39 @@ "namespace": "unnamed:attack_enemy", "task": { "type": "selector", - // Define the stimuli for this task. They can provide context such as target entities/positions + // Define the stimuli for this task. They can provide context such as value entities/positions "stimuli": [ { "type": "target_closest_entity", "entityType": "player", - "range": 25 + "range": 25, + // If true, it will keep looking and update accordingly, rather than continuing to value + // the same entity until it is no longer in range. + "dynamic": false, + }, + { + // Will value the last attacker always + "type": "target_last_attacker", + //todo how will it decide how long until it forgets? Entity setting for memory? + //todo min time until it can change value (in case two people are back and forth hitting it) } + + // Various necessary behaviors + // - smelling: value without line of sight then pause (do an animation) and continue targeting + // - hearing: warden-like + // - slime jump on head: jumps on your head and you have to hit it off + + // - interrupts: Be able to interrupt an action if a condition changes. + // this works in one of 2 ways: + // - specifying that certain actions may interrupt others + // - specifying that certain can be interrupted ], // Children are evaluated in order. "children": { - // If there is a target (from stimuli) + // If there is a value (from stimuli) "target != null": { "type": "parallel", - // Follow the target and attempt to attack it + // Follow the value and attempt to attack it children: [ { "type": "follow_target" diff --git a/modules/entity/src/main/resources/test_fish.json5 b/modules/entity/src/main/resources/test_fish.json5 index 443bc629..2d13d516 100644 --- a/modules/entity/src/main/resources/test_fish.json5 +++ b/modules/entity/src/main/resources/test_fish.json5 @@ -2,10 +2,10 @@ "namespace": "unnamed:attack_enemy", "task": { "type": "selector", - // No stimuli, the fish will have its target set programmatically + // No stimuli, the fish will have its value set programmatically "stimuli": [], "children": { - //If we have a target, move towards it + // If we have a value, move towards it "target != null": { "type": "follow_target" }, diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestFollowTargetTaskIntegration.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestFollowTargetTaskIntegration.java index d3c580c4..1492bbd4 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestFollowTargetTaskIntegration.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestFollowTargetTaskIntegration.java @@ -7,14 +7,14 @@ @EnvTest public class TestFollowTargetTaskIntegration { - // No target + // No value @Test public void testNoTarget(Env env) { } - // Static target + // Static value - // Moving target + // Moving value } diff --git a/modules/entity/src/test/java/unnamed/mmo/mql/MqlTest.java b/modules/entity/src/test/java/unnamed/mmo/mql/MqlTest.java new file mode 100644 index 00000000..e56fcbfa --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/mql/MqlTest.java @@ -0,0 +1,38 @@ +package unnamed.mmo.mql; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import unnamed.mmo.mql.parser.MqlParser; +import unnamed.mmo.mql.runtime.MqlRuntimeError; +import unnamed.mmo.mql.runtime.MqlScope; +import unnamed.mmo.mql.value.MqlHolder; +import unnamed.mmo.mql.value.MqlNumberValue; +import unnamed.mmo.mql.value.MqlValue; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MqlTest { + + @Test + public void basicQueryCall() { + var source = "q.is_alive"; + var expr = new MqlParser(source).parse(); + + var scope = new MqlScope() { + @Override + public @NotNull MqlValue get(@NotNull String name) { + if (!name.equals("q") && !name.equals("query")) + throw new MqlRuntimeError("unknown environment object: " + name); + return (MqlHolder) queryFunction -> switch (queryFunction) { + case "is_alive" -> new MqlNumberValue(1); + default -> throw new MqlRuntimeError("no such query function: " + queryFunction); + }; + } + }; + var result = expr.evaluateToBool(scope); + + assertTrue(result); + + } + +} diff --git a/modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlLexer.java b/modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlLexer.java new file mode 100644 index 00000000..c53d151b --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlLexer.java @@ -0,0 +1,41 @@ +package unnamed.mmo.mql.parser; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestMqlLexer { + + @ParameterizedTest + @MethodSource("individualSymbols") + public void testIndividualSymbols(String input, MqlToken.Type expected) { + var lexer = new MqlLexer(input); + + var token = lexer.next(); + assertNotNull(token); + assertEquals(expected, token.type()); + + var eof = lexer.next(); + assertNull(eof); + } + + private static Stream individualSymbols() { + return Stream.of( + Arguments.of("+", MqlToken.Type.PLUS), + Arguments.of(".", MqlToken.Type.DOT), + + Arguments.of("123", MqlToken.Type.NUMBER), + Arguments.of("123.", MqlToken.Type.NUMBER), + Arguments.of("123.456", MqlToken.Type.NUMBER), + + Arguments.of("abc", MqlToken.Type.IDENT), + Arguments.of("aBc", MqlToken.Type.IDENT), + Arguments.of("aBc1", MqlToken.Type.IDENT) + ); + } + +} diff --git a/modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlParser.java b/modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlParser.java new file mode 100644 index 00000000..baffb99a --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlParser.java @@ -0,0 +1,39 @@ +package unnamed.mmo.mql.parser; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import unnamed.mmo.mql.tree.MqlPrinter; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestMqlParser { + + @MethodSource("inputPairs") + @ParameterizedTest(name = "{0}") + public void testInputPairs(String name, String input, String expected) { + var expr = new MqlParser(input).parse(); + var actual = new MqlPrinter().visit(expr, null); + + assertEquals(expected, actual); + } + + private static Stream inputPairs() { + return Stream.of( + Arguments.of("basic number", + "1", "1.0"), + Arguments.of("basic ref", + "abc", "abc"), + Arguments.of("basic add", + "1 + 2", "(+ 1.0 2.0)"), + Arguments.of("nested add", + "1 + 2 + 3", "(+ (+ 1.0 2.0) 3.0)"), + Arguments.of("basic access", + "a.b", "(. a b)"), + Arguments.of("access/add precedence", + "a.b + 1", "(+ (. a b) 1.0)") + ); + } +} From 89403f795c8278083e85422784a21658207cecf3 Mon Sep 17 00:00:00 2001 From: mworzala Date: Thu, 1 Sep 2022 20:09:09 +0300 Subject: [PATCH 09/25] minor tweaks and experimentation --- .../mmo/entity/brain/task/SelectorTask.java | 110 +++++++++++++----- .../main/java/unnamed/mmo/mql/MqlScript.java | 39 +++++++ .../java/unnamed/mmo/mql/tree/MqlExpr.java | 15 --- .../src/main/resources/fisherman_brain.json5 | 2 +- .../test/java/unnamed/mmo/mql/MqlTest.java | 8 +- 5 files changed, 127 insertions(+), 47 deletions(-) create mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/MqlScript.java diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java index aa7bc282..734a8de3 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java @@ -1,46 +1,54 @@ package unnamed.mmo.entity.brain.task; +import com.google.gson.JsonParser; +import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; import unnamed.mmo.entity.brain.Brain; +import unnamed.mmo.mql.MqlScript; +import unnamed.mmo.util.DFUUtil; + +import java.util.List; +import java.util.function.Function; import static unnamed.mmo.util.ExtraCodecs.lazy; public class SelectorTask extends AbstractTask { private final Spec spec; - private final Task targetTask; - private final Task otherwiseTask; +// private final Task targetTask; +// private final Task otherwiseTask; private Task activeTask; public SelectorTask(Spec spec) { this.spec = spec; - this.targetTask = spec.target().create(); - this.otherwiseTask = spec.otherwise().create(); +// this.targetTask = spec.target().create(); +// this.otherwiseTask = spec.otherwise().create(); } @Override public void tick(@NotNull Brain brain) { - final Entity target = brain.getTarget(); - // Check if there is a value, and we are not currently running the value task. - if (target != null && activeTask != targetTask) { - selectAndStart(brain, targetTask); - return; - } - if (target == null && activeTask != otherwiseTask) { - selectAndStart(brain, otherwiseTask); - return; - } - - // Restart a task if it has finished - if (activeTask.getState() != State.RUNNING) { - activeTask.start(brain); - } - - activeTask.tick(brain); +// final Entity target = brain.getTarget(); +// // Check if there is a value, and we are not currently running the value task. +// if (target != null && activeTask != targetTask) { +// selectAndStart(brain, targetTask); +// return; +// } +// if (target == null && activeTask != otherwiseTask) { +// selectAndStart(brain, otherwiseTask); +// return; +// } +// +// // Restart a task if it has finished +// if (activeTask.getState() != State.RUNNING) { +// activeTask.start(brain); +// } +// +// activeTask.tick(brain); } private void selectAndStart(Brain brain, Task task) { @@ -55,15 +63,32 @@ private void selectAndStart(Brain brain, Task task) { activeTask.start(brain); } + public record Descriptor( + MqlScript script, + Task.Spec task, + boolean canInterrupt + ) implements Task.Spec { + @Override + public @NotNull Task create() { + return task.create(); + } + } public record Spec( - Task.Spec target, - Task.Spec otherwise + //todo stimuli + List children ) implements Task.Spec { - public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( - lazy(() -> Task.Spec.CODEC).fieldOf("target").forGetter(Spec::target), - lazy(() -> Task.Spec.CODEC).fieldOf("otherwise").forGetter(Spec::target) - ).apply(i, Spec::new)); + public static final Codec> abcx = Codec.pair( + lazy(() -> Task.Spec.CODEC), + RecordCodecBuilder.create(i -> i.group( + Codec.BOOL.fieldOf("canInterrupt").forGetter(b -> b) + ).apply(i, b -> b)) + ); +// public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( +// Codec.unboundedMap(MqlScript.CODEC, Codec.pair(lazy(() -> Task.Spec.CODEC), Codec.BOOL.fieldOf("").forGetter(it -> false))) +// .xmap(DFUUtil::mapToPairList, DFUUtil::pairListToMap) +// .fieldOf("children").forGetter(Spec::children) +// ).apply(i, Spec::new)); @Override public @NotNull Task create() { @@ -71,5 +96,36 @@ public record Spec( } } + public static void main(String[] args) { +// var result = JsonOps.INSTANCE.withDecoder(Spec.CODEC) +// .apply(JsonParser.parseString(""" +// { +// "children": { +// "1.0": "abc", +// "q.is_alive": "def", +// "3.0": "ghi" +// } +// }""")) +// .result() +// .get() +// .getFirst(); + var result2 = JsonOps.INSTANCE.withDecoder(Spec.abcx) + .apply(JsonParser.parseString(""" + { + "type": "unnamed:idle", + "time": 1, + "canInterrupt": false + }""")) + .result() + .get() + .getFirst(); + + System.out.println(result2); + +// for (var entry : result.children) { +// System.out.println(entry.getSecond()); +// } + } + } diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/MqlScript.java b/modules/entity/src/main/java/unnamed/mmo/mql/MqlScript.java new file mode 100644 index 00000000..ef5e83d4 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/mql/MqlScript.java @@ -0,0 +1,39 @@ +package unnamed.mmo.mql; + +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.mql.parser.MqlParser; +import unnamed.mmo.mql.runtime.MqlScope; +import unnamed.mmo.mql.tree.MqlExpr; +import unnamed.mmo.mql.value.MqlNumberValue; +import unnamed.mmo.mql.value.MqlValue; +import unnamed.mmo.util.DFUUtil; + +public record MqlScript(@NotNull MqlExpr expr) { + + public static final Codec CODEC = Codec.either(Codec.STRING, Codec.STRING.listOf()) + .xmap(either -> DFUUtil.value(either.mapRight(lines -> String.join("\n", lines))), Either::left) + //todo lossless parser + .xmap(MqlScript::parse, unused -> {throw new RuntimeException("cannot serialize an mql script");}); + + public static @NotNull MqlScript parse(@NotNull String script) { + MqlExpr expr = new MqlParser(script).parse(); + return new MqlScript(expr); + } + + public double evaluate(@NotNull MqlScope scope) { + MqlValue result = expr().evaluate(scope); + if (result instanceof MqlNumberValue num) + return num.value(); + return 0.0; + } + + public boolean evaluateToBool(@NotNull MqlScope scope) { + MqlValue result = expr().evaluate(scope); + if (result instanceof MqlNumberValue num) + return num.value() != 0; + return result != MqlValue.NULL; + } + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlExpr.java b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlExpr.java index 80d9c056..ce2b40e8 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlExpr.java +++ b/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlExpr.java @@ -2,26 +2,11 @@ import org.jetbrains.annotations.NotNull; import unnamed.mmo.mql.runtime.MqlScope; -import unnamed.mmo.mql.value.MqlNumberValue; import unnamed.mmo.mql.value.MqlValue; public sealed interface MqlExpr permits MqlAccessExpr, MqlBinaryExpr, MqlNumberExpr, MqlIdentExpr { MqlValue evaluate(@NotNull MqlScope scope); - default double evaluateToNumber(@NotNull MqlScope scope) { - MqlValue result = evaluate(scope); - if (result instanceof MqlNumberValue num) - return num.value(); - return 0.0; - } - - default boolean evaluateToBool(@NotNull MqlScope scope) { - MqlValue result = evaluate(scope); - if (result instanceof MqlNumberValue num) - return num.value() != 0; - return result != MqlValue.NULL; - } - R visit(@NotNull MqlVisitor visitor, P p); } diff --git a/modules/entity/src/main/resources/fisherman_brain.json5 b/modules/entity/src/main/resources/fisherman_brain.json5 index be40be3d..84441615 100644 --- a/modules/entity/src/main/resources/fisherman_brain.json5 +++ b/modules/entity/src/main/resources/fisherman_brain.json5 @@ -27,7 +27,7 @@ "canInterrupt": true }, - "world.time < 2400": { + "q.time_of_day < 0.5": { // Walk to fishing spot then play animation forever "type": "sequence", "children": [ diff --git a/modules/entity/src/test/java/unnamed/mmo/mql/MqlTest.java b/modules/entity/src/test/java/unnamed/mmo/mql/MqlTest.java index e56fcbfa..372be604 100644 --- a/modules/entity/src/test/java/unnamed/mmo/mql/MqlTest.java +++ b/modules/entity/src/test/java/unnamed/mmo/mql/MqlTest.java @@ -9,6 +9,7 @@ import unnamed.mmo.mql.value.MqlNumberValue; import unnamed.mmo.mql.value.MqlValue; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class MqlTest { @@ -29,10 +30,9 @@ public void basicQueryCall() { }; } }; - var result = expr.evaluateToBool(scope); - - assertTrue(result); - + var result = expr.evaluate(scope); + assertTrue(result instanceof MqlNumberValue); + assertEquals(1, ((MqlNumberValue) result).value()); } } From a6bd6bfcb806ab3ab366796994216cfa5b0b3c1f Mon Sep 17 00:00:00 2001 From: mworzala Date: Thu, 1 Sep 2022 23:52:10 +0300 Subject: [PATCH 10/25] Rework selector task to use mql script, add the concept of stimuli but currently hardcoded --- .../java/net/hollowcube/server/dev/Main.java | 47 +++-- .../mmo/entity/EntityMqlQueryContext.java | 26 +++ .../unnamed/mmo/entity/UnnamedEntity.java | 3 +- .../stimuli/NearbyEntityStimuliSource.java | 37 ++++ .../entity/brain/stimuli/StimuliSource.java | 13 ++ .../entity/brain/task/FollowTargetTask.java | 10 ++ .../mmo/entity/brain/task/SelectorTask.java | 161 ++++++++++-------- .../unnamed/mmo/entity/brain/task/Task.java | 1 + .../entity/brain/task/WanderInRegionTask.java | 2 +- .../main/java/unnamed/mmo/mql/MqlScript.java | 3 + .../src/main/java/unnamed/mmo/mql/README.md | 14 +- .../java/unnamed/mmo/mql/value/MqlValue.java | 8 + 12 files changed, 230 insertions(+), 95 deletions(-) create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/EntityMqlQueryContext.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/NearbyEntityStimuliSource.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/StimuliSource.java diff --git a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java index 3bfaf979..661c86db 100644 --- a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java +++ b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java @@ -5,6 +5,9 @@ import net.hollowcube.player.PlayerImpl; import net.hollowcube.server.dev.tool.DebugToolManager; import net.hollowcube.server.instance.TickTrackingInstance; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerProcess; import net.minestom.server.command.builder.Command; @@ -36,21 +39,22 @@ import unnamed.mmo.chat.ChatManager; import unnamed.mmo.chat.storage.ChatStorage; import unnamed.mmo.command.BaseCommandRegister; +import unnamed.mmo.damage.DamageProcessor; +import unnamed.mmo.data.number.NumberProvider; import unnamed.mmo.entity.UnnamedEntity; -import unnamed.mmo.entity.brain.SingleTaskBrain; -import unnamed.mmo.entity.brain.task.FollowTargetTask; -import unnamed.mmo.entity.brain.task.SelectorTask; -import unnamed.mmo.entity.brain.task.WanderInRegionTask; +import unnamed.mmo.entity.brain.task.*; import unnamed.mmo.item.Item; import unnamed.mmo.damage.DamageProcessor; import unnamed.mmo.item.Item; import unnamed.mmo.item.ItemManager; +import unnamed.mmo.mql.MqlScript; import unnamed.mmo.item.entity.OwnedItemEntity; import unnamed.mmo.player.PlayerImpl; import unnamed.mmo.quest.QuestFacet; import unnamed.mmo.server.dev.tool.DebugToolManager; import unnamed.mmo.server.instance.TickTrackingInstance; +import java.util.List; import java.util.HashMap; import java.util.Map; import java.util.ServiceLoader; @@ -100,17 +104,34 @@ public static void main(String[] args) { //todo test entity - UnnamedEntity entity = new UnnamedEntity(new SelectorTask(new SelectorTask.Spec( - new FollowTargetTask.Spec(), - new WanderInRegionTask.Spec() - ))); + JsonElement json = JsonParser.parseString(""" + { + "type": "unnamed:selector", + "children": { + "q.has_target": { + "type": "unnamed:follow_target" + }, + "": { + "type": "unnamed:sequence", + "children": [ + { + "type": "unnamed:wander_in_region" + }, + { + "type": "unnamed:idle", + "time": 5 + } + ], + + "canInterrupt": true + } + } + }"""); + Task task = JsonOps.INSTANCE.withDecoder(SelectorTask.Spec.CODEC) + .apply(json).getOrThrow(false, System.err::println).getFirst().create(); + UnnamedEntity entity = new UnnamedEntity(task); entity.setInstance(instance, new Pos(0, 40, 0)) .thenAccept(unused -> System.out.println("Spawned")); - - MinecraftServer.getSchedulerManager().buildTask(() -> { - System.out.println("SETTING TARGET"); - ((SingleTaskBrain) entity.brain()).setTarget(player); - }).delay(5, net.minestom.server.utils.time.TimeUnit.SECOND).schedule(); }); BaseCommandRegister.registerCommands(); //todo this should be in a facet? diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/EntityMqlQueryContext.java b/modules/entity/src/main/java/unnamed/mmo/entity/EntityMqlQueryContext.java new file mode 100644 index 00000000..d83855d1 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/EntityMqlQueryContext.java @@ -0,0 +1,26 @@ +package unnamed.mmo.entity; + +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.LivingEntity; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.mql.runtime.MqlRuntimeError; +import unnamed.mmo.mql.runtime.MqlScope; +import unnamed.mmo.mql.value.MqlHolder; +import unnamed.mmo.mql.value.MqlValue; + +//todo this whole class sucks, need to work out mql querying better +public record EntityMqlQueryContext(@NotNull UnnamedEntity entity) implements MqlScope { + @Override + public @NotNull MqlValue get(@NotNull String name) { + if (!name.equals("q") && !name.equals("query")) + throw new MqlRuntimeError("unknown environment object: " + name); + return (MqlHolder) queryFunction -> switch (queryFunction) { + // Entity + case "is_alive" -> MqlValue.from(!entity.isDead()); + case "has_target" -> MqlValue.from(entity.brain().getTarget() != null); + // World + case "time_of_day" -> MqlValue.from(entity.getInstance().getTime() / 18_000D); + default -> throw new MqlRuntimeError("no such query function: " + queryFunction); + }; + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java b/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java index a06e8cc9..c07ff534 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java @@ -3,6 +3,7 @@ import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.LivingEntity; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import unnamed.mmo.entity.brain.Brain; @@ -12,7 +13,7 @@ import java.util.concurrent.CompletableFuture; -public class UnnamedEntity extends Entity { +public class UnnamedEntity extends LivingEntity { private final Brain brain; public UnnamedEntity(Task task) { diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/NearbyEntityStimuliSource.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/NearbyEntityStimuliSource.java new file mode 100644 index 00000000..e55b85bf --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/NearbyEntityStimuliSource.java @@ -0,0 +1,37 @@ +package unnamed.mmo.entity.brain.stimuli; + +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.EntityTracker; +import net.minestom.server.instance.Instance; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.brain.Brain; +import unnamed.mmo.entity.brain.SingleTaskBrain; + +import java.util.ArrayList; +import java.util.List; + +public class NearbyEntityStimuliSource implements StimuliSource { + + @Override + public void update(@NotNull Brain brain) { + final Entity entity = brain.entity(); + final Instance instance = entity.getInstance(); + + List nearby = new ArrayList<>(); + instance.getEntityTracker().nearbyEntities(entity.getPosition(), 5, EntityTracker.Target.PLAYERS, nearby::add); + + double minDistance = Double.MAX_VALUE; + Player closest = null; + for (Player player : nearby) { + double distance = player.getDistanceSquared(entity); + if (distance < minDistance) { + minDistance = distance; + closest = player; + } + } + + ((SingleTaskBrain) brain).setTarget(closest); + } + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/StimuliSource.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/StimuliSource.java new file mode 100644 index 00000000..cce5e8fa --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/StimuliSource.java @@ -0,0 +1,13 @@ +package unnamed.mmo.entity.brain.stimuli; + +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.brain.Brain; + +public interface StimuliSource { + + void update(@NotNull Brain brain); + + + + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java index 07b6d7dc..654a2a82 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java @@ -1,5 +1,7 @@ package unnamed.mmo.entity.brain.task; +import com.google.auto.service.AutoService; +import com.mojang.serialization.Codec; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; import unnamed.mmo.entity.brain.Brain; @@ -28,10 +30,18 @@ public void tick(@NotNull Brain brain) { public record Spec() implements Task.Spec { + public static final Codec CODEC = Codec.unit(new Spec()); @Override public @NotNull Task create() { return new FollowTargetTask(); } } + + @AutoService(Task.Factory.class) + public static class Factory extends Task.Factory { + public Factory() { + super("unnamed:follow_target", Spec.class, Spec.CODEC); + } + } } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java index 734a8de3..46385978 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java @@ -1,68 +1,109 @@ package unnamed.mmo.entity.brain.task; -import com.google.gson.JsonParser; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; -import com.mojang.serialization.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; -import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import unnamed.mmo.entity.EntityMqlQueryContext; +import unnamed.mmo.entity.UnnamedEntity; import unnamed.mmo.entity.brain.Brain; +import unnamed.mmo.entity.brain.stimuli.NearbyEntityStimuliSource; +import unnamed.mmo.entity.brain.stimuli.StimuliSource; import unnamed.mmo.mql.MqlScript; -import unnamed.mmo.util.DFUUtil; +import java.util.ArrayList; import java.util.List; -import java.util.function.Function; import static unnamed.mmo.util.ExtraCodecs.lazy; public class SelectorTask extends AbstractTask { + private static final Logger LOGGER = LoggerFactory.getLogger(SelectorTask.class); private final Spec spec; -// private final Task targetTask; -// private final Task otherwiseTask; + private final List children = new ArrayList<>(); - private Task activeTask; + private EntityMqlQueryContext queryContext; + private int activeTask = -1; + + //todo temp + private final StimuliSource stimuli = new NearbyEntityStimuliSource(); public SelectorTask(Spec spec) { this.spec = spec; -// this.targetTask = spec.target().create(); -// this.otherwiseTask = spec.otherwise().create(); + for (var child : spec.children) { + children.add(child.create()); + } } + @Override - public void tick(@NotNull Brain brain) { -// final Entity target = brain.getTarget(); -// // Check if there is a value, and we are not currently running the value task. -// if (target != null && activeTask != targetTask) { -// selectAndStart(brain, targetTask); -// return; -// } -// if (target == null && activeTask != otherwiseTask) { -// selectAndStart(brain, otherwiseTask); -// return; -// } -// -// // Restart a task if it has finished -// if (activeTask.getState() != State.RUNNING) { -// activeTask.start(brain); -// } -// -// activeTask.tick(brain); + public void start(@NotNull Brain brain) { + super.start(brain); + this.queryContext = new EntityMqlQueryContext((UnnamedEntity) brain.entity()); + + evaluate(brain); } - private void selectAndStart(Brain brain, Task task) { - // End the active task - if (activeTask != null) { - //todo bad - ((AbstractTask) activeTask).end(true); + @Override + public void tick(@NotNull Brain brain) { + stimuli.update(brain); //todo not sure these should update every tick? + + // Try to tick the current task, if present + if (activeTask != -1) { + Task active = children.get(activeTask); + active.tick(brain); + + // Check if task is complete + switch (active.getState()) { + case FAILED -> end(false); + case COMPLETE -> { + // Current task finished with success, select a new task. + activeTask = -1; // Reset the active task so evaluate restarts it if relevant + evaluate(brain); + return; + } + // Otherwise do nothing and continue as normal + } + + // If the current task cannot be interrupted, do nothing else + Descriptor desc = spec.children.get(activeTask); + if (!desc.canInterrupt()) return; } - // start the other task - activeTask = task; - activeTask.start(brain); + // Attempt to choose a new task. + //todo do not test for change every tick in the future + evaluate(brain); } + /** Evaluate each task in order, choosing the first matching one. */ + private void evaluate(@NotNull Brain brain) { + for (int i = 0; i < children.size(); i++) { + Descriptor child = spec.children.get(i); + // Try to evaluate this child + if (!child.script.evaluateToBool(queryContext)) + continue; + + // If the selected task is also the current task, do nothing + if (activeTask == i) return; + + // Cancel the old task + //todo should do this better + brain.navigator().setPathTo(null); + + // Change task and start the new one + LOGGER.info("starting new task: {}", i); + activeTask = i; + Task newTask = children.get(i); + newTask.start(brain); + return; + } + } + + + + public record Descriptor( MqlScript script, Task.Spec task, @@ -78,17 +119,21 @@ public record Spec( //todo stimuli List children ) implements Task.Spec { - public static final Codec> abcx = Codec.pair( + private static final Codec> TASK_MIXIN = Codec.pair( lazy(() -> Task.Spec.CODEC), RecordCodecBuilder.create(i -> i.group( - Codec.BOOL.fieldOf("canInterrupt").forGetter(b -> b) + Codec.BOOL.optionalFieldOf("canInterrupt", false).forGetter(b -> b) ).apply(i, b -> b)) ); -// public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( -// Codec.unboundedMap(MqlScript.CODEC, Codec.pair(lazy(() -> Task.Spec.CODEC), Codec.BOOL.fieldOf("").forGetter(it -> false))) -// .xmap(DFUUtil::mapToPairList, DFUUtil::pairListToMap) -// .fieldOf("children").forGetter(Spec::children) -// ).apply(i, Spec::new)); + + private static final Codec> DESCRIPTOR_LIST = Codec.unboundedMap(MqlScript.CODEC, TASK_MIXIN) + .xmap(m -> m.entrySet().stream().map(entry -> new Descriptor(entry.getKey(), entry.getValue().getFirst(), entry.getValue().getSecond())).toList(), + d -> {throw new RuntimeException("not implemented");}); + + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + DESCRIPTOR_LIST.fieldOf("children").forGetter(Spec::children) + ).apply(i, Spec::new)); + @Override public @NotNull Task create() { @@ -96,36 +141,6 @@ public record Spec( } } - public static void main(String[] args) { -// var result = JsonOps.INSTANCE.withDecoder(Spec.CODEC) -// .apply(JsonParser.parseString(""" -// { -// "children": { -// "1.0": "abc", -// "q.is_alive": "def", -// "3.0": "ghi" -// } -// }""")) -// .result() -// .get() -// .getFirst(); - var result2 = JsonOps.INSTANCE.withDecoder(Spec.abcx) - .apply(JsonParser.parseString(""" - { - "type": "unnamed:idle", - "time": 1, - "canInterrupt": false - }""")) - .result() - .get() - .getFirst(); - - System.out.println(result2); - -// for (var entry : result.children) { -// System.out.println(entry.getSecond()); -// } - } } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java index 937980f8..2218d77a 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java @@ -29,6 +29,7 @@ class Factory extends ResourceFactory { static final Registry.Index, Factory> TYPE_REGISTRY = REGISTRY.index(Factory::type); @SuppressWarnings("Convert2MethodRef") + //todo turn into Registry#required when updated public static final Codec CODEC = Codec.STRING.xmap(ns -> REGISTRY.get(ns), Factory::name); public Factory(String namespace, Class type, Codec codec) { diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java index f3c78cc2..28d579cf 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java @@ -48,7 +48,7 @@ public record Spec() implements Task.Spec { } } - @AutoService(WanderInRegionTask.class) + @AutoService(Task.Factory.class) public static class Factory extends Task.Factory { public Factory() { super("unnamed:wander_in_region", Spec.class, Spec.CODEC); diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/MqlScript.java b/modules/entity/src/main/java/unnamed/mmo/mql/MqlScript.java index ef5e83d4..b1e4b902 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/MqlScript.java +++ b/modules/entity/src/main/java/unnamed/mmo/mql/MqlScript.java @@ -6,6 +6,7 @@ import unnamed.mmo.mql.parser.MqlParser; import unnamed.mmo.mql.runtime.MqlScope; import unnamed.mmo.mql.tree.MqlExpr; +import unnamed.mmo.mql.tree.MqlNumberExpr; import unnamed.mmo.mql.value.MqlNumberValue; import unnamed.mmo.mql.value.MqlValue; import unnamed.mmo.util.DFUUtil; @@ -18,6 +19,8 @@ public record MqlScript(@NotNull MqlExpr expr) { .xmap(MqlScript::parse, unused -> {throw new RuntimeException("cannot serialize an mql script");}); public static @NotNull MqlScript parse(@NotNull String script) { + if (script.trim().isEmpty()) // Empty script is always the number 1 + return new MqlScript(new MqlNumberExpr(1)); MqlExpr expr = new MqlParser(script).parse(); return new MqlScript(expr); } diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/README.md b/modules/entity/src/main/java/unnamed/mmo/mql/README.md index 1b5e1dc0..91858221 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/README.md +++ b/modules/entity/src/main/java/unnamed/mmo/mql/README.md @@ -14,10 +14,10 @@ but may eventually be refactored to something more performant in the future. `mql` implements a subset of the query functions in MoLang. They are described below. -| Function | Status | Description | -|---------------|-----------------|------------------------------------------------------------------------------------------------------------| -| q.time_of_day | NOT IMPLEMENTED | Gets the current time of day in the world as a decimal: midnight=0.0, sunrise=0.25, noon=0.5, sunset=0.75. | -| q.has_target | NOT IMPLEMENTED | Returns true if the entity has a target, false otherwise. | -| | | | -| | | | -| | | | +| Function | Status | Description | +|---------------|--------|------------------------------------------------------------------------------------------------------------| +| q.time_of_day | + | Gets the current time of day in the world as a decimal: midnight=0.0, sunrise=0.25, noon=0.5, sunset=0.75. | +| q.has_target | + | Returns true if the entity has a target, false otherwise. | +| q.is_alive | + | Returns true if the entity is alive, false otherwise | +| | | | +| | | | diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlValue.java b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlValue.java index eeb59ab6..f4eb7c8f 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlValue.java +++ b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlValue.java @@ -9,6 +9,14 @@ public sealed interface MqlValue permits MqlCallable, MqlHolder, MqlIdentValue, MqlNull, MqlNumberValue { MqlValue NULL = MqlNull.INSTANCE; + static @NotNull MqlValue from(boolean bool) { + return new MqlNumberValue(bool ? 1 : 0); + } + + static @NotNull MqlValue from(double dbl) { + return new MqlNumberValue(dbl); + } + default Target cast(@NotNull Class targetType) { if (targetType.isInstance(this)) //noinspection unchecked From 89867db56373ba58455401381b36ad9c68909e7e Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 4 Sep 2022 15:50:41 -0400 Subject: [PATCH 11/25] attack testing in FollowTargetTask, factor in path optimizer and iams version --- .../unnamed/mmo/entity/UnnamedEntity.java | 9 ++- .../mmo/entity/brain/SingleTaskBrain.java | 2 +- .../entity/brain/task/FollowTargetTask.java | 16 +++- .../mmo/entity/brain/task/IdleTask.java | 2 +- .../mmo/entity/brain/task/SelectorTask.java | 4 +- .../mmo/entity/brain/task/SequenceTask.java | 4 +- .../unnamed/mmo/entity/brain/task/Task.java | 2 +- .../entity/brain/task/WanderInRegionTask.java | 2 +- .../mmo/entity/pathfinding/PFNavigator.java | 4 +- .../mmo/entity/pathfinding/PFPath.java | 9 ++- .../entity/pathfinding/PFPathGenerator.java | 35 +-------- .../entity/pathfinding/PFPathOptimizer.java | 73 +++++++++++++++++++ 12 files changed, 117 insertions(+), 45 deletions(-) create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathOptimizer.java diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java b/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java index c07ff534..54ccacef 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java @@ -4,12 +4,13 @@ import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; import net.minestom.server.entity.LivingEntity; +import net.minestom.server.event.EventDispatcher; +import net.minestom.server.event.entity.EntityAttackEvent; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import unnamed.mmo.entity.brain.Brain; import unnamed.mmo.entity.brain.SingleTaskBrain; import unnamed.mmo.entity.brain.task.Task; -import unnamed.mmo.entity.brain.task.WanderInRegionTask; import java.util.concurrent.CompletableFuture; @@ -41,4 +42,10 @@ public void update(long time) { public @NotNull Brain brain() { return brain; } + + public void attack(@NotNull Entity target) { + swingMainHand(); + EntityAttackEvent attackEvent = new EntityAttackEvent(this, target); + EventDispatcher.call(attackEvent); + } } \ No newline at end of file diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java index be8761b0..8d790012 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java @@ -59,7 +59,7 @@ public void tick(long time) { navigator.tick(time); switch (task.getState()) { case INIT, COMPLETE -> task.start(this); - case RUNNING -> task.tick(this); + case RUNNING -> task.tick(this, time); case FAILED -> { if (!failed) System.out.println("Failed"); diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java index 654a2a82..f7f590d5 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java @@ -3,20 +3,28 @@ import com.google.auto.service.AutoService; import com.mojang.serialization.Codec; import net.minestom.server.entity.Entity; +import net.minestom.server.utils.time.Cooldown; +import net.minestom.server.utils.time.TimeUnit; import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.UnnamedEntity; import unnamed.mmo.entity.brain.Brain; +import java.time.Duration; + public class FollowTargetTask extends AbstractTask { private int tick = 0; + private Cooldown attackCooldown = new Cooldown(Duration.of(1, TimeUnit.SECOND)); + @Override public void start(@NotNull Brain brain) { super.start(brain); } @Override - public void tick(@NotNull Brain brain) { + public void tick(@NotNull Brain brain, long time) { + final Entity target = brain.getTarget(); if (target == null) { end(true); @@ -26,6 +34,12 @@ public void tick(@NotNull Brain brain) { if (tick++ % 5 == 0) { brain.setPathTo(target.getPosition()); } + + double distance = brain.entity().getDistanceSquared(target); + if (distance < 4 && attackCooldown.isReady(time)) { + attackCooldown.refreshLastUpdate(time); + ((UnnamedEntity) brain.entity()).attack(target); + } } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/IdleTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/IdleTask.java index 8ec41028..acbbc986 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/IdleTask.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/IdleTask.java @@ -25,7 +25,7 @@ public void start(@NotNull Brain brain) { } @Override - public void tick(@NotNull Brain brain) { + public void tick(@NotNull Brain brain, long time) { sleepTime -= 1; if (sleepTime < 1) { end(true); diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java index 46385978..fa43fe98 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java @@ -47,13 +47,13 @@ public void start(@NotNull Brain brain) { } @Override - public void tick(@NotNull Brain brain) { + public void tick(@NotNull Brain brain, long time) { stimuli.update(brain); //todo not sure these should update every tick? // Try to tick the current task, if present if (activeTask != -1) { Task active = children.get(activeTask); - active.tick(brain); + active.tick(brain, time); // Check if task is complete switch (active.getState()) { diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java index 7a262b91..99db0789 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java @@ -38,9 +38,9 @@ public void start(@NotNull Brain brain) { } @Override - public void tick(@NotNull Brain brain) { + public void tick(@NotNull Brain brain, long time) { final Task current = current(); - current.tick(brain); + current.tick(brain, time); // Do nothing if current task is still running if (current.getState() == State.RUNNING) return; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java index 2218d77a..b9813d56 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java @@ -12,7 +12,7 @@ public sealed interface Task permits AbstractTask { void start(@NotNull Brain brain); - void tick(@NotNull Brain brain); + void tick(@NotNull Brain brain, long time); enum State { INIT, RUNNING, COMPLETE, FAILED diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java index 28d579cf..47dff860 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java @@ -33,7 +33,7 @@ public void start(@NotNull Brain brain) { } @Override - public void tick(@NotNull Brain brain) { + public void tick(@NotNull Brain brain, long time) { if (brain.navigator().isActive()) return; end(true); } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java index 42d86865..ec5b8447 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java @@ -121,7 +121,9 @@ public synchronized boolean setPathTo(@Nullable Point point, double minimumDista point, 100, this.entity.getBoundingBox().depth() * 2, - this.entity.getBoundingBox()); + this.entity.getBoundingBox(), + PFPathOptimizer.NOOP + ); final boolean success = path != null; this.goalPosition = success ? point : null; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPath.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPath.java index 41b2d439..9adc1cda 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPath.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPath.java @@ -1,12 +1,19 @@ package unnamed.mmo.entity.pathfinding; +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.collision.CollisionUtils; +import net.minestom.server.collision.PhysicsResult; import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.Instance; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.List; public class PFPath { - final List nodes; + List nodes; public int index = 0; public PFPath(List nodes) { diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java index 132bbc70..ed8a545a 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java @@ -22,7 +22,7 @@ private static Collection getNeighborPositions(Point point) { } static Comparator pNodeComparator = (a, b) -> Float.compare(a.cost(), b.cost()); - public static PFPath generate(Instance instance, Point orgStart, Point orgTarget, double maxDistance, double closeDistance, BoundingBox boundingBox) { + public static PFPath generate(Instance instance, Point orgStart, Point orgTarget, double maxDistance, double closeDistance, BoundingBox boundingBox, PFPathOptimizer optimizer) { Point start = PFNode.gravitySnap(instance, orgStart); Point target = PFNode.gravitySnap(instance, orgTarget); if (start == null || target == null) return null; @@ -81,38 +81,7 @@ public static PFPath generate(Instance instance, Point orgStart, Point orgTarget result.add(0, current.point()); } -// Collections.reverse(result); -// if (open.isEmpty()) return null; -// -// PNode current = open.poll(); -// -// if (current.point.distance(value) > closeDistance) return null; -// -// while (current.parent != null) { -// path.getNodes().add(current); -// current = current.parent; -// } -// -// Collections.reverse(path.getNodes()); -// path.reduceNodes(instance, expandedBoundingBox); -// path.addNodes(instance, expandedBoundingBox); - -// if (path.getNodes().size() > 0) { -// PNode pEnd = new PNode(value, 0, 0, path.getNodes().get(path.getNodes().size() - 1)); -// path.getNodes().add(pEnd); -// } -// -// return path; - return new PFPath(result); + return optimizer.optimize(new PFPath(result), instance, expandedBoundingBox); } - public static void main(String[] args) { - Queue queue = new PriorityQueue<>(pNodeComparator); - PFNode a = new PFNode(Vec.ZERO, 2); - queue.offer(a); - queue.offer(a); - queue.remove(a); - - PFPathGenerator.generate(null, new Vec(0, 0, 0), new Vec(5, 0, 0), 10, 0.8, null); - } } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathOptimizer.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathOptimizer.java new file mode 100644 index 00000000..40196c29 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathOptimizer.java @@ -0,0 +1,73 @@ +package unnamed.mmo.entity.pathfinding; + +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.collision.CollisionUtils; +import net.minestom.server.collision.PhysicsResult; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.Instance; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +@FunctionalInterface +public interface PFPathOptimizer { + + @NotNull PFPath optimize(@NotNull PFPath path, @NotNull Instance instance, @NotNull BoundingBox bb); + + PFPathOptimizer NOOP = (path, instance, bb) -> path; + + PFPathOptimizer IAM = (path, instance, bb) -> { + List newNodes = new ArrayList<>(); + + { + var nodes = path.nodes; + int current = 0; + int next = 1; + + for (int i = 0; i < nodes.size() - 1; i++) { + var currentNode = nodes.get(current); + var nextNode = nodes.get(next); + var diff = nextNode.sub(currentNode); + PhysicsResult res = CollisionUtils.handlePhysics(instance, instance.getChunkAt(currentNode), bb, Pos.fromPoint(currentNode), Vec.fromPoint(diff), null); + if (res.collisionX() || res.collisionY() || res.collisionZ()) { + newNodes.add(nodes.get(next - 1)); + newNodes.add(nodes.get(next)); + current = next; + } + next++; + } + if (nodes.size() > 0) + newNodes.add(nodes.get(nodes.size() - 1)); + + path = new PFPath(newNodes); + } + + { + var nodes = path.nodes; + newNodes = new ArrayList<>(); + for (int current = 0; current < nodes.size() - 1; current++) { + var currentNode = nodes.get(current); + var nextNode = nodes.get(current + 1); + newNodes.add(currentNode); + Point diff = nextNode.sub(currentNode); + PhysicsResult res = CollisionUtils.handlePhysics(instance, instance.getChunkAt(currentNode), bb, Pos.fromPoint(currentNode), Vec.fromPoint(diff), null); + if (res.collisionX() || res.collisionY() || res.collisionZ()) { + Point end = res.newPosition(); + if (nextNode.distance(end) > 0.1) { + newNodes.add(end); + } + } + } + if (nodes.size() > 0) + newNodes.add(nodes.get(nodes.size() - 1)); + + path = new PFPath(nodes); + } + + return path; + }; + +} From 5f499e0f7287dc96ea01606095a1e859ccd63319 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 4 Sep 2022 18:07:30 -0400 Subject: [PATCH 12/25] base interface for new PF, first version of water path generator --- .../unnamed/mmo/entity/motion/Navigator.java | 8 ++ .../mmo/entity/motion/PathGenerator.java | 48 ++++++++++ .../unnamed/mmo/entity/motion/Pathfinder.java | 8 ++ .../mmo/entity/motion/util/PhysicsUtil.java | 79 +++++++++++++++++ .../mmo/entity/brain/task/TestIdleTask.java | 2 +- .../entity/brain/task/TestSequenceTask.java | 6 +- .../mmo/entity/brain/task/test/MockTask.java | 2 +- .../entity/motion/TestPathGeneratorLand.java | 23 +++++ .../entity/motion/TestPathGeneratorWater.java | 87 +++++++++++++++++++ .../entity/motion/util/TestPhysicsUtil.java | 81 +++++++++++++++++ .../unnamed/mmo/test/MockBlockGetter.java | 51 +++++++++++ 11 files changed, 390 insertions(+), 5 deletions(-) create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/motion/Navigator.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorLand.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorWater.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/motion/util/TestPhysicsUtil.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/test/MockBlockGetter.java diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Navigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/Navigator.java new file mode 100644 index 00000000..944cf6bf --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/Navigator.java @@ -0,0 +1,8 @@ +package unnamed.mmo.entity.motion; + +/** + * Consumes paths from a {@link Pathfinder} to navigate an {@link net.minestom.server.entity.Entity} in an instance. + */ +public interface Navigator { + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java new file mode 100644 index 00000000..291b656a --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java @@ -0,0 +1,48 @@ +package unnamed.mmo.entity.motion; + +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.utils.Direction; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.motion.util.PhysicsUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Generates paths to be consumed by a {@link Pathfinder}. + *

    + * Should take into account the entity capabilities, e.g. avoiding danger. + */ +public interface PathGenerator { + + @NotNull Collection generate(@NotNull Block.Getter world, @NotNull Point pos, @NotNull BoundingBox bb); + + + PathGenerator LAND = (world, pos, bb) -> { + + return List.of(); + }; + + PathGenerator WATER = (world, pos, bb) -> { + List neighbors = new ArrayList<>(); + for (Direction direction : Direction.values()) { + var neighbor = pos.add(direction.normalX(), direction.normalY(), direction.normalZ()); + // Ensure the block is water, otherwise we cannot move to it + if (world.getBlock(neighbor, Block.Getter.Condition.TYPE).id() != Block.WATER.id()) continue; + // Ensure the BB fits at that block + if (PhysicsUtil.testCollision(world, neighbor, bb)) continue; + + neighbors.add(neighbor); + } + return neighbors; + }; + + PathGenerator AIR = (world, pos, bb) -> { + + return List.of(); + }; + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java new file mode 100644 index 00000000..b65d0271 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java @@ -0,0 +1,8 @@ +package unnamed.mmo.entity.motion; + +/** + * Consumes nodes from a {@link PathGenerator} to create a path. + */ +public interface Pathfinder { + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java new file mode 100644 index 00000000..cc8fcb26 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java @@ -0,0 +1,79 @@ +package unnamed.mmo.entity.motion.util; + +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * Amalgamation of Minestom physics utility calls and a simpler static bounding box check in a world. + */ +public final class PhysicsUtil { + private PhysicsUtil() {} + + /** + * Simplified check if a bounding box collides with a solid block. + *

    + * Currently, block shapes are ignored. Solid=full cube, empty=nothing + */ + public static boolean testCollision(@NotNull Block.Getter world, @NotNull Point pos, @NotNull BoundingBox bb) { + List blocks = new ArrayList<>(); + for (double x = bb.minX() + pos.x(); x <= bb.maxX() + pos.x(); x++) { + for (double y = bb.minY() + pos.y(); y <= bb.maxY() + pos.y(); y++) { + for (double z = bb.minZ() + pos.z(); z <= bb.maxZ() + pos.z(); z++) { + blocks.add(new Vec(Math.floor(x), Math.floor(y), Math.floor(z))); + } + blocks.add(new Vec(Math.floor(x), Math.floor(y), Math.floor(bb.maxX() + pos.z()))); + } + blocks.add(new Vec(Math.floor(x), Math.floor(bb.maxY() + pos.y()), Math.floor(bb.minZ() + pos.z()))); + } + blocks.add(new Vec(Math.floor(bb.maxX() + pos.x()), Math.floor(bb.maxY() + pos.y()), Math.floor(bb.maxZ() + pos.z()))); + + for (var block : blocks) { + if (world.getBlock(block, Block.Getter.Condition.TYPE).isSolid()) { + return true; + } + } + return false; + +// if (isInvalid) { +// boolean isInvalidUp = false; +// for (var block : blocks) { +// if (world.getBlock(block.add(0, 1, 0), Block.Getter.Condition.TYPE).isSolid()) { +// isInvalidUp = true; +// break; +// } +// } +// +// if (isInvalidUp) return true; +// } + //Collection overlapping = BoundingBoxUtilKt.getBlocks(expandedBoundingBox, point); +// +// boolean isInvalid = false; +// for (Point block : overlapping) { +// if (blockGetter.getBlock(block, Block.Getter.Condition.TYPE).isSolid()) { +// isInvalid = true; +// break; +// } +// } +// +// if (isInvalid) { +// // Check up 1 block +// boolean isInvalidUp = false; +// for (Point block : overlapping) { +// if (blockGetter.getBlock(block.add(0, 1, 0), Block.Getter.Condition.TYPE).isSolid()) { +// isInvalidUp = true; +// break; +// } +// } +// +// if (isInvalidUp) continue; +// point = point.add(0, 1, 0); +// } + } + +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java index 7011af8e..2ce88baa 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java @@ -19,7 +19,7 @@ public void testHappyCase() { task.start(brain); for (int i = 0; i < 5; i++) { assertThat(task.getState()).isEqualTo(Task.State.RUNNING); - task.tick(brain); + task.tick(brain, 0); } assertThat(task.getState()).isEqualTo(Task.State.COMPLETE); } diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java index 01b1e0c5..7f5504b4 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java @@ -31,7 +31,7 @@ public void testSingleTaskSuccess() { var task = new SequenceTask(spec); task.start(brain); - task.tick(brain); + task.tick(brain, System.currentTimeMillis()); // Should have passed and mock1 should also have been run (passed) assertThat(mock1).isComplete(); @@ -47,10 +47,10 @@ public void testMultiTaskSuccess() { var task = new SequenceTask(spec); task.start(brain); - task.tick(brain); + task.tick(brain, System.currentTimeMillis()); assertThat(mock1).isComplete(); - task.tick(brain); + task.tick(brain, System.currentTimeMillis()); assertThat(mock2).isComplete(); assertThat(task).isComplete(); diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java index 8bb6e7fa..e13b2151 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java @@ -13,7 +13,7 @@ public MockTask(Boolean pass) { } @Override - public void tick(@NotNull Brain brain) { + public void tick(@NotNull Brain brain, long time) { if (pass != null) end(pass); } diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorLand.java b/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorLand.java new file mode 100644 index 00000000..f9bde8e0 --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorLand.java @@ -0,0 +1,23 @@ +package unnamed.mmo.entity.motion; + +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.block.Block; +import org.junit.jupiter.api.Test; +import unnamed.mmo.test.MockBlockGetter; + +import static com.google.common.truth.Truth.assertThat; + +public class TestPathGeneratorLand { + + @Test + public void testSingleWaterBlock() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.block(0, 0, 0, Block.WATER); + var start = new Vec(0, 0, 0); + + var result = PathGenerator.WATER.generate(world, start, bb); + assertThat(result).isEmpty(); + } + +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorWater.java b/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorWater.java new file mode 100644 index 00000000..98653489 --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorWater.java @@ -0,0 +1,87 @@ +package unnamed.mmo.entity.motion; + +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.block.Block; +import org.junit.jupiter.api.Test; +import unnamed.mmo.test.MockBlockGetter; + +import static com.google.common.truth.Truth.assertThat; + +public class TestPathGeneratorWater { + + @Test + public void testSingleWaterBlock() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.block(0, 0, 0, Block.WATER); + var start = new Vec(0, 0, 0); + + var result = PathGenerator.WATER.generate(world, start, bb); + assertThat(result).isEmpty(); + } + + @Test + public void testSingleNeighbor() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.range( + 0, 0, 0, + 1, 0, 0, + Block.WATER + ); + var start = new Vec(0, 0, 0); + + var result = PathGenerator.WATER.generate(world, start, bb); + assertThat(result).containsExactly(new Vec(1, 0, 0)); + } + + @Test + public void testAllNeighbors() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.range( + -1, -1, -1, + 1, 1, 1, + Block.WATER + ); + var start = new Vec(0, 0, 0); + + var result = PathGenerator.WATER.generate(world, start, bb); + assertThat(result).containsExactly( + new Vec(-1, 0, 0), + new Vec(1, 0, 0), + new Vec(0, -1, 0), + new Vec(0, 1, 0), + new Vec(0, 0, -1), + new Vec(0, 0, 1) + ); + } + + @Test + public void testSingleNeighbor2() { + var bb = new BoundingBox(0.5, 0.5, 0.5); + var world = MockBlockGetter.range( + -2, -2, -2, + 2, 2, 2, + Block.STONE + ).set(0, 0, 0, Block.WATER).set(1, 0, 0, Block.WATER); + var start = new Vec(0.5, 0, 0.5); + + var result = PathGenerator.WATER.generate(world, start, bb); + assertThat(result).containsExactly(new Vec(1.5, 0, 0.5)); + } + + @Test + public void testSingleNeighborBigBB() { + // Single neighbor surrounded by stone, but the BB is too big to fit + var bb = new BoundingBox(1.5, 1.5, 1.5); + var world = MockBlockGetter.range( + -2, -2, -2, + 2, 2, 2, + Block.STONE + ).set(0, 0, 0, Block.WATER).set(1, 0, 0, Block.WATER); + var start = new Vec(0.5, 0, 0.5); + + var result = PathGenerator.WATER.generate(world, start, bb); + assertThat(result).isEmpty(); + } + +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/motion/util/TestPhysicsUtil.java b/modules/entity/src/test/java/unnamed/mmo/entity/motion/util/TestPhysicsUtil.java new file mode 100644 index 00000000..616cdafb --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/motion/util/TestPhysicsUtil.java @@ -0,0 +1,81 @@ +package unnamed.mmo.entity.motion.util; + +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.block.Block; +import org.junit.jupiter.api.Test; +import unnamed.mmo.test.MockBlockGetter; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestPhysicsUtil { + + @Test + public void testEmpty() { + var bb = new BoundingBox(100, 100, 100); + var world = MockBlockGetter.empty(); + var pos = new Vec(0, 0, 0); + + var result = PhysicsUtil.testCollision(world, pos, bb); + + assertFalse(result); + } + + @Test + public void testSmallInsideBlock() { + var bb = new BoundingBox(0.5, 0.5, 0.5); + var world = MockBlockGetter.block(0, 0, 0, Block.STONE); + var pos = new Vec(0, 0, 0); + + var result = PhysicsUtil.testCollision(world, pos, bb); + + assertTrue(result); + } + + @Test + public void testBetweenBlocks() { + var bb = new BoundingBox(0.6, 1.95, 0.6); + var world = MockBlockGetter.block(1, 1, 0, Block.STONE) + .set(-1, 1, 0, Block.STONE); + var pos = new Vec(0, 0, 0); + + var result = PhysicsUtil.testCollision(world, pos, bb); + + assertTrue(result); + } + + @Test + public void testUnderBlock() { + var bb = new BoundingBox(0.6, 1.95, 0.6); + var world = MockBlockGetter.block(0, 1, 0, Block.STONE); + var pos = new Vec(0.5, 0, 0.5); + + var result = PhysicsUtil.testCollision(world, pos, bb); + + assertTrue(result); + } + + @Test + public void testUnderBlock2() { + var bb = new BoundingBox(0.6, 1.95, 0.6); + var world = MockBlockGetter.block(0, 2, 0, Block.STONE); + var pos = new Vec(0.5, 0, 0.5); + + var result = PhysicsUtil.testCollision(world, pos, bb); + + assertFalse(result); + } + + @Test + public void testWaterNoCollision() { + var bb = new BoundingBox(0.6, 0.6, 0.6); + var world = MockBlockGetter.range(-1, -1, -1, 1, 1, 1, Block.WATER); + var pos = new Vec(0.5, 0, 0.5); + + var result = PhysicsUtil.testCollision(world, pos, bb); + + assertFalse(result); + } + +} diff --git a/modules/entity/src/test/java/unnamed/mmo/test/MockBlockGetter.java b/modules/entity/src/test/java/unnamed/mmo/test/MockBlockGetter.java new file mode 100644 index 00000000..17c1787a --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/test/MockBlockGetter.java @@ -0,0 +1,51 @@ +package unnamed.mmo.test; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnknownNullability; + +import java.util.HashMap; +import java.util.Map; + +public class MockBlockGetter implements Block.Getter { + private final Map data; + + public static MockBlockGetter empty() { + return new MockBlockGetter(new HashMap<>()); + } + + public static MockBlockGetter block(int x, int y, int z, Block block) { + Map data = new HashMap<>(); + data.put(new Vec(x, y, z), block); + return new MockBlockGetter(data); + } + + public static MockBlockGetter range(int startX, int startY, int startZ, int endX, int endY, int endZ, Block block) { + Map data = new HashMap<>(); + for (int x = startX; x <= endX; x++) { + for (int y = startY; y <= endY; y++) { + for (int z = startZ; z <= endZ; z++) { + data.put(new Vec(x, y, z), block); + } + } + } + return new MockBlockGetter(data); + } + + public MockBlockGetter(Map data) { + this.data = data; + } + + @Override + public @UnknownNullability Block getBlock(int x, int y, int z, @NotNull Condition condition) { + return data.getOrDefault(new Vec(x, y, z), Block.AIR); + } + + public MockBlockGetter set(int x, int y, int z, Block block) { + data.put(new Vec(x, y, z), block); + return this; + } + +} From 17f69355f7c1ba35ff25f041d8e508934c7172b2 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 4 Sep 2022 19:41:29 -0400 Subject: [PATCH 13/25] first version of land path generator --- .../mmo/entity/motion/PathGenerator.java | 22 ++++- .../entity/motion/TestPathGeneratorLand.java | 81 ++++++++++++++++++- .../entity/motion/TestPathGeneratorWater.java | 14 ++-- 3 files changed, 103 insertions(+), 14 deletions(-) diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java index 291b656a..f436248a 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java @@ -2,6 +2,7 @@ import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import net.minestom.server.utils.Direction; import org.jetbrains.annotations.NotNull; @@ -11,6 +12,8 @@ import java.util.Collection; import java.util.List; +import static net.minestom.server.instance.block.Block.Getter.Condition; + /** * Generates paths to be consumed by a {@link Pathfinder}. *

    @@ -22,16 +25,29 @@ public interface PathGenerator { PathGenerator LAND = (world, pos, bb) -> { + pos = new Vec(pos.blockX() + 0.5, pos.blockY(), pos.blockZ() + 0.5); + List neighbors = new ArrayList<>(); + for (Direction direction : Direction.HORIZONTAL) { + for (int y = -1; y <= 1; y++) { + var neighbor = pos.add(direction.normalX(), direction.normalY() + y, direction.normalZ()); + // Block below must be solid, or we cannot move to it + if (!world.getBlock(neighbor.add(0, -1, 0), Condition.TYPE).isSolid()) continue; + // Ensure the BB fits at that block + if (PhysicsUtil.testCollision(world, neighbor, bb)) continue; - return List.of(); + neighbors.add(neighbor); + } + } + return neighbors; }; PathGenerator WATER = (world, pos, bb) -> { + pos = new Vec(pos.blockX() + 0.5, pos.blockY(), pos.blockZ() + 0.5); List neighbors = new ArrayList<>(); for (Direction direction : Direction.values()) { var neighbor = pos.add(direction.normalX(), direction.normalY(), direction.normalZ()); // Ensure the block is water, otherwise we cannot move to it - if (world.getBlock(neighbor, Block.Getter.Condition.TYPE).id() != Block.WATER.id()) continue; + if (world.getBlock(neighbor, Condition.TYPE).id() != Block.WATER.id()) continue; // Ensure the BB fits at that block if (PhysicsUtil.testCollision(world, neighbor, bb)) continue; @@ -41,7 +57,7 @@ public interface PathGenerator { }; PathGenerator AIR = (world, pos, bb) -> { - + //todo return List.of(); }; diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorLand.java b/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorLand.java index f9bde8e0..673ffe5b 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorLand.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorLand.java @@ -11,13 +11,86 @@ public class TestPathGeneratorLand { @Test - public void testSingleWaterBlock() { + public void testEmpty() { var bb = new BoundingBox(0.1, 0.1, 0.1); - var world = MockBlockGetter.block(0, 0, 0, Block.WATER); - var start = new Vec(0, 0, 0); + var world = MockBlockGetter.empty(); + var start = new Vec(0, 1, 0); - var result = PathGenerator.WATER.generate(world, start, bb); + var result = PathGenerator.LAND.generate(world, start, bb); assertThat(result).isEmpty(); } + @Test + public void testSingleNeighbor() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.range( + 0, 0, 0, + 1, 0, 0, + Block.STONE); + var start = new Vec(0, 1, 0); + + var result = PathGenerator.LAND.generate(world, start, bb); + assertThat(result).containsExactly( + new Vec(1.5, 1, 0.5) + ); + } + + @Test + public void testAllNeighborsFlat() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.range( + -1, 0, -1, + 1, 0, 1, + Block.STONE); + var start = new Vec(0, 1, 0); + + var result = PathGenerator.LAND.generate(world, start, bb); + assertThat(result).containsExactly( + new Vec(1.5, 1, 0.5), + new Vec(-0.5, 1, 0.5), + new Vec(0.5, 1, 1.5), + new Vec(0.5, 1, -0.5) + ); + } + + @Test + public void testSingleNeighborBigBB() { + var bb = new BoundingBox(0.6, 1.95, 0.6); + var world = MockBlockGetter.range( + 0, 0, 0, + 1, 0, 0, + Block.STONE) + .set(1, 2, 0, Block.STONE); + var start = new Vec(0, 1, 0); + + var result = PathGenerator.LAND.generate(world, start, bb); + assertThat(result).isEmpty(); + } + + @Test + public void testSingleStepUp() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.empty() + .set(1, 1, 0, Block.STONE); + var start = new Vec(0, 1, 0); + + var result = PathGenerator.LAND.generate(world, start, bb); + assertThat(result).containsExactly( + new Vec(1.5, 2, 0.5) + ); + } + + @Test + public void testSingleStepDown() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.empty() + .set(1, -1, 0, Block.STONE); + var start = new Vec(0, 1, 0); + + var result = PathGenerator.LAND.generate(world, start, bb); + assertThat(result).containsExactly( + new Vec(1.5, 0, 0.5) + ); + } + } diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorWater.java b/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorWater.java index 98653489..f48159a8 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorWater.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorWater.java @@ -31,7 +31,7 @@ public void testSingleNeighbor() { var start = new Vec(0, 0, 0); var result = PathGenerator.WATER.generate(world, start, bb); - assertThat(result).containsExactly(new Vec(1, 0, 0)); + assertThat(result).containsExactly(new Vec(1.5, 0, 0.5)); } @Test @@ -46,12 +46,12 @@ public void testAllNeighbors() { var result = PathGenerator.WATER.generate(world, start, bb); assertThat(result).containsExactly( - new Vec(-1, 0, 0), - new Vec(1, 0, 0), - new Vec(0, -1, 0), - new Vec(0, 1, 0), - new Vec(0, 0, -1), - new Vec(0, 0, 1) + new Vec(-0.5, 0, 0.5), + new Vec(1.5, 0, 0.5), + new Vec(0.5, -1, 0.5), + new Vec(0.5, 1, 0.5), + new Vec(0.5, 0, -0.5), + new Vec(0.5, 0, 1.5) ); } From 581c5af6e9613524b1f523e7f4035d8e9e67a883 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 4 Sep 2022 20:53:24 -0400 Subject: [PATCH 14/25] first a star draft --- .../java/unnamed/mmo/entity/motion/Path.java | 9 ++ .../unnamed/mmo/entity/motion/Pathfinder.java | 92 +++++++++++++ .../mmo/entity/motion/util/PhysicsUtil.java | 27 ++++ .../entity/pathfinding/PFPathGenerator.java | 9 -- .../entity/motion/TestPathfinderAStar.java | 78 +++++++++++ .../entity/motion/util/TestPhysicsUtil.java | 128 +++++++++++------- 6 files changed, 284 insertions(+), 59 deletions(-) create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/motion/Path.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathfinderAStar.java diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Path.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/Path.java new file mode 100644 index 00000000..4425a09b --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/Path.java @@ -0,0 +1,9 @@ +package unnamed.mmo.entity.motion; + +import net.minestom.server.coordinate.Point; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public record Path(@NotNull List nodes) { +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java index b65d0271..46cf4e56 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java @@ -1,8 +1,100 @@ package unnamed.mmo.entity.motion; +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import unnamed.mmo.entity.motion.util.PhysicsUtil; + +import java.util.*; + /** * Consumes nodes from a {@link PathGenerator} to create a path. */ public interface Pathfinder { + @Nullable Path findPath( + @NotNull PathGenerator pathGenerator, + @NotNull Block.Getter world, + @NotNull Point from, + @NotNull Point to, + @NotNull BoundingBox bb + ); + + /** + * Reference implementation of a* + * + * @see wikipedia + */ + Pathfinder A_STAR = (pathGenerator, world, start, goal, bb) -> { + //todo should only snap for land, otherwise it should target directly. perhaps the path generator will + // have to have a method to prep the start and end or something +// final Point start = PhysicsUtil.gravitySnap(world, from); +// final Point goal = PhysicsUtil.gravitySnap(world, to); +// if (start == null || goal == null) return null; + + // Min acceptable distance from the target. + //todo this should be a parameter somewhere i guess + // Also i am not sure this is necessary? could keep this internal and just add the target as the final point? + float minDistance = 0.8f; + + //todo should the bb be expanded? + + //todo maxSize + int maxSize = 100 * 7; + + Map minCost = new HashMap<>(); + minCost.put(start, 0f); + + //todo probably super slot to do a lookup for each of these + Queue open = new PriorityQueue<>((a, b) -> Float.compare(minCost.get(a), minCost.get(b))); + open.add(start); + + Map cameFrom = new HashMap<>(); + + + while (!open.isEmpty()) { + Point current = open.peek(); + if (current.distance(goal) < minDistance) + break; // Path was found + + // Safety stop if we are never actually going to find it + if (cameFrom.size() >= maxSize) { + //todo log + return null; + } + + open.poll(); + float currentCost = minCost.get(current); + for (var neighbor : pathGenerator.generate(world, current, bb)) { + float neighborCost = (float) (currentCost + current.distance(neighbor)); + if (neighborCost < minCost.getOrDefault(neighbor, Float.POSITIVE_INFINITY)) { + // New cheapest path, update indices + float neighborTotalCost = (float) (neighborCost + neighbor.distance(goal)); // Heuristic is just distance + cameFrom.put(neighbor, current); + minCost.put(neighbor, neighborTotalCost); + + if (!open.contains(neighbor)) { + open.add(neighbor); + } + } + } + } + + // Backtrack using cameFrom to find the best path + List result = new ArrayList<>(); + Point current = open.peek(); + if (current == null) return null; // Ran out of nodes + result.add(current); + while (cameFrom.containsKey(current)) { + current = cameFrom.get(current); + result.add(0, current); + } + + //todo optimize the path + + return new Path(result); + }; } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java index cc8fcb26..bd0125f5 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java @@ -5,16 +5,22 @@ import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; +import static net.minestom.server.instance.block.Block.Getter.Condition; + /** * Amalgamation of Minestom physics utility calls and a simpler static bounding box check in a world. */ public final class PhysicsUtil { private PhysicsUtil() {} + private static final int MAX_SNAP_DISTANCE = 32; + + /** * Simplified check if a bounding box collides with a solid block. *

    @@ -76,4 +82,25 @@ public static boolean testCollision(@NotNull Block.Getter world, @NotNull Point // } } + /** + * Snap the given point to the ground. If the point is the ground block, this moves it to + * the air block on top of the ground, if it is in the air, it snaps to the ground underneath. + *

    + * If there is no block within {@link #MAX_SNAP_DISTANCE}, null is returned. + */ + public static @Nullable Point gravitySnap(@NotNull Block.Getter world, @NotNull Point point) { + if (world.getBlock(point, Condition.TYPE).isSolid()) + return point.add(0, 1, 0); + + var ground = point.sub(0, 1, 0); + while (!world.getBlock(ground, Condition.TYPE).isSolid()) { + ground = ground.sub(0, 1, 0); + if (Math.abs(ground.blockY() - point.blockY()) > MAX_SNAP_DISTANCE) + return null; + } + + // Snap to the exact Y position + //todo need to take into account bounding box + return ground.withY(y -> Math.floor(y + 1)); + } } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java index ed8a545a..c91b5d83 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java @@ -12,15 +12,6 @@ public static double heuristic (Point node, Point target) { return node.distance(target); } - private static Collection getNeighborPositions(Point point) { - return List.of( - point.add(1, 0, 0), - point.add(-1, 0, 0), - point.add(0, 0, 1), - point.add(0, 0, -1) - ); - } - static Comparator pNodeComparator = (a, b) -> Float.compare(a.cost(), b.cost()); public static PFPath generate(Instance instance, Point orgStart, Point orgTarget, double maxDistance, double closeDistance, BoundingBox boundingBox, PFPathOptimizer optimizer) { Point start = PFNode.gravitySnap(instance, orgStart); diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathfinderAStar.java b/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathfinderAStar.java new file mode 100644 index 00000000..ff0e865e --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathfinderAStar.java @@ -0,0 +1,78 @@ +package unnamed.mmo.entity.motion; + +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.block.Block; +import net.minestom.server.utils.Direction; +import org.junit.jupiter.api.Test; +import unnamed.mmo.test.MockBlockGetter; + +import java.util.ArrayList; +import java.util.List; + +import static com.google.common.truth.Truth.assertThat; +import static net.minestom.server.instance.block.Block.Getter.Condition; + +public class TestPathfinderAStar { + + @Test + public void testSamePoint() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.empty(); + var start = new Vec(0, 0, 0); + var goal = new Vec(0, 0, 0); + + var result = Pathfinder.A_STAR.findPath(ALL, world, start, goal, bb); + assertThat(result).isNotNull(); + assertThat(result.nodes()).containsExactly(new Vec(0, 0, 0)); + } + + @Test + public void testBasicLine() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.empty(); + var start = new Vec(0.5, 0, 0.5); + var goal = new Vec(3, 0, 0); + + var result = Pathfinder.A_STAR.findPath(ALL, world, start, goal, bb); + assertThat(result).isNotNull(); + assertThat(result.nodes()).containsExactly( + new Vec(0.5, 0, 0.5), + new Vec(1.5, 0, 0.5), + new Vec(2.5, 0, 0.5) + ); + } + + @Test + public void testBasicAvoidance() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.block(1, 0, 0, Block.STONE); + var start = new Vec(0.5, 0, 0.5); + var goal = new Vec(2.5, 0, 0.5); + + var result = Pathfinder.A_STAR.findPath(ALL, world, start, goal, bb); + assertThat(result).isNotNull(); + assertThat(result.nodes()).containsExactly( + new Vec(0.5, 0, 0.5), + new Vec(0.5, 0, 1.5), + new Vec(1.5, 0, 1.5), + new Vec(2.5, 0, 1.5), + new Vec(2.5, 0, 0.5) + ); + } + + + // A path generator which returns any solid block in a direction (up/down/nsew) + private static final PathGenerator ALL = (world, pos, bb) -> { + pos = new Vec(pos.blockX() + 0.5, pos.blockY(), pos.blockZ() + 0.5); + List neighbors = new ArrayList<>(); + for (Direction direction : Direction.values()) { + var neighbor = pos.add(direction.normalX(), direction.normalY(), direction.normalZ()); + if (world.getBlock(neighbor, Condition.TYPE).isSolid()) continue; + neighbors.add(neighbor); + } + return neighbors; + }; + +} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/motion/util/TestPhysicsUtil.java b/modules/entity/src/test/java/unnamed/mmo/entity/motion/util/TestPhysicsUtil.java index 616cdafb..4f1cb15a 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/motion/util/TestPhysicsUtil.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/motion/util/TestPhysicsUtil.java @@ -1,81 +1,109 @@ package unnamed.mmo.entity.motion.util; import net.minestom.server.collision.BoundingBox; +import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import unnamed.mmo.test.MockBlockGetter; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; public class TestPhysicsUtil { - @Test - public void testEmpty() { - var bb = new BoundingBox(100, 100, 100); - var world = MockBlockGetter.empty(); - var pos = new Vec(0, 0, 0); + public static class TestCollision { + @Test + public void testEmpty() { + var bb = new BoundingBox(100, 100, 100); + var world = MockBlockGetter.empty(); + var pos = new Vec(0, 0, 0); - var result = PhysicsUtil.testCollision(world, pos, bb); + var result = PhysicsUtil.testCollision(world, pos, bb); - assertFalse(result); - } + assertFalse(result); + } - @Test - public void testSmallInsideBlock() { - var bb = new BoundingBox(0.5, 0.5, 0.5); - var world = MockBlockGetter.block(0, 0, 0, Block.STONE); - var pos = new Vec(0, 0, 0); + @Test + public void testSmallInsideBlock() { + var bb = new BoundingBox(0.5, 0.5, 0.5); + var world = MockBlockGetter.block(0, 0, 0, Block.STONE); + var pos = new Vec(0, 0, 0); - var result = PhysicsUtil.testCollision(world, pos, bb); + var result = PhysicsUtil.testCollision(world, pos, bb); - assertTrue(result); - } + assertTrue(result); + } - @Test - public void testBetweenBlocks() { - var bb = new BoundingBox(0.6, 1.95, 0.6); - var world = MockBlockGetter.block(1, 1, 0, Block.STONE) - .set(-1, 1, 0, Block.STONE); - var pos = new Vec(0, 0, 0); + @Test + public void testBetweenBlocks() { + var bb = new BoundingBox(0.6, 1.95, 0.6); + var world = MockBlockGetter.block(1, 1, 0, Block.STONE).set(-1, 1, 0, Block.STONE); + var pos = new Vec(0, 0, 0); - var result = PhysicsUtil.testCollision(world, pos, bb); + var result = PhysicsUtil.testCollision(world, pos, bb); - assertTrue(result); - } + assertTrue(result); + } - @Test - public void testUnderBlock() { - var bb = new BoundingBox(0.6, 1.95, 0.6); - var world = MockBlockGetter.block(0, 1, 0, Block.STONE); - var pos = new Vec(0.5, 0, 0.5); + @Test + public void testUnderBlock() { + var bb = new BoundingBox(0.6, 1.95, 0.6); + var world = MockBlockGetter.block(0, 1, 0, Block.STONE); + var pos = new Vec(0.5, 0, 0.5); - var result = PhysicsUtil.testCollision(world, pos, bb); + var result = PhysicsUtil.testCollision(world, pos, bb); - assertTrue(result); - } + assertTrue(result); + } - @Test - public void testUnderBlock2() { - var bb = new BoundingBox(0.6, 1.95, 0.6); - var world = MockBlockGetter.block(0, 2, 0, Block.STONE); - var pos = new Vec(0.5, 0, 0.5); + @Test + public void testUnderBlock2() { + var bb = new BoundingBox(0.6, 1.95, 0.6); + var world = MockBlockGetter.block(0, 2, 0, Block.STONE); + var pos = new Vec(0.5, 0, 0.5); - var result = PhysicsUtil.testCollision(world, pos, bb); + var result = PhysicsUtil.testCollision(world, pos, bb); - assertFalse(result); - } + assertFalse(result); + } + + @Test + public void testWaterNoCollision() { + var bb = new BoundingBox(0.6, 0.6, 0.6); + var world = MockBlockGetter.range(-1, -1, -1, 1, 1, 1, Block.WATER); + var pos = new Vec(0.5, 0, 0.5); - @Test - public void testWaterNoCollision() { - var bb = new BoundingBox(0.6, 0.6, 0.6); - var world = MockBlockGetter.range(-1, -1, -1, 1, 1, 1, Block.WATER); - var pos = new Vec(0.5, 0, 0.5); + var result = PhysicsUtil.testCollision(world, pos, bb); - var result = PhysicsUtil.testCollision(world, pos, bb); + assertFalse(result); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("gravitySnapCases") + public void testGravitySnap(String name, Point start, boolean expected) { + var world = MockBlockGetter.block(0, 50, 0, Block.STONE); + var result = PhysicsUtil.gravitySnap(world, start); + if (expected) { + assertEquals(new Vec(0, 51, 0), result); + } else { + assertNull(result); + } + } - assertFalse(result); + private static Stream gravitySnapCases() { + // Each case has a block at 0, 50, 0 + return Stream.of( + Arguments.of("test correct place already", new Vec(0, 51, 0), true), + Arguments.of("inside block", new Vec(0, 50, 0), true), + Arguments.of("above block", new Vec(0, 58, 0), true), + Arguments.of("below block (fail to find)", new Vec(0, 40, 0), false) + ); } } From cf19db5e4cc6387858a71d7bbf6fed7c85006ac7 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 4 Sep 2022 21:24:59 -0400 Subject: [PATCH 15/25] add first version of string pull optimizer --- .../java/unnamed/mmo/entity/motion/Path.java | 9 ++ .../mmo/entity/motion/PathOptimizer.java | 47 ++++++++ .../mmo/entity/motion/util/PhysicsUtil.java | 43 +++++++ .../motion/TestPathOptimizerStringPull.java | 111 ++++++++++++++++++ 4 files changed, 210 insertions(+) create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/motion/PathOptimizer.java create mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathOptimizerStringPull.java diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Path.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/Path.java index 4425a09b..c041f476 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Path.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/Path.java @@ -6,4 +6,13 @@ import java.util.List; public record Path(@NotNull List nodes) { + + public Point get(int index) { + return nodes.get(index); + } + + public int size() { + return nodes.size(); + } + } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathOptimizer.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathOptimizer.java new file mode 100644 index 00000000..ffb0bf5e --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathOptimizer.java @@ -0,0 +1,47 @@ +package unnamed.mmo.entity.motion; + +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.collision.CollisionUtils; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; +import unnamed.mmo.entity.motion.util.PhysicsUtil; + +import java.util.ArrayList; +import java.util.List; + +public interface PathOptimizer { + + @NotNull Path optimize(@NotNull Path path, @NotNull Block.Getter world, @NotNull BoundingBox bb); + + + /** Returns the input path with no modification. */ + PathOptimizer NOOP = (path, world, bb) -> path; + + /** + * Walks the path attempting to drop intermediate nodes and walk directly to the next one. + * If there is collision, add the current node and start again on the next one. + */ + PathOptimizer STRING_PULL = (path, world, bb) -> { + // On short paths there is nothing that can be shortened. + if (path.size() < 3) return path; + + List newPath = new ArrayList<>(); + newPath.add(path.get(0)); + + int current = 0; + int next = 1; + for (int i = 0; i < path.size() - 1; i++) { + boolean didCollide = PhysicsUtil.testCollisionSwept(world, bb, path.get(current), path.get(next)); + if (didCollide) { + newPath.add(path.get(next - 1)); + current = next; + } + next++; + } + + newPath.add(path.get(path.size() - 1)); + return new Path(newPath); + }; + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java index bd0125f5..a6365412 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java @@ -1,12 +1,19 @@ package unnamed.mmo.entity.motion.util; import net.minestom.server.collision.BoundingBox; +import net.minestom.server.collision.CollisionUtils; +import net.minestom.server.collision.PhysicsResult; import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; @@ -15,11 +22,14 @@ /** * Amalgamation of Minestom physics utility calls and a simpler static bounding box check in a world. */ +@SuppressWarnings("UnstableApiUsage") public final class PhysicsUtil { private PhysicsUtil() {} private static final int MAX_SNAP_DISTANCE = 32; + private static final MethodHandle handlePhysics; + /** * Simplified check if a bounding box collides with a solid block. @@ -82,6 +92,28 @@ public static boolean testCollision(@NotNull Block.Getter world, @NotNull Point // } } + /** + * Collision check from a starting point to a given position. + *

    + * Uses Minestom internal physics check, which does account for block shapes. + */ + public static boolean testCollisionSwept(@NotNull Block.Getter world, @NotNull BoundingBox bb, @NotNull Point from, @NotNull Point to) { + PhysicsResult result; + if (world instanceof Instance instance) { + result = CollisionUtils.handlePhysics(instance, instance.getChunkAt(from), + bb, Pos.fromPoint(from), Vec.fromPoint(to.sub(from)), null); + } else { + // Not advisable in production, but acceptable to use in tests + try { + result = (PhysicsResult) handlePhysics.invoke(bb, Vec.fromPoint(to.sub(from)), Pos.fromPoint(from), world, null); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + return result.collisionX() || result.collisionY() || result.collisionZ(); + } + /** * Snap the given point to the ground. If the point is the ground block, this moves it to * the air block on top of the ground, if it is in the air, it snaps to the ground underneath. @@ -103,4 +135,15 @@ public static boolean testCollision(@NotNull Block.Getter world, @NotNull Point //todo need to take into account bounding box return ground.withY(y -> Math.floor(y + 1)); } + + static { + try { + Class blockCollision = Class.forName("net.minestom.server.collision.BlockCollision"); + Method rHandlePhysics = blockCollision.getDeclaredMethod("handlePhysics", BoundingBox.class, Vec.class, Pos.class, Block.Getter.class, PhysicsResult.class); + rHandlePhysics.setAccessible(true); + handlePhysics = MethodHandles.lookup().unreflect(rHandlePhysics); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } } diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathOptimizerStringPull.java b/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathOptimizerStringPull.java new file mode 100644 index 00000000..5ad45ba5 --- /dev/null +++ b/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathOptimizerStringPull.java @@ -0,0 +1,111 @@ +package unnamed.mmo.entity.motion; + +import net.minestom.server.collision.BoundingBox; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.block.Block; +import org.junit.jupiter.api.Test; +import unnamed.mmo.test.MockBlockGetter; + +import java.util.List; + +import static com.google.common.truth.Truth.assertThat; + +public class TestPathOptimizerStringPull { + + @Test + public void testEmpty() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.empty(); + var path = new Path(List.of()); + + var result = PathOptimizer.STRING_PULL.optimize(path, world, bb); + assertThat(result.nodes()).isEmpty(); + } + + @Test + public void testTwoPoints() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.empty(); + var path = new Path(List.of( + new Vec(0, 0, 0), + new Vec(1, 0, 0) + )); + + var result = PathOptimizer.STRING_PULL.optimize(path, world, bb); + assertThat(result.nodes()).isEqualTo(path.nodes()); + } + + @Test + public void testThreePointsNoObstacles() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.empty(); + var path = new Path(List.of( + new Vec(0, 0, 0), + new Vec(1, 0, 0), + new Vec(1, 0, 1) + )); + + var result = PathOptimizer.STRING_PULL.optimize(path, world, bb); + assertThat(result.nodes()).containsExactly( + new Vec(0, 0, 0), + new Vec(1, 0, 1) + ); + } + + @Test + public void testFivePointsNoObstacles() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.empty(); + var path = new Path(List.of( + new Vec(0, 0, 0), + new Vec(1, 0, 0), + new Vec(1, 0, 1), + new Vec(1, 0, 2), + new Vec(1, 0, 3) + )); + + var result = PathOptimizer.STRING_PULL.optimize(path, world, bb); + assertThat(result.nodes()).containsExactly( + new Vec(0, 0, 0), + new Vec(1, 0, 3) + ); + } + + @Test + public void testThreePointsWithObstacle() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.block(1, 0, 1, Block.STONE); + var path = new Path(List.of( + new Vec(0, 0, 0), + new Vec(2, 0, 0), + new Vec(2, 0, 2) + )); + + var result = PathOptimizer.STRING_PULL.optimize(path, world, bb); + assertThat(result.nodes()).containsExactly( + new Vec(0, 0, 0), + new Vec(2, 0, 0), + new Vec(2, 0, 2) + ); + } + + @Test + public void testFivePointsWithObstacle() { + var bb = new BoundingBox(0.1, 0.1, 0.1); + var world = MockBlockGetter.block(1, 0, 1, Block.STONE); + var path = new Path(List.of( + new Vec(0, 0, 0), + new Vec(2, 0, 0), + new Vec(2, 0, 2), + new Vec(2, 0, 4), + new Vec(2, 0, 6) + )); + + var result = PathOptimizer.STRING_PULL.optimize(path, world, bb); + assertThat(result.nodes()).containsExactly( + new Vec(0, 0, 0), + new Vec(2, 0, 0), + new Vec(2, 0, 6) + ); + } +} From f25851a5028d998310966e451fdf3c0b0c441e00 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 4 Sep 2022 22:42:42 -0400 Subject: [PATCH 16/25] first (pretty bad) draft of slime movement. lots of duplication, doesnt look where it is going, overall kinda cursed --- .../unnamed/mmo/entity/UnnamedEntity.java | 4 +- .../mmo/entity/brain/SingleTaskBrain.java | 2 +- .../mmo/entity/brain/navigator/Navigator.java | 15 +- .../mmo/entity/motion/MotionNavigator.java | 141 +++++++++++++++++ .../entity/motion/MotionNavigatorSlime.java | 146 ++++++++++++++++++ .../unnamed/mmo/entity/motion/Navigator.java | 8 - .../mmo/entity/motion/PathGenerator.java | 9 +- .../TestNavigatorBasicIntegration.java | 2 +- .../mmo/entity/brain/navigator/TestUtil.java | 4 +- 9 files changed, 316 insertions(+), 15 deletions(-) create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigatorSlime.java delete mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/motion/Navigator.java diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java b/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java index 54ccacef..64a0c659 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java @@ -4,6 +4,7 @@ import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; import net.minestom.server.entity.LivingEntity; +import net.minestom.server.entity.metadata.other.SlimeMeta; import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.entity.EntityAttackEvent; import net.minestom.server.instance.Instance; @@ -18,7 +19,8 @@ public class UnnamedEntity extends LivingEntity { private final Brain brain; public UnnamedEntity(Task task) { - super(EntityType.ZOMBIE); + super(EntityType.SLIME); + ((SlimeMeta) getEntityMeta()).setSize(2); brain = new SingleTaskBrain(this, task); } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java index 8d790012..3e9a2300 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java @@ -18,7 +18,7 @@ public class SingleTaskBrain implements Brain { public SingleTaskBrain(@NotNull Entity entity, @NotNull Task task) { this.entity = entity; - this.navigator = Navigator.custom(entity); + this.navigator = Navigator.motionSlime(entity); this.task = task; } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java index 6fa449a4..05dfa7ba 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java @@ -4,9 +4,12 @@ import net.minestom.server.entity.Entity; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import unnamed.mmo.entity.brain.Brain; +import unnamed.mmo.entity.motion.MotionNavigator; +import unnamed.mmo.entity.motion.MotionNavigatorSlime; -public sealed interface Navigator permits CustomNavigator, EnodiaNavigator, HydrazineNavigator { +public interface Navigator { static @NotNull Navigator enodia(@NotNull Entity entity) { return new EnodiaNavigator(entity); @@ -20,9 +23,17 @@ public sealed interface Navigator permits CustomNavigator, EnodiaNavigator, Hydr return new CustomNavigator(entity); } + static @NotNull Navigator motion(@NotNull Entity entity) { + return new MotionNavigator(entity); + } + + static @NotNull Navigator motionSlime(@NotNull Entity entity) { + return new MotionNavigatorSlime(entity); + } + default void setInstance(@NotNull Instance instance) {} - boolean setPathTo(@NotNull Point point); + boolean setPathTo(@Nullable Point point); boolean isActive(); diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java new file mode 100644 index 00000000..54543a82 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java @@ -0,0 +1,141 @@ +package unnamed.mmo.entity.motion; + +import net.minestom.server.attribute.Attribute; +import net.minestom.server.collision.CollisionUtils; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.LivingEntity; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.Instance; +import net.minestom.server.utils.position.PositionUtils; +import net.minestom.server.utils.time.Cooldown; +import net.minestom.server.utils.time.TimeUnit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import unnamed.mmo.entity.brain.navigator.Navigator; + +import java.time.Duration; + +/** + * Consumes paths from a {@link Pathfinder} to navigate an {@link net.minestom.server.entity.Entity} in an instance. + */ +public final class MotionNavigator implements Navigator { + private final Cooldown jumpCooldown = new Cooldown(Duration.of(40, TimeUnit.SERVER_TICK)); + private final Entity entity; + + private Point goal = null; + private Path path = null; + private int index = 0; + + public MotionNavigator(@NotNull Entity entity) { + this.entity = entity; + } + + @Override + public boolean isActive() { + return path != null; + } + + public void reset() { + goal = null; + path = null; + index = 0; + } + + @Override + public synchronized boolean setPathTo(@Nullable Point point) { + // Providing a null point clears the navigation task + if (point == null) { + reset(); + return true; + } + + float minDistance = 0.8f; //todo move me + + // Early exit if trying to path to the same point as before, or the entity is already close enough + if (goal != null && path != null && point.samePoint(goal)) + return true; + if (entity.getPosition().distance(point) < minDistance) { + // In this case we reset because we are changing the path to the given point (which we are + // nearby already) so navigation should stop after this point. + reset(); + return true; + } + + // Ensure the entity is in an instance + final Instance instance = entity.getInstance(); + if (instance == null) + return false; + + // Cannot set a path outside the world border + if (!instance.getWorldBorder().isInside(point)) + return false; + + // Cannot path to an unloaded chunk + final Chunk chunk = instance.getChunkAt(point); + if (chunk == null || !chunk.isLoaded()) + return false; + + // Attempt to find a path + path = Pathfinder.A_STAR.findPath(PathGenerator.LAND, instance, + entity.getPosition(), point, entity.getBoundingBox()); + + boolean success = path != null; + goal = success ? point : null; + return success; + } + + @Override + public void tick(long time) { + if (goal == null || path == null) return; // No path + if (entity instanceof LivingEntity livingEntity && livingEntity.isDead()) return; + + // If we are close enough to the goal position, just stop + float minDistance = 0.8f; //todo move me + if (entity.getDistance(goal) < minDistance) { + reset(); + return; + } + + Point current = index < path.size() ? path.get(index) : goal; + + float movementSpeed = 0.1f; + if (entity instanceof LivingEntity livingEntity) { + movementSpeed = livingEntity.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue(); + } + + // Move towards the current target, trying to jump if stuck + boolean isStuck = moveTowards(current, movementSpeed); + //todo jump if stuck + + // Move to next point if stuck + if (entity.getPosition().distanceSquared(current) < 0.4) { + index++; + } + } + + private boolean moveTowards(@NotNull Point direction, double speed) { + final Pos position = entity.getPosition(); + final double dx = direction.x() - position.x(); + final double dy = direction.y() - position.y(); + final double dz = direction.z() - position.z(); + // the purpose of these few lines is to slow down entities when they reach their destination + final double distSquared = dx * dx + dy * dy + dz * dz; + if (speed > distSquared) { + speed = distSquared; + } + final double radians = Math.atan2(dz, dx); + final double speedX = Math.cos(radians) * speed; + final double speedY = dy * speed; + final double speedZ = Math.sin(radians) * speed; + final float yaw = PositionUtils.getLookYaw(dx, dz); + final float pitch = PositionUtils.getLookPitch(dx, dy, dz); + // Prevent ghosting + final var physicsResult = CollisionUtils.handlePhysics(entity, new Vec(speedX, speedY, speedZ)); + this.entity.refreshPosition(Pos.fromPoint(physicsResult.newPosition()).withView(yaw, pitch)); + return physicsResult.collisionX() || physicsResult.collisionY() || physicsResult.collisionZ(); + } + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigatorSlime.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigatorSlime.java new file mode 100644 index 00000000..1819e783 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigatorSlime.java @@ -0,0 +1,146 @@ +package unnamed.mmo.entity.motion; + +import net.minestom.server.attribute.Attribute; +import net.minestom.server.collision.CollisionUtils; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.LivingEntity; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.Instance; +import net.minestom.server.utils.position.PositionUtils; +import net.minestom.server.utils.time.Cooldown; +import net.minestom.server.utils.time.TimeUnit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import unnamed.mmo.entity.brain.navigator.Navigator; + +import java.time.Duration; + +/** + * Consumes paths from a {@link Pathfinder} to navigate an {@link Entity} in an instance. + */ +public final class MotionNavigatorSlime implements Navigator { + private final Cooldown jumpCooldown = new Cooldown(Duration.of(40, TimeUnit.SERVER_TICK)); + private final Entity entity; + + private Point goal = null; + private Path path = null; + private int index = 0; + + public MotionNavigatorSlime(@NotNull Entity entity) { + this.entity = entity; + } + + @Override + public boolean isActive() { + return path != null; + } + + public void reset() { + goal = null; + path = null; + index = 0; + } + + @Override + public synchronized boolean setPathTo(@Nullable Point point) { + // Providing a null point clears the navigation task + if (point == null) { + reset(); + return true; + } + + float minDistance = 0.8f; //todo move me + + // Early exit if trying to path to the same point as before, or the entity is already close enough + if (goal != null && path != null && point.samePoint(goal)) + return true; + if (entity.getPosition().distance(point) < minDistance) { + // In this case we reset because we are changing the path to the given point (which we are + // nearby already) so navigation should stop after this point. + reset(); + return true; + } + + // Ensure the entity is in an instance + final Instance instance = entity.getInstance(); + if (instance == null) + return false; + + // Cannot set a path outside the world border + if (!instance.getWorldBorder().isInside(point)) + return false; + + // Cannot path to an unloaded chunk + final Chunk chunk = instance.getChunkAt(point); + if (chunk == null || !chunk.isLoaded()) + return false; + + // Attempt to find a path + path = Pathfinder.A_STAR.findPath(PathGenerator.LAND, instance, + entity.getPosition(), point, entity.getBoundingBox()); + + boolean success = path != null; + goal = success ? point : null; + return success; + } + + @Override + public void tick(long time) { + if (goal == null || path == null) return; // No path + if (entity instanceof LivingEntity livingEntity && livingEntity.isDead()) return; + + // If we are close enough to the goal position, just stop + float minDistance = 0.8f; //todo move me + if (entity.getDistance(goal) < minDistance) { + reset(); + return; + } + + Point current = index < path.size() ? path.get(index) : goal; + + float movementSpeed = 0.1f; + if (entity instanceof LivingEntity livingEntity) { + movementSpeed = livingEntity.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue(); + } + + // Move towards the current target, trying to jump if stuck + if (jumpCooldown.isReady(time)) { + moveTowards(current, movementSpeed); + jumpCooldown.refreshLastUpdate(time); + } + + // Move to next point if stuck + if (entity.getPosition().distanceSquared(current) < 0.4) { + index++; + } + } + + private boolean moveTowards(@NotNull Point direction, double speed) { + final Pos position = entity.getPosition(); + final double dx = direction.x() - position.x(); + final double dy = direction.y() - position.y(); + final double dz = direction.z() - position.z(); + // the purpose of these few lines is to slow down entities when they reach their destination + final double distSquared = dx * dx + dy * dy + dz * dz; + if (speed > distSquared) { + speed = distSquared; + } + final double radians = Math.atan2(dz, dx); + final double speedX = Math.cos(radians) * speed * 10; + final double speedY = 8 + dy * speed * 2.5; + final double speedZ = Math.sin(radians) * speed * 10; + entity.setVelocity(new Vec(speedX, speedY, speedZ)); + return true; + +// final float yaw = PositionUtils.getLookYaw(dx, dz); +// final float pitch = PositionUtils.getLookPitch(dx, dy, dz); +// // Prevent ghosting +// final var physicsResult = CollisionUtils.handlePhysics(entity, new Vec(speedX, speedY, speedZ)); +// this.entity.refreshPosition(Pos.fromPoint(physicsResult.newPosition()).withView(yaw, pitch)); +// return physicsResult.collisionX() || physicsResult.collisionY() || physicsResult.collisionZ(); + } + +} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Navigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/Navigator.java deleted file mode 100644 index 944cf6bf..00000000 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Navigator.java +++ /dev/null @@ -1,8 +0,0 @@ -package unnamed.mmo.entity.motion; - -/** - * Consumes paths from a {@link Pathfinder} to navigate an {@link net.minestom.server.entity.Entity} in an instance. - */ -public interface Navigator { - -} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java index f436248a..3de173f2 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java @@ -31,7 +31,14 @@ public interface PathGenerator { for (int y = -1; y <= 1; y++) { var neighbor = pos.add(direction.normalX(), direction.normalY() + y, direction.normalZ()); // Block below must be solid, or we cannot move to it - if (!world.getBlock(neighbor.add(0, -1, 0), Condition.TYPE).isSolid()) continue; + try { + if (!world.getBlock(neighbor.add(0, -1, 0), Condition.TYPE).isSolid()) continue; + } catch (RuntimeException e) { + //todo need a better solution here. Instance throws an exception if the chunk is unloaded + // but that is kinda awful behavior here. Probably i will need to check if the chunk + // is loaded, but then i cant use a block getter + continue; + } // Ensure the BB fits at that block if (PhysicsUtil.testCollision(world, neighbor, bb)) continue; diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java index 223e57e4..ccdaf171 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java @@ -30,7 +30,7 @@ public void testBasicMovement(String name, Function newNaviga entity.setInstance(instance, new Pos(5, 40, 5)).join(); navigator.setInstance(instance); - navigator.setPathTo(new Pos(0, 40, 0)); + var pathFound = navigator.setPathTo(new Pos(0, 40, 0)); var result = env.tickWhile(() -> { System.out.println(entity.getPosition()); navigator.tick(System.currentTimeMillis()); diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java index b9f02a7c..6c59a9ad 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java @@ -2,6 +2,7 @@ import net.minestom.server.entity.Entity; import org.junit.jupiter.params.provider.Arguments; +import unnamed.mmo.entity.motion.MotionNavigator; import java.util.function.Function; import java.util.stream.Stream; @@ -12,7 +13,8 @@ public static Stream navigators() { return Stream.of( // Arguments.of("enodia", (Function) EnodiaNavigator::new), // Arguments.of("hydrazine", (Function) HydrazineNavigator::new), - Arguments.of("custom", (Function) CustomNavigator::new) +// Arguments.of("custom", (Function) CustomNavigator::new), + Arguments.of("motion", (Function) MotionNavigator::new) ); } } From dc19ab92e3f1de12fda7394e227b0de1f5b20d1e Mon Sep 17 00:00:00 2001 From: mworzala Date: Mon, 5 Sep 2022 06:15:29 -0400 Subject: [PATCH 17/25] minor tweaks and debugging entity direction problem --- .../java/net/hollowcube/server/dev/Main.java | 64 ++++++++++----- .../mmo/entity/brain/SingleTaskBrain.java | 2 +- .../mmo/entity/motion/MotionNavigator.java | 77 +++++++++++++++++++ .../entity/motion/MotionNavigatorSlime.java | 4 + .../unnamed/mmo/entity/motion/Pathfinder.java | 6 +- 5 files changed, 130 insertions(+), 23 deletions(-) diff --git a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java index 661c86db..c1e2fb21 100644 --- a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java +++ b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java @@ -7,24 +7,36 @@ import net.hollowcube.server.instance.TickTrackingInstance; import com.google.gson.JsonElement; import com.google.gson.JsonParser; +import com.mattworzala.debug.DebugMessage; +import com.mattworzala.debug.Layer; +import com.mattworzala.debug.shape.Line; import com.mojang.serialization.JsonOps; +import net.kyori.adventure.audience.Audience; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerProcess; import net.minestom.server.command.builder.Command; +import net.minestom.server.adventure.MinestomAdventure; import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; import net.minestom.server.event.EventNode; import net.minestom.server.event.GlobalEventHandler; import net.minestom.server.event.player.PlayerLoginEvent; +import net.minestom.server.event.player.PlayerPacketOutEvent; import net.minestom.server.event.player.PlayerSpawnEvent; import net.minestom.server.extras.MojangAuth; import net.minestom.server.instance.Instance; import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; +import net.minestom.server.network.packet.server.*; +import net.minestom.server.network.packet.server.play.EntityHeadLookPacket; +import net.minestom.server.network.packet.server.play.EntityPositionAndRotationPacket; import net.minestom.server.potion.Potion; import net.minestom.server.potion.PotionEffect; +import net.minestom.server.utils.NamespaceID; import net.minestom.server.world.DimensionType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -60,6 +72,7 @@ import java.util.ServiceLoader; import java.util.UUID; import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @@ -104,30 +117,43 @@ public static void main(String[] args) { //todo test entity +// JsonElement json = JsonParser.parseString(""" +// { +// "type": "unnamed:selector", +// "children": { +// "q.has_target": { +// "type": "unnamed:follow_target" +// }, +// "": { +// "type": "unnamed:sequence", +// "children": [ +// { +// "type": "unnamed:wander_in_region" +// }, +// { +// "type": "unnamed:idle", +// "time": 5 +// } +// ], +// +// "canInterrupt": true +// } +// } +// }"""); JsonElement json = JsonParser.parseString(""" { - "type": "unnamed:selector", - "children": { - "q.has_target": { - "type": "unnamed:follow_target" + "type": "unnamed:sequence", + "children": [ + { + "type": "unnamed:wander_in_region" }, - "": { - "type": "unnamed:sequence", - "children": [ - { - "type": "unnamed:wander_in_region" - }, - { - "type": "unnamed:idle", - "time": 5 - } - ], - - "canInterrupt": true + { + "type": "unnamed:idle", + "time": 20 } - } + ] }"""); - Task task = JsonOps.INSTANCE.withDecoder(SelectorTask.Spec.CODEC) + Task task = JsonOps.INSTANCE.withDecoder(Task.Spec.CODEC) .apply(json).getOrThrow(false, System.err::println).getFirst().create(); UnnamedEntity entity = new UnnamedEntity(task); entity.setInstance(instance, new Pos(0, 40, 0)) diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java index 3e9a2300..ff9ed879 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java @@ -18,7 +18,7 @@ public class SingleTaskBrain implements Brain { public SingleTaskBrain(@NotNull Entity entity, @NotNull Task task) { this.entity = entity; - this.navigator = Navigator.motionSlime(entity); + this.navigator = Navigator.motion(entity); this.task = task; } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java index 54543a82..c32754b8 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java @@ -1,5 +1,10 @@ package unnamed.mmo.entity.motion; +import com.mattworzala.debug.DebugMessage; +import com.mattworzala.debug.Layer; +import com.mattworzala.debug.shape.Box; +import com.mattworzala.debug.shape.Line; +import com.mattworzala.debug.shape.OutlineBox; import net.minestom.server.attribute.Attribute; import net.minestom.server.collision.CollisionUtils; import net.minestom.server.coordinate.Point; @@ -9,6 +14,7 @@ import net.minestom.server.entity.LivingEntity; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; +import net.minestom.server.utils.NamespaceID; import net.minestom.server.utils.position.PositionUtils; import net.minestom.server.utils.time.Cooldown; import net.minestom.server.utils.time.TimeUnit; @@ -17,12 +23,14 @@ import unnamed.mmo.entity.brain.navigator.Navigator; import java.time.Duration; +import java.util.ArrayList; /** * Consumes paths from a {@link Pathfinder} to navigate an {@link net.minestom.server.entity.Entity} in an instance. */ public final class MotionNavigator implements Navigator { private final Cooldown jumpCooldown = new Cooldown(Duration.of(40, TimeUnit.SERVER_TICK)); + private final Cooldown debugCooldown = new Cooldown(Duration.of(1, TimeUnit.SERVER_TICK)); private final Entity entity; private Point goal = null; @@ -96,9 +104,15 @@ public void tick(long time) { float minDistance = 0.8f; //todo move me if (entity.getDistance(goal) < minDistance) { reset(); + sendDebugData(); return; } + if (debugCooldown.isReady(time)) { + debugCooldown.refreshLastUpdate(time); + sendDebugData(); + } + Point current = index < path.size() ? path.get(index) : goal; float movementSpeed = 0.1f; @@ -132,10 +146,73 @@ private boolean moveTowards(@NotNull Point direction, double speed) { final double speedZ = Math.sin(radians) * speed; final float yaw = PositionUtils.getLookYaw(dx, dz); final float pitch = PositionUtils.getLookPitch(dx, dy, dz); + // Prevent ghosting final var physicsResult = CollisionUtils.handlePhysics(entity, new Vec(speedX, speedY, speedZ)); this.entity.refreshPosition(Pos.fromPoint(physicsResult.newPosition()).withView(yaw, pitch)); return physicsResult.collisionX() || physicsResult.collisionY() || physicsResult.collisionZ(); } + + // SECTION: Debug rendering + // Eventually this should be only in the dev server. Just don't currently have a way to do a "mixin" here. + // Probably will have some way to set the entity provider somewhere. + + private @NotNull String debugNamespace(){ + return "debug_" + entity.getUuid(); + } + + private void sendDebugData() { + var builder = DebugMessage.builder() + .clear(debugNamespace()); + + addPathfinderDebugData(builder); + addTargetPoint(builder); + + // Send the server side view + + builder.set(NamespaceID.from(debugNamespace(), "view_dir"), new Line.Builder() + .point(entity.getPosition().asVec()) + .point(entity.getPosition().direction().mul(2).add(entity.getPosition().asVec())) + .color(0xFFFFFFFF) + .layer(Layer.TOP) + .build()); + + builder.build() + .sendTo(entity.getViewersAsAudience()); + } + + private void addPathfinderDebugData(DebugMessage.Builder builder) { + if (path == null) return; + var nodes = path.nodes(); + var linePoints = new ArrayList(); + + for (int i = index; i < nodes.size(); i++) { + var pos = Vec.fromPoint(nodes.get(i)); + builder.set( + debugNamespace() + ":pf_node_" + i, + new Box(pos.sub(0.4, 0.0, 0.4), pos.add(0.4, 0.1, 0.4), 0x331CB2F5, Layer.TOP) + ); + linePoints.add(pos.withY(y -> y + 0.05)); + } + builder.set( + debugNamespace() + ":pf_path", + new Line(linePoints, 10f, 0xFF1CB2F5, Layer.TOP) + ); + } + + private void addTargetPoint(DebugMessage.Builder builder) { + if (goal == null) return; + builder.set( + debugNamespace() + ":pf_target", + new OutlineBox.Builder() + .block(goal.blockX(), goal.blockY(), goal.blockZ(), 0) + .color(0x55FF0000) + .layer(Layer.TOP) + .colorLine(0xFFFF0000) + .layerLine(Layer.TOP) + .build() + ); + } + } diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigatorSlime.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigatorSlime.java index 1819e783..06dc0c08 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigatorSlime.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigatorSlime.java @@ -106,6 +106,10 @@ public void tick(long time) { movementSpeed = livingEntity.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue(); } + // Alternative way to do this movement: + // - rotate towards the target pos each tick (interpolated probably, though the client interpolation might be enough) + // - jump in facing direction occasionally + // Move towards the current target, trying to jump if stuck if (jumpCooldown.isReady(time)) { moveTowards(current, movementSpeed); diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java index 46cf4e56..5915444e 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java @@ -2,11 +2,9 @@ import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; -import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import unnamed.mmo.entity.motion.util.PhysicsUtil; import java.util.*; @@ -93,8 +91,10 @@ public interface Pathfinder { result.add(0, current); } + Path path = new Path(result); //todo optimize the path + path = PathOptimizer.STRING_PULL.optimize(path, world, bb); - return new Path(result); + return path; }; } From 8543b22c8b903144ca9512bb4d006c6d6fcf6399 Mon Sep 17 00:00:00 2001 From: mworzala Date: Mon, 5 Sep 2022 06:52:02 -0400 Subject: [PATCH 18/25] more debugging --- .../java/net/hollowcube/server/dev/Main.java | 51 +++++++++------ .../mmo/entity/HeadRotationZombie.java | 63 +++++++++++++++++++ .../mmo/entity/motion/MotionNavigator.java | 9 +++ 3 files changed, 105 insertions(+), 18 deletions(-) create mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/HeadRotationZombie.java diff --git a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java index c1e2fb21..fd786848 100644 --- a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java +++ b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java @@ -10,15 +10,18 @@ import com.mattworzala.debug.DebugMessage; import com.mattworzala.debug.Layer; import com.mattworzala.debug.shape.Line; +import com.mattworzala.debug.shape.Text; import com.mojang.serialization.JsonOps; import net.kyori.adventure.audience.Audience; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerProcess; import net.minestom.server.command.builder.Command; import net.minestom.server.adventure.MinestomAdventure; +import net.minestom.server.command.builder.Command; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; import net.minestom.server.event.EventNode; @@ -53,6 +56,7 @@ import unnamed.mmo.command.BaseCommandRegister; import unnamed.mmo.damage.DamageProcessor; import unnamed.mmo.data.number.NumberProvider; +import unnamed.mmo.entity.HeadRotationZombie; import unnamed.mmo.entity.UnnamedEntity; import unnamed.mmo.entity.brain.task.*; import unnamed.mmo.item.Item; @@ -140,24 +144,35 @@ public static void main(String[] args) { // } // } // }"""); - JsonElement json = JsonParser.parseString(""" - { - "type": "unnamed:sequence", - "children": [ - { - "type": "unnamed:wander_in_region" - }, - { - "type": "unnamed:idle", - "time": 20 - } - ] - }"""); - Task task = JsonOps.INSTANCE.withDecoder(Task.Spec.CODEC) - .apply(json).getOrThrow(false, System.err::println).getFirst().create(); - UnnamedEntity entity = new UnnamedEntity(task); - entity.setInstance(instance, new Pos(0, 40, 0)) - .thenAccept(unused -> System.out.println("Spawned")); +// JsonElement json = JsonParser.parseString(""" +// { +// "type": "unnamed:sequence", +// "children": [ +// { +// "type": "unnamed:wander_in_region" +// }, +// { +// "type": "unnamed:idle", +// "time": 20 +// } +// ] +// }"""); +// Task task = JsonOps.INSTANCE.withDecoder(Task.Spec.CODEC) +// .apply(json).getOrThrow(false, System.err::println).getFirst().create(); +// UnnamedEntity entity = new UnnamedEntity(task); +// entity.setInstance(instance, new Pos(0, 40, 0)) +// .thenAccept(unused -> System.out.println("Spawned")); + +// Entity entity = new Entity(EntityType.ZOMBIE) { +// @Override +// public void tick(long time) { +// super.tick(time); +// +// lookAt(player); +// } +// }; + Entity entity = new HeadRotationZombie(); + entity.setInstance(instance, new Pos(0, 40, 0)); }); BaseCommandRegister.registerCommands(); //todo this should be in a facet? diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/HeadRotationZombie.java b/modules/entity/src/main/java/unnamed/mmo/entity/HeadRotationZombie.java new file mode 100644 index 00000000..f6704950 --- /dev/null +++ b/modules/entity/src/main/java/unnamed/mmo/entity/HeadRotationZombie.java @@ -0,0 +1,63 @@ +package unnamed.mmo.entity; + +import net.minestom.server.collision.CollisionUtils; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.LivingEntity; +import net.minestom.server.utils.position.PositionUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.ThreadLocalRandom; + +public class HeadRotationZombie extends LivingEntity { + private Point target; + + public HeadRotationZombie() { + super(EntityType.ZOMBIE); + } + + @Override + public void tick(long time) { + super.tick(time); + + if (target == null || getPosition().distance(target) < 0.8) { + selectTarget(); + } + + moveTowards(target, 0.2f); + + } + + private void selectTarget() { + target = new Vec( + ThreadLocalRandom.current().nextInt(-10, 10), + 40, + ThreadLocalRandom.current().nextInt(-10, 10) + ); + } + + private void moveTowards(@NotNull Point direction, double speed) { + final Pos position = getPosition(); + final double dx = direction.x() - position.x(); + final double dy = direction.y() - position.y(); + final double dz = direction.z() - position.z(); + // the purpose of these few lines is to slow down entities when they reach their destination + final double distSquared = dx * dx + dy * dy + dz * dz; + if (speed > distSquared) { + speed = distSquared; + } + final double radians = Math.atan2(dz, dx); + final double speedX = Math.cos(radians) * speed; + final double speedY = dy * speed; + final double speedZ = Math.sin(radians) * speed; + final float yaw = PositionUtils.getLookYaw(dx, dz); + final float pitch = PositionUtils.getLookPitch(dx, dy, dz); + + // Prevent ghosting + final var physicsResult = CollisionUtils.handlePhysics(this, new Vec(speedX, speedY, speedZ)); + refreshPosition(Pos.fromPoint(physicsResult.newPosition()).withView(yaw, pitch)); + } +} + diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java index c32754b8..c23c3127 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java @@ -5,6 +5,7 @@ import com.mattworzala.debug.shape.Box; import com.mattworzala.debug.shape.Line; import com.mattworzala.debug.shape.OutlineBox; +import net.minestom.server.MinecraftServer; import net.minestom.server.attribute.Attribute; import net.minestom.server.collision.CollisionUtils; import net.minestom.server.coordinate.Point; @@ -14,7 +15,10 @@ import net.minestom.server.entity.LivingEntity; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; +import net.minestom.server.network.packet.server.play.EntityHeadLookPacket; +import net.minestom.server.network.packet.server.play.EntityRotationPacket; import net.minestom.server.utils.NamespaceID; +import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.position.PositionUtils; import net.minestom.server.utils.time.Cooldown; import net.minestom.server.utils.time.TimeUnit; @@ -177,6 +181,11 @@ private void sendDebugData() { .color(0xFFFFFFFF) .layer(Layer.TOP) .build()); + EntityHeadLookPacket headLook = new EntityHeadLookPacket(entity.getEntityId(), entity.getPosition().yaw()); + PacketUtils.sendGroupedPacket(entity.getViewers(), headLook); + EntityRotationPacket rotation = new EntityRotationPacket(entity.getEntityId(), entity.getPosition().yaw(), entity.getPosition().pitch(), true); + PacketUtils.sendGroupedPacket(entity.getViewers(), rotation); + builder.build() .sendTo(entity.getViewersAsAudience()); From 5c6fee69792f112a509b09c92a53875c48c77547 Mon Sep 17 00:00:00 2001 From: mworzala Date: Mon, 5 Sep 2022 08:30:44 -0400 Subject: [PATCH 19/25] minor tweaks and debugging --- .../java/net/hollowcube/server/dev/Main.java | 20 ++++++++----------- .../mmo/entity/HeadRotationZombie.java | 1 + 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java index fd786848..522d93aa 100644 --- a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java +++ b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java @@ -19,24 +19,23 @@ import net.minestom.server.adventure.MinestomAdventure; import net.minestom.server.command.builder.Command; import net.minestom.server.coordinate.Pos; -import net.minestom.server.coordinate.Vec; -import net.minestom.server.entity.Entity; -import net.minestom.server.entity.EntityType; -import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; import net.minestom.server.event.EventNode; import net.minestom.server.event.GlobalEventHandler; import net.minestom.server.event.player.PlayerLoginEvent; import net.minestom.server.event.player.PlayerPacketOutEvent; -import net.minestom.server.event.player.PlayerSpawnEvent; -import net.minestom.server.extras.MojangAuth; import net.minestom.server.instance.Instance; import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.block.Block; +import net.minestom.server.network.packet.server.CachedPacket; +import net.minestom.server.network.packet.server.FramedPacket; +import net.minestom.server.network.packet.server.LazyPacket; +import net.minestom.server.network.packet.server.SendablePacket; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.network.packet.server.*; import net.minestom.server.network.packet.server.play.EntityHeadLookPacket; import net.minestom.server.network.packet.server.play.EntityPositionAndRotationPacket; +import net.minestom.server.network.packet.server.play.EntityRotationPacket; import net.minestom.server.potion.Potion; import net.minestom.server.potion.PotionEffect; import net.minestom.server.utils.NamespaceID; @@ -57,6 +56,7 @@ import unnamed.mmo.damage.DamageProcessor; import unnamed.mmo.data.number.NumberProvider; import unnamed.mmo.entity.HeadRotationZombie; +import unnamed.mmo.logging.LoggerFactory; import unnamed.mmo.entity.UnnamedEntity; import unnamed.mmo.entity.brain.task.*; import unnamed.mmo.item.Item; @@ -87,18 +87,14 @@ public static void main(String[] args) { MinecraftServer server = MinecraftServer.init(); - MojangAuth.init(); - MinecraftServer.getConnectionManager().setPlayerProvider(PlayerImpl::new); - InstanceManager instanceManager = MinecraftServer.getInstanceManager(); - - Instance instance = new TickTrackingInstance(UUID.randomUUID(), DimensionType.OVERWORLD); - instanceManager.registerInstance(instance); + Instance instance = instanceManager.createInstanceContainer(); instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); GlobalEventHandler eventHandler = MinecraftServer.getGlobalEventHandler(); eventHandler.addListener(PlayerLoginEvent.class, event -> { final Player player = event.getPlayer(); + player.setPermissionLevel(2); event.setSpawningInstance(instance); player.setRespawnPoint(new Pos(0, 42, 0)); }); diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/HeadRotationZombie.java b/modules/entity/src/main/java/unnamed/mmo/entity/HeadRotationZombie.java index f6704950..7c88ee61 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/HeadRotationZombie.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/HeadRotationZombie.java @@ -16,6 +16,7 @@ public class HeadRotationZombie extends LivingEntity { public HeadRotationZombie() { super(EntityType.ZOMBIE); + onGround = false; } @Override From b3fb1a9a7366c82585b105dbb3e66938b2a7b1d0 Mon Sep 17 00:00:00 2001 From: mworzala Date: Mon, 5 Sep 2022 09:09:40 -0400 Subject: [PATCH 20/25] remove debugging stuff --- .../java/unnamed/mmo/server/dev/Main.java | 163 ++++++++++++++++++ modules/entity/build.gradle.kts | 1 - modules/entity/libs/enodia-pf-1.0.1.jar | Bin 74586 -> 0 bytes .../brain/navigator/EnodiaNavigator.java | 68 -------- .../mmo/entity/brain/navigator/Navigator.java | 4 - .../mmo/entity/brain/TestEnodiaPF.java | 72 -------- .../mmo/entity/brain/navigator/TestUtil.java | 1 - 7 files changed, 163 insertions(+), 146 deletions(-) create mode 100644 modules/development/src/main/java/unnamed/mmo/server/dev/Main.java delete mode 100644 modules/entity/libs/enodia-pf-1.0.1.jar delete mode 100644 modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/EnodiaNavigator.java delete mode 100644 modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java diff --git a/modules/development/src/main/java/unnamed/mmo/server/dev/Main.java b/modules/development/src/main/java/unnamed/mmo/server/dev/Main.java new file mode 100644 index 00000000..f248bb10 --- /dev/null +++ b/modules/development/src/main/java/unnamed/mmo/server/dev/Main.java @@ -0,0 +1,163 @@ +package unnamed.mmo.server.dev; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.mattworzala.debug.DebugMessage; +import com.mojang.serialization.JsonOps; +import net.minestom.server.MinecraftServer; +import net.minestom.server.command.builder.Command; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.event.GlobalEventHandler; +import net.minestom.server.event.player.PlayerLoginEvent; +import net.minestom.server.event.player.PlayerSpawnEvent; +import net.minestom.server.extras.MojangAuth; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.InstanceManager; +import net.minestom.server.instance.block.Block; +import net.minestom.server.potion.Potion; +import net.minestom.server.potion.PotionEffect; +import net.minestom.server.world.DimensionType; +import unnamed.mmo.blocks.BlockInteracter; +import unnamed.mmo.blocks.ore.Ore; +import unnamed.mmo.chat.ChatManager; +import unnamed.mmo.chat.storage.ChatStorage; +import unnamed.mmo.command.BaseCommandRegister; +import unnamed.mmo.damage.DamageProcessor; +import unnamed.mmo.entity.UnnamedEntity; +import unnamed.mmo.entity.brain.task.Task; +import unnamed.mmo.item.Item; +import unnamed.mmo.item.ItemManager; +import unnamed.mmo.item.entity.OwnedItemEntity; +import unnamed.mmo.player.PlayerImpl; +import unnamed.mmo.quest.QuestFacet; +import unnamed.mmo.server.dev.tool.DebugToolManager; +import unnamed.mmo.server.instance.TickTrackingInstance; + +import java.util.UUID; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; + +public class Main { + + public static void main(String[] args) { + System.setProperty("minestom.terminal.disabled", "true"); +// System.setProperty("minestom.viewable-packet", "false"); + + MinecraftServer server = MinecraftServer.init(); + + InstanceManager instanceManager = MinecraftServer.getInstanceManager(); + Instance instance = new TickTrackingInstance(UUID.randomUUID(), DimensionType.OVERWORLD); + instanceManager.registerInstance(instance); + instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); + + // + GlobalEventHandler eventHandler = MinecraftServer.getGlobalEventHandler(); + eventHandler.addListener(PlayerLoginEvent.class, event -> { + final Player player = event.getPlayer(); + event.setSpawningInstance(instance); + player.setRespawnPoint(new Pos(0, 40, 0)); + }); + + MojangAuth.init(); + MinecraftServer.getConnectionManager().setPlayerProvider(PlayerImpl::new); + + eventHandler.addListener(PlayerSpawnEvent.class, event -> { + final Player player = event.getPlayer(); + player.setGameMode(GameMode.SURVIVAL); + player.setPermissionLevel(4); + player.setAllowFlying(true); + + // Testing + event.getSpawnInstance().setBlock(5, 43, 5, Ore.fromNamespaceId("unnamed:gold_ore").asBlock()); + event.getSpawnInstance().setBlock(4, 43, 5, Ore.fromNamespaceId("unnamed:diamond_ore").asBlock()); + player.getInventory().addItemStack(Item.fromNamespaceId("unnamed:diamond_pickaxe").asItemStack()); + + //todo this needs to be done elsewhere + player.addEffect(new Potion(PotionEffect.MINING_FATIGUE, (byte) -1, Short.MAX_VALUE, (byte) 0x0)); + + //todo a command for this + player.getInventory().addItemStack(DebugToolManager.createTool("unnamed:hello")); + + + //todo test entity +// JsonElement json = JsonParser.parseString(""" +// { +// "type": "unnamed:selector", +// "children": { +// "q.has_target": { +// "type": "unnamed:follow_target" +// }, +// "": { +// "type": "unnamed:sequence", +// "children": [ +// { +// "type": "unnamed:wander_in_region" +// }, +// { +// "type": "unnamed:idle", +// "time": 5 +// } +// ], +// +// "canInterrupt": true +// } +// } +// }"""); + JsonElement json = JsonParser.parseString(""" + { + "type": "unnamed:sequence", + "children": [ + { + "type": "unnamed:wander_in_region" + }, + { + "type": "unnamed:idle", + "time": 20 + } + ] + }"""); + Task task = JsonOps.INSTANCE.withDecoder(Task.Spec.CODEC) + .apply(json).getOrThrow(false, System.err::println).getFirst().create(); + UnnamedEntity entity = new UnnamedEntity(task); + entity.setInstance(instance, new Pos(0, 40, 0)) + .thenAccept(unused -> System.out.println("Spawned")); + }); + + BaseCommandRegister.registerCommands(); + + Command c = new Command("clear"); + c.setDefaultExecutor((sender, context) -> { + DebugMessage.builder().clear().build().sendTo(sender); + }); + MinecraftServer.getCommandManager().register(c); + + ChatStorage chatStorage = ChatStorage.noop(); + ChatManager chatManager = new ChatManager(chatStorage); + chatManager.hook(MinecraftServer.process()); + + //todo properly implement a config system & use facets better + ItemManager itemManager = new ItemManager(); + itemManager.hook(MinecraftServer.process()); + OwnedItemEntity.Handler itemEntityHandler = new OwnedItemEntity.Handler(); + itemEntityHandler.hook(MinecraftServer.process()); + + //todo stupid facet implementation + DebugToolManager debugToolManager = new DebugToolManager(); + debugToolManager.hook(MinecraftServer.process()); + + QuestFacet questFacet = new QuestFacet(); + questFacet.hook(MinecraftServer.process()); + + MinecraftServer.getSchedulerManager().buildShutdownTask(() -> + ForkJoinPool.commonPool().awaitQuiescence(10, TimeUnit.SECONDS)); + + BlockInteracter.registerEvents(); + DamageProcessor.init(); + + server.start("0.0.0.0", 25565); + + } + +} diff --git a/modules/entity/build.gradle.kts b/modules/entity/build.gradle.kts index df480752..5612c744 100644 --- a/modules/entity/build.gradle.kts +++ b/modules/entity/build.gradle.kts @@ -4,5 +4,4 @@ plugins { dependencies { implementation(project(":modules:common")) - implementation(files("libs/enodia-pf-1.0.1.jar")) } diff --git a/modules/entity/libs/enodia-pf-1.0.1.jar b/modules/entity/libs/enodia-pf-1.0.1.jar deleted file mode 100644 index b1b2d5b114b6c786db7b5e1bf537a11114ab986f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74586 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

  • --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 ent instanceof LivingEntity ? - ((LivingEntity) ent).getAttributeValue(Attribute.MOVEMENT_SPEED) : - Attribute.MOVEMENT_SPEED.defaultValue(), - (unused1, unused2, unused3) -> true - ); - - private static final PathfindingCapabilities DEFAULT_CAPABILITIES = PathfindingCapabilities.Companion.getDefault(); - private static final MovementImportance DEFAULT_IMPORTANCE = MovementImportance.Companion.getUNIMPORTANT(); - - private final Entity entity; - private MovementProcessor enodia; - - EnodiaNavigator(@NotNull Entity entity) { - this.entity = entity; - this.enodia = null; - } - - @Override - public void setInstance(@NotNull Instance instance) { - enodia = MOVEMENT_HUB.createMovementProcessor(entity, DEFAULT_CAPABILITIES); - } - - @Override - public boolean setPathTo(@Nullable Point point) { - if (point == null) { - this.enodia.stop(true); - return true; - } - - // Last parameter here is the max distance from the value. - return enodia.goTo(point, DEFAULT_IMPORTANCE, 0.5f); - } - - @Override - public boolean isActive() { - if (enodia == null) { - return false; - } - return enodia.isActive(); - } - - @Override - public void tick(long time) { - if (enodia != null) { - enodia.tick(time); - } - } -} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java index 05dfa7ba..4fe35168 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java @@ -11,10 +11,6 @@ public interface Navigator { - static @NotNull Navigator enodia(@NotNull Entity entity) { - return new EnodiaNavigator(entity); - } - static @NotNull Navigator hydrazine(@NotNull Entity entity) { return new HydrazineNavigator(entity); } diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java deleted file mode 100644 index 5f1d4fbe..00000000 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/TestEnodiaPF.java +++ /dev/null @@ -1,72 +0,0 @@ -package unnamed.mmo.entity.brain; - -import net.minestom.server.attribute.Attribute; -import net.minestom.server.coordinate.Pos; -import net.minestom.server.coordinate.Vec; -import net.minestom.server.entity.Entity; -import net.minestom.server.entity.EntityType; -import net.minestom.server.test.Env; -import net.minestom.server.test.EnvTest; -import org.junit.jupiter.api.Test; -import sexy.kostya.enodia.EnodiaPF; -import sexy.kostya.enodia.movement.MovementProcessor; -import sexy.kostya.enodia.movement.importance.MovementImportance; -import sexy.kostya.enodia.pathfinding.PathfindingCapabilities; - -import java.time.Duration; - -import static com.google.common.truth.Truth.assertThat; - -@EnvTest -public class TestEnodiaPF { - - @Test - public void test(Env env) { - var enodia = EnodiaPF.Companion.forImmutableWorlds(); - var hub = enodia.initializeMovementProcessingHub( - 2, - 5, - ent -> Attribute.MOVEMENT_SPEED.defaultValue(), - (a, b, c) -> true - ); - - var instance = env.createFlatInstance(); - var player = env.createPlayer(instance, new Pos(0, 42, 0)); - - var capabilities = PathfindingCapabilities.Companion.getDefault(); - var entity = new EnodiaEntity(); - entity.setInstance(instance, new Pos(5, 42, 5)) - .thenAccept(unused -> { - entity.movementProcessor = hub.createMovementProcessor(entity, capabilities); - - entity.movementProcessor.goTo(player, MovementImportance.Companion.getUNIMPORTANT(), 2); - }) - .join(); - - boolean result = env.tickWhile(() -> { - System.out.println(entity.getPosition()); -// return !entity.getPosition().sameBlock(new Vec(0, 40, 0)); - return entity.movementProcessor.isActive(); - }, Duration.ofMillis(100)); - - assertThat(result).isTrue(); - assertThat(entity.movementProcessor.isActive()).isFalse(); - } - - static class EnodiaEntity extends Entity { - - public EnodiaEntity() { - super(EntityType.ZOMBIE); - } - - MovementProcessor movementProcessor = null; - - @Override - public void update(long time) { - super.update(time); - if (movementProcessor != null) { - movementProcessor.tick(time); - } - } - } -} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java index 6c59a9ad..4256618c 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java +++ b/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java @@ -11,7 +11,6 @@ public class TestUtil { public static Stream navigators() { return Stream.of( -// Arguments.of("enodia", (Function) EnodiaNavigator::new), // Arguments.of("hydrazine", (Function) HydrazineNavigator::new), // Arguments.of("custom", (Function) CustomNavigator::new), Arguments.of("motion", (Function) MotionNavigator::new) From 90971941e73dceae0d01f414fec7ff844960518f Mon Sep 17 00:00:00 2001 From: mworzala Date: Mon, 5 Sep 2022 15:48:26 -0400 Subject: [PATCH 21/25] examples --- .../java/unnamed/mmo/server/dev/Main.java | 65 +++++++++--------- modules/entity/README.md | 66 +++++++++++++++++++ .../unnamed/mmo/entity/UnnamedEntity.java | 6 +- .../src/main/resources/data/behaviors.json | 32 +++++++++ .../src/main/resources/data/entities.json | 6 ++ .../src/main/resources/entity_reference.json5 | 0 6 files changed, 141 insertions(+), 34 deletions(-) create mode 100644 modules/entity/README.md create mode 100644 modules/entity/src/main/resources/data/behaviors.json create mode 100644 modules/entity/src/main/resources/data/entities.json create mode 100644 modules/entity/src/main/resources/entity_reference.json5 diff --git a/modules/development/src/main/java/unnamed/mmo/server/dev/Main.java b/modules/development/src/main/java/unnamed/mmo/server/dev/Main.java index f248bb10..19962488 100644 --- a/modules/development/src/main/java/unnamed/mmo/server/dev/Main.java +++ b/modules/development/src/main/java/unnamed/mmo/server/dev/Main.java @@ -26,6 +26,7 @@ import unnamed.mmo.command.BaseCommandRegister; import unnamed.mmo.damage.DamageProcessor; import unnamed.mmo.entity.UnnamedEntity; +import unnamed.mmo.entity.brain.task.SelectorTask; import unnamed.mmo.entity.brain.task.Task; import unnamed.mmo.item.Item; import unnamed.mmo.item.ItemManager; @@ -82,43 +83,43 @@ public static void main(String[] args) { //todo test entity -// JsonElement json = JsonParser.parseString(""" -// { -// "type": "unnamed:selector", -// "children": { -// "q.has_target": { -// "type": "unnamed:follow_target" -// }, -// "": { -// "type": "unnamed:sequence", -// "children": [ -// { -// "type": "unnamed:wander_in_region" -// }, -// { -// "type": "unnamed:idle", -// "time": 5 -// } -// ], -// -// "canInterrupt": true -// } -// } -// }"""); JsonElement json = JsonParser.parseString(""" { - "type": "unnamed:sequence", - "children": [ - { - "type": "unnamed:wander_in_region" + "type": "unnamed:selector", + "children": { + "q.has_target": { + "type": "unnamed:follow_target" }, - { - "type": "unnamed:idle", - "time": 20 + "": { + "type": "unnamed:sequence", + "children": [ + { + "type": "unnamed:wander_in_region" + }, + { + "type": "unnamed:idle", + "time": 5 + } + ], + + "canInterrupt": true } - ] + } }"""); - Task task = JsonOps.INSTANCE.withDecoder(Task.Spec.CODEC) +// JsonElement json = JsonParser.parseString(""" +// { +// "type": "unnamed:sequence", +// "children": [ +// { +// "type": "unnamed:wander_in_region" +// }, +// { +// "type": "unnamed:idle", +// "time": 20 +// } +// ] +// }"""); + Task task = JsonOps.INSTANCE.withDecoder(SelectorTask.Spec.CODEC) .apply(json).getOrThrow(false, System.err::println).getFirst().create(); UnnamedEntity entity = new UnnamedEntity(task); entity.setInstance(instance, new Pos(0, 40, 0)) diff --git a/modules/entity/README.md b/modules/entity/README.md new file mode 100644 index 00000000..6c0044d9 --- /dev/null +++ b/modules/entity/README.md @@ -0,0 +1,66 @@ +# Entities +Module for defining entities as well as their associated behavior. + +## Entity Definition +```json5 +{ + "namespace": "unnamed:test", + + // The model to use. For now this must be a vanilla entity type. + "model": "minecraft:pig", + // A reference to a behavior file + "behavior": "unnamed:test_behavior", + // (optional) The navigator to use for this entity. If not specified, the land navigator will be used. + "navigator": "minecraft:land", + + // (optional) The loot table to use when the entity dies + "loot_table": "unnamed:test_loot", + + // Stats + //todo need to be able to specify things like walk speed, jump height, etc. These are navigator parameters i suppose +} +``` + +## Behavior Definition +```json5 +{ + "namespace": "test_behavior", + + // Root behavior node + "type": "sequence", + "children": [/* ... */], + // (optional) If true, the task may be interrupted during execution by a parent sequence. Defaults to false. + "canInterrupt": false, +} +``` + +A behavior node is a JSON object with a type and some set of properties defined on the node itself. The basic primitives +are `sequence`s and `selector`s. They are documented below: + +```json5 +{ + // Sequence is a set of tasks to perform in order. If any task fails, the sequence fails. + "type": "unnamed:sequence", + // Each child is executed in order. If the sequence may be interrupted then any child task may be interrupted. + "children": [/* ... */], +} +``` + +```json5 +{ + // Selector is a set of tasks which will be performed based on their order and condition. + "type": "unnamed:selector", + // An array of stimuli definitions which will be active as long as this selector is active. This can be used for + // performance reasons (e.g. do not tick a stimuli source if you do not have to), but should not be used if there + // are two conflicting stimuli sources (e.g. do not have two targeting stimuli nested within each other). + "stimuli": [/* ... */], + // A set of mql expressions and tasks. Each expression is evaluated in order, executing the first task to completion. + // If the selected task may not be interrupted, it is executed to completion. If it may be interrupted, the + // conditions will be continuously evaluated, and the selected task will change if an earlier condition passes. + // An empty mql expression will always evaluate to true. + "children": { + "q.has_target": {/* ... */}, + "": {/* ... */}, + } +} +``` diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java b/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java index 64a0c659..201d3b09 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java +++ b/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java @@ -19,8 +19,10 @@ public class UnnamedEntity extends LivingEntity { private final Brain brain; public UnnamedEntity(Task task) { - super(EntityType.SLIME); - ((SlimeMeta) getEntityMeta()).setSize(2); + super(EntityType.ZOMBIE); + if (getEntityMeta() instanceof SlimeMeta slimeMeta) { + slimeMeta.setSize(1); + } brain = new SingleTaskBrain(this, task); } diff --git a/modules/entity/src/main/resources/data/behaviors.json b/modules/entity/src/main/resources/data/behaviors.json new file mode 100644 index 00000000..2f46c2f2 --- /dev/null +++ b/modules/entity/src/main/resources/data/behaviors.json @@ -0,0 +1,32 @@ +[ + { + "namespace": "hostile_generic", + "type": "unnamed:selector", + "stimuli": [ + { + "type": "unnamed:target_entity_in_range", + "entity_type": "player", + "range": 16, + "line_of_sight": true + } + ], + "children": { + "q.has_target": { + "type": "unnamed:follow_target" + }, + "": { + "type": "unnamed:sequence", + "children": [ + { + "type": "unnamed:idle_time", + "time": { "min": 20, "max": 60 } + }, + { + "type": "unnamed:wander_in_region" + } + ], + "can_interrupt": true + } + } + } +] \ No newline at end of file diff --git a/modules/entity/src/main/resources/data/entities.json b/modules/entity/src/main/resources/data/entities.json new file mode 100644 index 00000000..78c0de0e --- /dev/null +++ b/modules/entity/src/main/resources/data/entities.json @@ -0,0 +1,6 @@ +[ + { + "id": "unnamed:slime", + "behavior": "unnamed:slime_generic" + } +] \ No newline at end of file diff --git a/modules/entity/src/main/resources/entity_reference.json5 b/modules/entity/src/main/resources/entity_reference.json5 new file mode 100644 index 00000000..e69de29b From 406d0cc87671587bf3c812d5ace7cff7b934c028 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 17 Sep 2022 08:23:46 -0400 Subject: [PATCH 22/25] rebase fixes --- .../java/net/hollowcube/server/dev/Main.java | 105 +---------- .../java/unnamed/mmo/server/dev/Main.java | 164 ------------------ .../entity/EntityMqlQueryContext.java | 12 +- .../entity/HeadRotationZombie.java | 2 +- .../hollowcube}/entity/UnnamedEntity.java | 8 +- .../hollowcube}/entity/brain/Brain.java | 4 +- .../entity/brain/SingleTaskBrain.java | 6 +- .../brain/navigator/CustomNavigator.java | 4 +- .../brain/navigator/HydrazineNavigator.java | 4 +- .../entity/brain/navigator/Navigator.java | 7 +- .../stimuli/NearbyEntityStimuliSource.java | 6 +- .../entity/brain/stimuli/StimuliSource.java | 4 +- .../entity/brain/task/AbstractTask.java | 4 +- .../entity/brain/task/FollowTargetTask.java | 6 +- .../entity/brain/task/IdleTask.java | 8 +- .../entity/brain/task/SelectorTask.java | 16 +- .../entity/brain/task/SequenceTask.java | 6 +- .../hollowcube}/entity/brain/task/Task.java | 8 +- .../entity/brain/task/WanderInRegionTask.java | 4 +- .../entity/motion/MotionNavigator.java | 5 +- .../entity/motion/MotionNavigatorSlime.java | 6 +- .../hollowcube}/entity/motion/Path.java | 2 +- .../entity/motion/PathGenerator.java | 4 +- .../entity/motion/PathOptimizer.java | 5 +- .../hollowcube}/entity/motion/Pathfinder.java | 2 +- .../entity/motion/util/PhysicsUtil.java | 2 +- .../entity/pathfinding/PFNavigator.java | 2 +- .../pathfinding/PFNeighborSupplier.java | 2 +- .../entity/pathfinding/PFNode.java | 2 +- .../entity/pathfinding/PFPath.java | 2 +- .../entity/pathfinding/PFPathGenerator.java | 3 +- .../entity/pathfinding/PFPathOptimizer.java | 2 +- .../mmo => net/hollowcube}/mql/MqlScript.java | 16 +- .../mmo => net/hollowcube}/mql/README.md | 0 .../hollowcube}/mql/parser/MqlLexer.java | 2 +- .../hollowcube}/mql/parser/MqlParseError.java | 2 +- .../hollowcube}/mql/parser/MqlParser.java | 5 +- .../hollowcube}/mql/parser/MqlToken.java | 2 +- .../mql/runtime/MqlRuntimeError.java | 2 +- .../hollowcube}/mql/runtime/MqlScope.java | 4 +- .../hollowcube}/mql/tree/MqlAccessExpr.java | 8 +- .../hollowcube}/mql/tree/MqlBinaryExpr.java | 8 +- .../hollowcube}/mql/tree/MqlExpr.java | 6 +- .../hollowcube}/mql/tree/MqlIdentExpr.java | 7 +- .../hollowcube}/mql/tree/MqlNumberExpr.java | 8 +- .../hollowcube}/mql/tree/MqlPrinter.java | 2 +- .../hollowcube}/mql/tree/MqlVisitor.java | 2 +- .../hollowcube}/mql/value/MqlCallable.java | 2 +- .../hollowcube}/mql/value/MqlHolder.java | 2 +- .../hollowcube}/mql/value/MqlIdentValue.java | 2 +- .../hollowcube}/mql/value/MqlNull.java | 2 +- .../hollowcube/mql/value/MqlNumberValue.java | 9 + .../hollowcube}/mql/value/MqlValue.java | 4 +- .../unnamed/mmo/mql/value/MqlNumberValue.java | 4 - .../TestNavigatorBasicIntegration.java | 6 +- .../entity/brain/navigator/TestUtil.java | 4 +- .../task/TestFollowTargetTaskIntegration.java | 2 +- .../entity/brain/task/TestIdleTask.java | 6 +- .../entity/brain/task/TestSequenceTask.java | 18 +- .../entity/brain/task/test/MockBrain.java | 6 +- .../entity/brain/task/test/MockTask.java | 7 +- .../entity/brain/task/test/TaskSubject.java | 4 +- .../entity/motion/TestPathGeneratorLand.java | 4 +- .../entity/motion/TestPathGeneratorWater.java | 4 +- .../motion/TestPathOptimizerStringPull.java | 4 +- .../entity/motion/TestPathfinderAStar.java | 4 +- .../entity/motion/util/TestPhysicsUtil.java | 4 +- .../mmo => net/hollowcube}/mql/MqlTest.java | 14 +- .../hollowcube}/mql/parser/TestMqlLexer.java | 2 +- .../hollowcube}/mql/parser/TestMqlParser.java | 4 +- .../hollowcube}/test/MockBlockGetter.java | 2 +- 71 files changed, 171 insertions(+), 440 deletions(-) delete mode 100644 modules/development/src/main/java/unnamed/mmo/server/dev/Main.java rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/EntityMqlQueryContext.java (75%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/HeadRotationZombie.java (98%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/UnnamedEntity.java (89%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/Brain.java (85%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/SingleTaskBrain.java (93%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/navigator/CustomNavigator.java (86%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/navigator/HydrazineNavigator.java (87%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/navigator/Navigator.java (82%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/stimuli/NearbyEntityStimuliSource.java (88%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/stimuli/StimuliSource.java (58%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/task/AbstractTask.java (84%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/task/FollowTargetTask.java (92%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/task/IdleTask.java (88%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/task/SelectorTask.java (91%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/task/SequenceTask.java (94%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/task/Task.java (87%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/brain/task/WanderInRegionTask.java (94%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/motion/MotionNavigator.java (98%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/motion/MotionNavigatorSlime.java (96%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/motion/Path.java (88%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/motion/PathGenerator.java (96%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/motion/PathOptimizer.java (90%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/motion/Pathfinder.java (98%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/motion/util/PhysicsUtil.java (99%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/pathfinding/PFNavigator.java (99%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/pathfinding/PFNeighborSupplier.java (98%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/pathfinding/PFNode.java (96%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/pathfinding/PFPath.java (95%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/pathfinding/PFPathGenerator.java (97%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/entity/pathfinding/PFPathOptimizer.java (98%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/MqlScript.java (79%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/README.md (100%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/parser/MqlLexer.java (98%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/parser/MqlParseError.java (84%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/parser/MqlParser.java (95%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/parser/MqlToken.java (83%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/runtime/MqlRuntimeError.java (83%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/runtime/MqlScope.java (67%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/tree/MqlAccessExpr.java (74%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/tree/MqlBinaryExpr.java (80%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/tree/MqlExpr.java (68%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/tree/MqlIdentExpr.java (70%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/tree/MqlNumberExpr.java (73%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/tree/MqlPrinter.java (96%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/tree/MqlVisitor.java (94%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/value/MqlCallable.java (86%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/value/MqlHolder.java (81%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/value/MqlIdentValue.java (84%) rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/value/MqlNull.java (79%) create mode 100644 modules/entity/src/main/java/net/hollowcube/mql/value/MqlNumberValue.java rename modules/entity/src/main/java/{unnamed/mmo => net/hollowcube}/mql/value/MqlValue.java (89%) delete mode 100644 modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNumberValue.java rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/entity/brain/navigator/TestNavigatorBasicIntegration.java (92%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/entity/brain/navigator/TestUtil.java (85%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/entity/brain/task/TestFollowTargetTaskIntegration.java (87%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/entity/brain/task/TestIdleTask.java (82%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/entity/brain/task/TestSequenceTask.java (75%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/entity/brain/task/test/MockBrain.java (77%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/entity/brain/task/test/MockTask.java (69%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/entity/brain/task/test/TaskSubject.java (90%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/entity/motion/TestPathGeneratorLand.java (97%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/entity/motion/TestPathGeneratorWater.java (96%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/entity/motion/TestPathOptimizerStringPull.java (97%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/entity/motion/TestPathfinderAStar.java (96%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/entity/motion/util/TestPhysicsUtil.java (97%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/mql/MqlTest.java (78%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/mql/parser/TestMqlLexer.java (97%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/mql/parser/TestMqlParser.java (94%) rename modules/entity/src/test/java/{unnamed/mmo => net/hollowcube}/test/MockBlockGetter.java (98%) diff --git a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java index 522d93aa..c5a1d1fa 100644 --- a/modules/development/src/main/java/net/hollowcube/server/dev/Main.java +++ b/modules/development/src/main/java/net/hollowcube/server/dev/Main.java @@ -2,44 +2,23 @@ import net.hollowcube.item.crafting.RecipeList; import net.hollowcube.item.crafting.ToolCraftingInventory; -import net.hollowcube.player.PlayerImpl; import net.hollowcube.server.dev.tool.DebugToolManager; -import net.hollowcube.server.instance.TickTrackingInstance; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; -import com.mattworzala.debug.DebugMessage; -import com.mattworzala.debug.Layer; -import com.mattworzala.debug.shape.Line; -import com.mattworzala.debug.shape.Text; -import com.mojang.serialization.JsonOps; -import net.kyori.adventure.audience.Audience; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerProcess; import net.minestom.server.command.builder.Command; -import net.minestom.server.adventure.MinestomAdventure; -import net.minestom.server.command.builder.Command; import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; import net.minestom.server.event.EventNode; import net.minestom.server.event.GlobalEventHandler; import net.minestom.server.event.player.PlayerLoginEvent; -import net.minestom.server.event.player.PlayerPacketOutEvent; +import net.minestom.server.event.player.PlayerSpawnEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.block.Block; -import net.minestom.server.network.packet.server.CachedPacket; -import net.minestom.server.network.packet.server.FramedPacket; -import net.minestom.server.network.packet.server.LazyPacket; -import net.minestom.server.network.packet.server.SendablePacket; import net.minestom.server.instance.block.BlockHandler; -import net.minestom.server.network.packet.server.*; -import net.minestom.server.network.packet.server.play.EntityHeadLookPacket; -import net.minestom.server.network.packet.server.play.EntityPositionAndRotationPacket; -import net.minestom.server.network.packet.server.play.EntityRotationPacket; import net.minestom.server.potion.Potion; import net.minestom.server.potion.PotionEffect; -import net.minestom.server.utils.NamespaceID; -import net.minestom.server.world.DimensionType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import net.hollowcube.blocks.BlockInteracter; @@ -48,35 +27,11 @@ import net.hollowcube.server.Facet; import net.hollowcube.server.ServerWrapper; import net.hollowcube.server.dev.command.BaseCommandRegister; -import unnamed.mmo.blocks.BlockInteracter; -import unnamed.mmo.blocks.ore.Ore; -import unnamed.mmo.chat.ChatManager; -import unnamed.mmo.chat.storage.ChatStorage; -import unnamed.mmo.command.BaseCommandRegister; -import unnamed.mmo.damage.DamageProcessor; -import unnamed.mmo.data.number.NumberProvider; -import unnamed.mmo.entity.HeadRotationZombie; -import unnamed.mmo.logging.LoggerFactory; -import unnamed.mmo.entity.UnnamedEntity; -import unnamed.mmo.entity.brain.task.*; -import unnamed.mmo.item.Item; -import unnamed.mmo.damage.DamageProcessor; -import unnamed.mmo.item.Item; -import unnamed.mmo.item.ItemManager; -import unnamed.mmo.mql.MqlScript; -import unnamed.mmo.item.entity.OwnedItemEntity; -import unnamed.mmo.player.PlayerImpl; -import unnamed.mmo.quest.QuestFacet; -import unnamed.mmo.server.dev.tool.DebugToolManager; -import unnamed.mmo.server.instance.TickTrackingInstance; - -import java.util.List; + import java.util.HashMap; import java.util.Map; import java.util.ServiceLoader; -import java.util.UUID; import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @@ -115,60 +70,6 @@ public static void main(String[] args) { //todo a command for this player.getInventory().addItemStack(DebugToolManager.createTool("unnamed:hello")); - - //todo test entity -// JsonElement json = JsonParser.parseString(""" -// { -// "type": "unnamed:selector", -// "children": { -// "q.has_target": { -// "type": "unnamed:follow_target" -// }, -// "": { -// "type": "unnamed:sequence", -// "children": [ -// { -// "type": "unnamed:wander_in_region" -// }, -// { -// "type": "unnamed:idle", -// "time": 5 -// } -// ], -// -// "canInterrupt": true -// } -// } -// }"""); -// JsonElement json = JsonParser.parseString(""" -// { -// "type": "unnamed:sequence", -// "children": [ -// { -// "type": "unnamed:wander_in_region" -// }, -// { -// "type": "unnamed:idle", -// "time": 20 -// } -// ] -// }"""); -// Task task = JsonOps.INSTANCE.withDecoder(Task.Spec.CODEC) -// .apply(json).getOrThrow(false, System.err::println).getFirst().create(); -// UnnamedEntity entity = new UnnamedEntity(task); -// entity.setInstance(instance, new Pos(0, 40, 0)) -// .thenAccept(unused -> System.out.println("Spawned")); - -// Entity entity = new Entity(EntityType.ZOMBIE) { -// @Override -// public void tick(long time) { -// super.tick(time); -// -// lookAt(player); -// } -// }; - Entity entity = new HeadRotationZombie(); - entity.setInstance(instance, new Pos(0, 40, 0)); }); BaseCommandRegister.registerCommands(); //todo this should be in a facet? diff --git a/modules/development/src/main/java/unnamed/mmo/server/dev/Main.java b/modules/development/src/main/java/unnamed/mmo/server/dev/Main.java deleted file mode 100644 index 19962488..00000000 --- a/modules/development/src/main/java/unnamed/mmo/server/dev/Main.java +++ /dev/null @@ -1,164 +0,0 @@ -package unnamed.mmo.server.dev; - -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; -import com.mattworzala.debug.DebugMessage; -import com.mojang.serialization.JsonOps; -import net.minestom.server.MinecraftServer; -import net.minestom.server.command.builder.Command; -import net.minestom.server.coordinate.Pos; -import net.minestom.server.entity.GameMode; -import net.minestom.server.entity.Player; -import net.minestom.server.event.GlobalEventHandler; -import net.minestom.server.event.player.PlayerLoginEvent; -import net.minestom.server.event.player.PlayerSpawnEvent; -import net.minestom.server.extras.MojangAuth; -import net.minestom.server.instance.Instance; -import net.minestom.server.instance.InstanceManager; -import net.minestom.server.instance.block.Block; -import net.minestom.server.potion.Potion; -import net.minestom.server.potion.PotionEffect; -import net.minestom.server.world.DimensionType; -import unnamed.mmo.blocks.BlockInteracter; -import unnamed.mmo.blocks.ore.Ore; -import unnamed.mmo.chat.ChatManager; -import unnamed.mmo.chat.storage.ChatStorage; -import unnamed.mmo.command.BaseCommandRegister; -import unnamed.mmo.damage.DamageProcessor; -import unnamed.mmo.entity.UnnamedEntity; -import unnamed.mmo.entity.brain.task.SelectorTask; -import unnamed.mmo.entity.brain.task.Task; -import unnamed.mmo.item.Item; -import unnamed.mmo.item.ItemManager; -import unnamed.mmo.item.entity.OwnedItemEntity; -import unnamed.mmo.player.PlayerImpl; -import unnamed.mmo.quest.QuestFacet; -import unnamed.mmo.server.dev.tool.DebugToolManager; -import unnamed.mmo.server.instance.TickTrackingInstance; - -import java.util.UUID; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.TimeUnit; - -public class Main { - - public static void main(String[] args) { - System.setProperty("minestom.terminal.disabled", "true"); -// System.setProperty("minestom.viewable-packet", "false"); - - MinecraftServer server = MinecraftServer.init(); - - InstanceManager instanceManager = MinecraftServer.getInstanceManager(); - Instance instance = new TickTrackingInstance(UUID.randomUUID(), DimensionType.OVERWORLD); - instanceManager.registerInstance(instance); - instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); - - // - GlobalEventHandler eventHandler = MinecraftServer.getGlobalEventHandler(); - eventHandler.addListener(PlayerLoginEvent.class, event -> { - final Player player = event.getPlayer(); - event.setSpawningInstance(instance); - player.setRespawnPoint(new Pos(0, 40, 0)); - }); - - MojangAuth.init(); - MinecraftServer.getConnectionManager().setPlayerProvider(PlayerImpl::new); - - eventHandler.addListener(PlayerSpawnEvent.class, event -> { - final Player player = event.getPlayer(); - player.setGameMode(GameMode.SURVIVAL); - player.setPermissionLevel(4); - player.setAllowFlying(true); - - // Testing - event.getSpawnInstance().setBlock(5, 43, 5, Ore.fromNamespaceId("unnamed:gold_ore").asBlock()); - event.getSpawnInstance().setBlock(4, 43, 5, Ore.fromNamespaceId("unnamed:diamond_ore").asBlock()); - player.getInventory().addItemStack(Item.fromNamespaceId("unnamed:diamond_pickaxe").asItemStack()); - - //todo this needs to be done elsewhere - player.addEffect(new Potion(PotionEffect.MINING_FATIGUE, (byte) -1, Short.MAX_VALUE, (byte) 0x0)); - - //todo a command for this - player.getInventory().addItemStack(DebugToolManager.createTool("unnamed:hello")); - - - //todo test entity - JsonElement json = JsonParser.parseString(""" - { - "type": "unnamed:selector", - "children": { - "q.has_target": { - "type": "unnamed:follow_target" - }, - "": { - "type": "unnamed:sequence", - "children": [ - { - "type": "unnamed:wander_in_region" - }, - { - "type": "unnamed:idle", - "time": 5 - } - ], - - "canInterrupt": true - } - } - }"""); -// JsonElement json = JsonParser.parseString(""" -// { -// "type": "unnamed:sequence", -// "children": [ -// { -// "type": "unnamed:wander_in_region" -// }, -// { -// "type": "unnamed:idle", -// "time": 20 -// } -// ] -// }"""); - Task task = JsonOps.INSTANCE.withDecoder(SelectorTask.Spec.CODEC) - .apply(json).getOrThrow(false, System.err::println).getFirst().create(); - UnnamedEntity entity = new UnnamedEntity(task); - entity.setInstance(instance, new Pos(0, 40, 0)) - .thenAccept(unused -> System.out.println("Spawned")); - }); - - BaseCommandRegister.registerCommands(); - - Command c = new Command("clear"); - c.setDefaultExecutor((sender, context) -> { - DebugMessage.builder().clear().build().sendTo(sender); - }); - MinecraftServer.getCommandManager().register(c); - - ChatStorage chatStorage = ChatStorage.noop(); - ChatManager chatManager = new ChatManager(chatStorage); - chatManager.hook(MinecraftServer.process()); - - //todo properly implement a config system & use facets better - ItemManager itemManager = new ItemManager(); - itemManager.hook(MinecraftServer.process()); - OwnedItemEntity.Handler itemEntityHandler = new OwnedItemEntity.Handler(); - itemEntityHandler.hook(MinecraftServer.process()); - - //todo stupid facet implementation - DebugToolManager debugToolManager = new DebugToolManager(); - debugToolManager.hook(MinecraftServer.process()); - - QuestFacet questFacet = new QuestFacet(); - questFacet.hook(MinecraftServer.process()); - - MinecraftServer.getSchedulerManager().buildShutdownTask(() -> - ForkJoinPool.commonPool().awaitQuiescence(10, TimeUnit.SECONDS)); - - BlockInteracter.registerEvents(); - DamageProcessor.init(); - - server.start("0.0.0.0", 25565); - - } - -} diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/EntityMqlQueryContext.java b/modules/entity/src/main/java/net/hollowcube/entity/EntityMqlQueryContext.java similarity index 75% rename from modules/entity/src/main/java/unnamed/mmo/entity/EntityMqlQueryContext.java rename to modules/entity/src/main/java/net/hollowcube/entity/EntityMqlQueryContext.java index d83855d1..23ecac23 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/EntityMqlQueryContext.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/EntityMqlQueryContext.java @@ -1,12 +1,10 @@ -package unnamed.mmo.entity; +package net.hollowcube.entity; -import net.minestom.server.entity.Entity; -import net.minestom.server.entity.LivingEntity; +import net.hollowcube.mql.runtime.MqlRuntimeError; +import net.hollowcube.mql.runtime.MqlScope; +import net.hollowcube.mql.value.MqlHolder; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.mql.runtime.MqlRuntimeError; -import unnamed.mmo.mql.runtime.MqlScope; -import unnamed.mmo.mql.value.MqlHolder; -import unnamed.mmo.mql.value.MqlValue; +import net.hollowcube.mql.value.MqlValue; //todo this whole class sucks, need to work out mql querying better public record EntityMqlQueryContext(@NotNull UnnamedEntity entity) implements MqlScope { diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/HeadRotationZombie.java b/modules/entity/src/main/java/net/hollowcube/entity/HeadRotationZombie.java similarity index 98% rename from modules/entity/src/main/java/unnamed/mmo/entity/HeadRotationZombie.java rename to modules/entity/src/main/java/net/hollowcube/entity/HeadRotationZombie.java index 7c88ee61..a79830bf 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/HeadRotationZombie.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/HeadRotationZombie.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity; +package net.hollowcube.entity; import net.minestom.server.collision.CollisionUtils; import net.minestom.server.coordinate.Point; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java b/modules/entity/src/main/java/net/hollowcube/entity/UnnamedEntity.java similarity index 89% rename from modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java rename to modules/entity/src/main/java/net/hollowcube/entity/UnnamedEntity.java index 201d3b09..d1dc9cf4 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/UnnamedEntity.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/UnnamedEntity.java @@ -1,5 +1,8 @@ -package unnamed.mmo.entity; +package net.hollowcube.entity; +import net.hollowcube.entity.brain.Brain; +import net.hollowcube.entity.brain.SingleTaskBrain; +import net.hollowcube.entity.brain.task.Task; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; @@ -9,9 +12,6 @@ import net.minestom.server.event.entity.EntityAttackEvent; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.brain.Brain; -import unnamed.mmo.entity.brain.SingleTaskBrain; -import unnamed.mmo.entity.brain.task.Task; import java.util.concurrent.CompletableFuture; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/Brain.java similarity index 85% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/Brain.java index 03565723..58c367d3 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/Brain.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/Brain.java @@ -1,11 +1,11 @@ -package unnamed.mmo.entity.brain; +package net.hollowcube.entity.brain; +import net.hollowcube.entity.brain.navigator.Navigator; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import unnamed.mmo.entity.brain.navigator.Navigator; public interface Brain { diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/SingleTaskBrain.java similarity index 93% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/SingleTaskBrain.java index ff9ed879..cc7202db 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/SingleTaskBrain.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/SingleTaskBrain.java @@ -1,12 +1,12 @@ -package unnamed.mmo.entity.brain; +package net.hollowcube.entity.brain; +import net.hollowcube.entity.brain.navigator.Navigator; +import net.hollowcube.entity.brain.task.Task; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import unnamed.mmo.entity.brain.navigator.Navigator; -import unnamed.mmo.entity.brain.task.Task; public class SingleTaskBrain implements Brain { diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/CustomNavigator.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/CustomNavigator.java similarity index 86% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/CustomNavigator.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/CustomNavigator.java index 9a1b4e08..492b6093 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/CustomNavigator.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/CustomNavigator.java @@ -1,9 +1,9 @@ -package unnamed.mmo.entity.brain.navigator; +package net.hollowcube.entity.brain.navigator; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.pathfinding.PFNavigator; +import net.hollowcube.entity.pathfinding.PFNavigator; final class CustomNavigator implements Navigator { private final PFNavigator navigator; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/HydrazineNavigator.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/HydrazineNavigator.java similarity index 87% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/HydrazineNavigator.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/HydrazineNavigator.java index ed8aa3c7..05a69cba 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/HydrazineNavigator.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/HydrazineNavigator.java @@ -1,12 +1,10 @@ -package unnamed.mmo.entity.brain.navigator; +package net.hollowcube.entity.brain.navigator; import com.extollit.gaming.ai.path.HydrazinePathFinder; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; -import net.minestom.server.entity.pathfinding.NavigableEntity; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.brain.Brain; final class HydrazineNavigator implements Navigator { private final net.minestom.server.entity.pathfinding.Navigator hydrazine; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/Navigator.java similarity index 82% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/Navigator.java index 4fe35168..9ee7618d 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/navigator/Navigator.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/Navigator.java @@ -1,13 +1,12 @@ -package unnamed.mmo.entity.brain.navigator; +package net.hollowcube.entity.brain.navigator; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import unnamed.mmo.entity.brain.Brain; -import unnamed.mmo.entity.motion.MotionNavigator; -import unnamed.mmo.entity.motion.MotionNavigatorSlime; +import net.hollowcube.entity.motion.MotionNavigator; +import net.hollowcube.entity.motion.MotionNavigatorSlime; public interface Navigator { diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/NearbyEntityStimuliSource.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/stimuli/NearbyEntityStimuliSource.java similarity index 88% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/NearbyEntityStimuliSource.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/stimuli/NearbyEntityStimuliSource.java index e55b85bf..0113cd98 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/NearbyEntityStimuliSource.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/stimuli/NearbyEntityStimuliSource.java @@ -1,12 +1,12 @@ -package unnamed.mmo.entity.brain.stimuli; +package net.hollowcube.entity.brain.stimuli; import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; import net.minestom.server.instance.EntityTracker; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.brain.Brain; -import unnamed.mmo.entity.brain.SingleTaskBrain; +import net.hollowcube.entity.brain.Brain; +import net.hollowcube.entity.brain.SingleTaskBrain; import java.util.ArrayList; import java.util.List; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/StimuliSource.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/stimuli/StimuliSource.java similarity index 58% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/StimuliSource.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/stimuli/StimuliSource.java index cce5e8fa..7065da8c 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/stimuli/StimuliSource.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/stimuli/StimuliSource.java @@ -1,7 +1,7 @@ -package unnamed.mmo.entity.brain.stimuli; +package net.hollowcube.entity.brain.stimuli; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.brain.Brain; +import net.hollowcube.entity.brain.Brain; public interface StimuliSource { diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/AbstractTask.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/AbstractTask.java similarity index 84% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/task/AbstractTask.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/task/AbstractTask.java index ad608f68..7c9748b6 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/AbstractTask.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/AbstractTask.java @@ -1,7 +1,7 @@ -package unnamed.mmo.entity.brain.task; +package net.hollowcube.entity.brain.task; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.brain.Brain; +import net.hollowcube.entity.brain.Brain; public abstract non-sealed class AbstractTask implements Task { private State state = State.INIT; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/FollowTargetTask.java similarity index 92% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/task/FollowTargetTask.java index f7f590d5..b7163840 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/FollowTargetTask.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/FollowTargetTask.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity.brain.task; +package net.hollowcube.entity.brain.task; import com.google.auto.service.AutoService; import com.mojang.serialization.Codec; @@ -6,8 +6,8 @@ import net.minestom.server.utils.time.Cooldown; import net.minestom.server.utils.time.TimeUnit; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.UnnamedEntity; -import unnamed.mmo.entity.brain.Brain; +import net.hollowcube.entity.UnnamedEntity; +import net.hollowcube.entity.brain.Brain; import java.time.Duration; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/IdleTask.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/IdleTask.java similarity index 88% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/task/IdleTask.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/task/IdleTask.java index acbbc986..28f85f73 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/IdleTask.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/IdleTask.java @@ -1,12 +1,12 @@ -package unnamed.mmo.entity.brain.task; +package net.hollowcube.entity.brain.task; import com.google.auto.service.AutoService; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.hollowcube.data.NumberSource; +import net.hollowcube.data.number.NumberProvider; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.data.NumberSource; -import unnamed.mmo.data.number.NumberProvider; -import unnamed.mmo.entity.brain.Brain; +import net.hollowcube.entity.brain.Brain; public class IdleTask extends AbstractTask { private final Spec spec; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/SelectorTask.java similarity index 91% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/task/SelectorTask.java index fa43fe98..11aafb76 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SelectorTask.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/SelectorTask.java @@ -1,22 +1,22 @@ -package unnamed.mmo.entity.brain.task; +package net.hollowcube.entity.brain.task; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.hollowcube.entity.brain.stimuli.NearbyEntityStimuliSource; +import net.hollowcube.entity.brain.stimuli.StimuliSource; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import unnamed.mmo.entity.EntityMqlQueryContext; -import unnamed.mmo.entity.UnnamedEntity; -import unnamed.mmo.entity.brain.Brain; -import unnamed.mmo.entity.brain.stimuli.NearbyEntityStimuliSource; -import unnamed.mmo.entity.brain.stimuli.StimuliSource; -import unnamed.mmo.mql.MqlScript; +import net.hollowcube.entity.EntityMqlQueryContext; +import net.hollowcube.entity.UnnamedEntity; +import net.hollowcube.entity.brain.Brain; +import net.hollowcube.mql.MqlScript; import java.util.ArrayList; import java.util.List; -import static unnamed.mmo.util.ExtraCodecs.lazy; +import static net.hollowcube.dfu.ExtraCodecs.lazy; public class SelectorTask extends AbstractTask { private static final Logger LOGGER = LoggerFactory.getLogger(SelectorTask.class); diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/SequenceTask.java similarity index 94% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/task/SequenceTask.java index 99db0789..829c6a02 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/SequenceTask.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/SequenceTask.java @@ -1,14 +1,14 @@ -package unnamed.mmo.entity.brain.task; +package net.hollowcube.entity.brain.task; import com.google.auto.service.AutoService; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.brain.Brain; +import net.hollowcube.entity.brain.Brain; import java.util.List; -import static unnamed.mmo.util.ExtraCodecs.lazy; +import static net.hollowcube.dfu.ExtraCodecs.lazy; public class SequenceTask extends AbstractTask { diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/Task.java similarity index 87% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/task/Task.java index b9813d56..c305c7f4 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/Task.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/Task.java @@ -1,10 +1,10 @@ -package unnamed.mmo.entity.brain.task; +package net.hollowcube.entity.brain.task; import com.mojang.serialization.Codec; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.brain.Brain; -import unnamed.mmo.registry.Registry; -import unnamed.mmo.registry.ResourceFactory; +import net.hollowcube.entity.brain.Brain; +import net.hollowcube.registry.Registry; +import net.hollowcube.registry.ResourceFactory; public sealed interface Task permits AbstractTask { diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/WanderInRegionTask.java similarity index 94% rename from modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java rename to modules/entity/src/main/java/net/hollowcube/entity/brain/task/WanderInRegionTask.java index 47dff860..7864707a 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/brain/task/WanderInRegionTask.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/brain/task/WanderInRegionTask.java @@ -1,10 +1,10 @@ -package unnamed.mmo.entity.brain.task; +package net.hollowcube.entity.brain.task; import com.google.auto.service.AutoService; import com.mojang.serialization.Codec; import net.minestom.server.coordinate.Vec; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.brain.Brain; +import net.hollowcube.entity.brain.Brain; import java.util.concurrent.ThreadLocalRandom; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java b/modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigator.java similarity index 98% rename from modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java rename to modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigator.java index c23c3127..01980ad0 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigator.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigator.java @@ -1,11 +1,11 @@ -package unnamed.mmo.entity.motion; +package net.hollowcube.entity.motion; import com.mattworzala.debug.DebugMessage; import com.mattworzala.debug.Layer; import com.mattworzala.debug.shape.Box; import com.mattworzala.debug.shape.Line; import com.mattworzala.debug.shape.OutlineBox; -import net.minestom.server.MinecraftServer; +import net.hollowcube.entity.brain.navigator.Navigator; import net.minestom.server.attribute.Attribute; import net.minestom.server.collision.CollisionUtils; import net.minestom.server.coordinate.Point; @@ -24,7 +24,6 @@ import net.minestom.server.utils.time.TimeUnit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import unnamed.mmo.entity.brain.navigator.Navigator; import java.time.Duration; import java.util.ArrayList; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigatorSlime.java b/modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigatorSlime.java similarity index 96% rename from modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigatorSlime.java rename to modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigatorSlime.java index 06dc0c08..9440ed78 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/MotionNavigatorSlime.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigatorSlime.java @@ -1,7 +1,7 @@ -package unnamed.mmo.entity.motion; +package net.hollowcube.entity.motion; +import net.hollowcube.entity.brain.navigator.Navigator; import net.minestom.server.attribute.Attribute; -import net.minestom.server.collision.CollisionUtils; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; @@ -9,12 +9,10 @@ import net.minestom.server.entity.LivingEntity; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; -import net.minestom.server.utils.position.PositionUtils; import net.minestom.server.utils.time.Cooldown; import net.minestom.server.utils.time.TimeUnit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import unnamed.mmo.entity.brain.navigator.Navigator; import java.time.Duration; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Path.java b/modules/entity/src/main/java/net/hollowcube/entity/motion/Path.java similarity index 88% rename from modules/entity/src/main/java/unnamed/mmo/entity/motion/Path.java rename to modules/entity/src/main/java/net/hollowcube/entity/motion/Path.java index c041f476..7f5bfd01 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Path.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/motion/Path.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity.motion; +package net.hollowcube.entity.motion; import net.minestom.server.coordinate.Point; import org.jetbrains.annotations.NotNull; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java b/modules/entity/src/main/java/net/hollowcube/entity/motion/PathGenerator.java similarity index 96% rename from modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java rename to modules/entity/src/main/java/net/hollowcube/entity/motion/PathGenerator.java index 3de173f2..6708de06 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathGenerator.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/motion/PathGenerator.java @@ -1,12 +1,12 @@ -package unnamed.mmo.entity.motion; +package net.hollowcube.entity.motion; +import net.hollowcube.entity.motion.util.PhysicsUtil; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import net.minestom.server.utils.Direction; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.motion.util.PhysicsUtil; import java.util.ArrayList; import java.util.Collection; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathOptimizer.java b/modules/entity/src/main/java/net/hollowcube/entity/motion/PathOptimizer.java similarity index 90% rename from modules/entity/src/main/java/unnamed/mmo/entity/motion/PathOptimizer.java rename to modules/entity/src/main/java/net/hollowcube/entity/motion/PathOptimizer.java index ffb0bf5e..63fd7560 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/PathOptimizer.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/motion/PathOptimizer.java @@ -1,11 +1,10 @@ -package unnamed.mmo.entity.motion; +package net.hollowcube.entity.motion; +import net.hollowcube.entity.motion.util.PhysicsUtil; import net.minestom.server.collision.BoundingBox; -import net.minestom.server.collision.CollisionUtils; import net.minestom.server.coordinate.Point; import net.minestom.server.instance.block.Block; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.motion.util.PhysicsUtil; import java.util.ArrayList; import java.util.List; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java b/modules/entity/src/main/java/net/hollowcube/entity/motion/Pathfinder.java similarity index 98% rename from modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java rename to modules/entity/src/main/java/net/hollowcube/entity/motion/Pathfinder.java index 5915444e..cd25b2c5 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/Pathfinder.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/motion/Pathfinder.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity.motion; +package net.hollowcube.entity.motion; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java b/modules/entity/src/main/java/net/hollowcube/entity/motion/util/PhysicsUtil.java similarity index 99% rename from modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java rename to modules/entity/src/main/java/net/hollowcube/entity/motion/util/PhysicsUtil.java index a6365412..437880ab 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/motion/util/PhysicsUtil.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/motion/util/PhysicsUtil.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity.motion.util; +package net.hollowcube.entity.motion.util; import net.minestom.server.collision.BoundingBox; import net.minestom.server.collision.CollisionUtils; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNavigator.java similarity index 99% rename from modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java rename to modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNavigator.java index ec5b8447..86d404cd 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNavigator.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNavigator.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity.pathfinding; +package net.hollowcube.entity.pathfinding; import com.mattworzala.debug.DebugMessage; import com.mattworzala.debug.Layer; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNeighborSupplier.java b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNeighborSupplier.java similarity index 98% rename from modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNeighborSupplier.java rename to modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNeighborSupplier.java index 00340323..4ab33591 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNeighborSupplier.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNeighborSupplier.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity.pathfinding; +package net.hollowcube.entity.pathfinding; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNode.java b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNode.java similarity index 96% rename from modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNode.java rename to modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNode.java index d9cd2241..129fdba7 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFNode.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNode.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity.pathfinding; +package net.hollowcube.entity.pathfinding; import net.minestom.server.coordinate.Point; import net.minestom.server.instance.block.Block; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPath.java b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPath.java similarity index 95% rename from modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPath.java rename to modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPath.java index 9adc1cda..39820853 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPath.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPath.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity.pathfinding; +package net.hollowcube.entity.pathfinding; import net.minestom.server.collision.BoundingBox; import net.minestom.server.collision.CollisionUtils; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathGenerator.java similarity index 97% rename from modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java rename to modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathGenerator.java index c91b5d83..542b9d21 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathGenerator.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathGenerator.java @@ -1,8 +1,7 @@ -package unnamed.mmo.entity.pathfinding; +package net.hollowcube.entity.pathfinding; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; -import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.Instance; import java.util.*; diff --git a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathOptimizer.java b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathOptimizer.java similarity index 98% rename from modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathOptimizer.java rename to modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathOptimizer.java index 40196c29..ab5af9b4 100644 --- a/modules/entity/src/main/java/unnamed/mmo/entity/pathfinding/PFPathOptimizer.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathOptimizer.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity.pathfinding; +package net.hollowcube.entity.pathfinding; import net.minestom.server.collision.BoundingBox; import net.minestom.server.collision.CollisionUtils; diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/MqlScript.java b/modules/entity/src/main/java/net/hollowcube/mql/MqlScript.java similarity index 79% rename from modules/entity/src/main/java/unnamed/mmo/mql/MqlScript.java rename to modules/entity/src/main/java/net/hollowcube/mql/MqlScript.java index b1e4b902..452ad168 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/MqlScript.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/MqlScript.java @@ -1,15 +1,15 @@ -package unnamed.mmo.mql; +package net.hollowcube.mql; import com.mojang.datafixers.util.Either; import com.mojang.serialization.Codec; +import net.hollowcube.dfu.DFUUtil; +import net.hollowcube.mql.parser.MqlParser; +import net.hollowcube.mql.runtime.MqlScope; +import net.hollowcube.mql.value.MqlNumberValue; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.mql.parser.MqlParser; -import unnamed.mmo.mql.runtime.MqlScope; -import unnamed.mmo.mql.tree.MqlExpr; -import unnamed.mmo.mql.tree.MqlNumberExpr; -import unnamed.mmo.mql.value.MqlNumberValue; -import unnamed.mmo.mql.value.MqlValue; -import unnamed.mmo.util.DFUUtil; +import net.hollowcube.mql.tree.MqlExpr; +import net.hollowcube.mql.tree.MqlNumberExpr; +import net.hollowcube.mql.value.MqlValue; public record MqlScript(@NotNull MqlExpr expr) { diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/README.md b/modules/entity/src/main/java/net/hollowcube/mql/README.md similarity index 100% rename from modules/entity/src/main/java/unnamed/mmo/mql/README.md rename to modules/entity/src/main/java/net/hollowcube/mql/README.md diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlLexer.java b/modules/entity/src/main/java/net/hollowcube/mql/parser/MqlLexer.java similarity index 98% rename from modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlLexer.java rename to modules/entity/src/main/java/net/hollowcube/mql/parser/MqlLexer.java index 22359a01..ea4a8bce 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlLexer.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/parser/MqlLexer.java @@ -1,4 +1,4 @@ -package unnamed.mmo.mql.parser; +package net.hollowcube.mql.parser; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParseError.java b/modules/entity/src/main/java/net/hollowcube/mql/parser/MqlParseError.java similarity index 84% rename from modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParseError.java rename to modules/entity/src/main/java/net/hollowcube/mql/parser/MqlParseError.java index b4c1b286..7a74b5b0 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParseError.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/parser/MqlParseError.java @@ -1,4 +1,4 @@ -package unnamed.mmo.mql.parser; +package net.hollowcube.mql.parser; import org.jetbrains.annotations.NotNull; diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParser.java b/modules/entity/src/main/java/net/hollowcube/mql/parser/MqlParser.java similarity index 95% rename from modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParser.java rename to modules/entity/src/main/java/net/hollowcube/mql/parser/MqlParser.java index 5fdf9b02..f8974727 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlParser.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/parser/MqlParser.java @@ -1,8 +1,9 @@ -package unnamed.mmo.mql.parser; +package net.hollowcube.mql.parser; +import net.hollowcube.mql.tree.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import unnamed.mmo.mql.tree.*; +import net.hollowcube.mql.tree.*; public class MqlParser { private final MqlLexer lexer; diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlToken.java b/modules/entity/src/main/java/net/hollowcube/mql/parser/MqlToken.java similarity index 83% rename from modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlToken.java rename to modules/entity/src/main/java/net/hollowcube/mql/parser/MqlToken.java index 343a6fba..c2c99eb5 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/parser/MqlToken.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/parser/MqlToken.java @@ -1,4 +1,4 @@ -package unnamed.mmo.mql.parser; +package net.hollowcube.mql.parser; import org.jetbrains.annotations.NotNull; diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlRuntimeError.java b/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlRuntimeError.java similarity index 83% rename from modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlRuntimeError.java rename to modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlRuntimeError.java index 5eb023f9..f7f63e28 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlRuntimeError.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlRuntimeError.java @@ -1,4 +1,4 @@ -package unnamed.mmo.mql.runtime; +package net.hollowcube.mql.runtime; import org.jetbrains.annotations.NotNull; diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlScope.java b/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScope.java similarity index 67% rename from modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlScope.java rename to modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScope.java index 146c949f..564397d1 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/runtime/MqlScope.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScope.java @@ -1,7 +1,7 @@ -package unnamed.mmo.mql.runtime; +package net.hollowcube.mql.runtime; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.mql.value.MqlValue; +import net.hollowcube.mql.value.MqlValue; public interface MqlScope { diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlAccessExpr.java b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlAccessExpr.java similarity index 74% rename from modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlAccessExpr.java rename to modules/entity/src/main/java/net/hollowcube/mql/tree/MqlAccessExpr.java index f868a3b1..9f7712e1 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlAccessExpr.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlAccessExpr.java @@ -1,9 +1,9 @@ -package unnamed.mmo.mql.tree; +package net.hollowcube.mql.tree; +import net.hollowcube.mql.runtime.MqlScope; +import net.hollowcube.mql.value.MqlHolder; +import net.hollowcube.mql.value.MqlValue; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.mql.runtime.MqlScope; -import unnamed.mmo.mql.value.MqlHolder; -import unnamed.mmo.mql.value.MqlValue; public record MqlAccessExpr( @NotNull MqlExpr lhs, diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlBinaryExpr.java b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlBinaryExpr.java similarity index 80% rename from modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlBinaryExpr.java rename to modules/entity/src/main/java/net/hollowcube/mql/tree/MqlBinaryExpr.java index c7e5ef79..7facdb8d 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlBinaryExpr.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlBinaryExpr.java @@ -1,9 +1,9 @@ -package unnamed.mmo.mql.tree; +package net.hollowcube.mql.tree; +import net.hollowcube.mql.runtime.MqlScope; +import net.hollowcube.mql.value.MqlNumberValue; +import net.hollowcube.mql.value.MqlValue; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.mql.runtime.MqlScope; -import unnamed.mmo.mql.value.MqlNumberValue; -import unnamed.mmo.mql.value.MqlValue; public record MqlBinaryExpr( @NotNull Op operator, diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlExpr.java b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlExpr.java similarity index 68% rename from modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlExpr.java rename to modules/entity/src/main/java/net/hollowcube/mql/tree/MqlExpr.java index ce2b40e8..4427cd12 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlExpr.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlExpr.java @@ -1,8 +1,8 @@ -package unnamed.mmo.mql.tree; +package net.hollowcube.mql.tree; +import net.hollowcube.mql.runtime.MqlScope; +import net.hollowcube.mql.value.MqlValue; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.mql.runtime.MqlScope; -import unnamed.mmo.mql.value.MqlValue; public sealed interface MqlExpr permits MqlAccessExpr, MqlBinaryExpr, MqlNumberExpr, MqlIdentExpr { diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlIdentExpr.java b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlIdentExpr.java similarity index 70% rename from modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlIdentExpr.java rename to modules/entity/src/main/java/net/hollowcube/mql/tree/MqlIdentExpr.java index d28da062..7aa668fe 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlIdentExpr.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlIdentExpr.java @@ -1,9 +1,8 @@ -package unnamed.mmo.mql.tree; +package net.hollowcube.mql.tree; +import net.hollowcube.mql.runtime.MqlScope; +import net.hollowcube.mql.value.MqlValue; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.mql.runtime.MqlScope; -import unnamed.mmo.mql.value.MqlIdentValue; -import unnamed.mmo.mql.value.MqlValue; public record MqlIdentExpr(@NotNull String value) implements MqlExpr { diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlNumberExpr.java b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlNumberExpr.java similarity index 73% rename from modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlNumberExpr.java rename to modules/entity/src/main/java/net/hollowcube/mql/tree/MqlNumberExpr.java index 05b0ab8f..73dd847d 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlNumberExpr.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlNumberExpr.java @@ -1,9 +1,9 @@ -package unnamed.mmo.mql.tree; +package net.hollowcube.mql.tree; +import net.hollowcube.mql.runtime.MqlScope; +import net.hollowcube.mql.value.MqlNumberValue; +import net.hollowcube.mql.value.MqlValue; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.mql.runtime.MqlScope; -import unnamed.mmo.mql.value.MqlNumberValue; -import unnamed.mmo.mql.value.MqlValue; public record MqlNumberExpr(@NotNull MqlNumberValue value) implements MqlExpr { diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlPrinter.java b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlPrinter.java similarity index 96% rename from modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlPrinter.java rename to modules/entity/src/main/java/net/hollowcube/mql/tree/MqlPrinter.java index 6b99dbb0..5cb42b6d 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlPrinter.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlPrinter.java @@ -1,4 +1,4 @@ -package unnamed.mmo.mql.tree; +package net.hollowcube.mql.tree; import org.jetbrains.annotations.NotNull; diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlVisitor.java b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlVisitor.java similarity index 94% rename from modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlVisitor.java rename to modules/entity/src/main/java/net/hollowcube/mql/tree/MqlVisitor.java index 625b44ae..4e14a246 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/tree/MqlVisitor.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/tree/MqlVisitor.java @@ -1,4 +1,4 @@ -package unnamed.mmo.mql.tree; +package net.hollowcube.mql.tree; import org.jetbrains.annotations.NotNull; diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlCallable.java b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlCallable.java similarity index 86% rename from modules/entity/src/main/java/unnamed/mmo/mql/value/MqlCallable.java rename to modules/entity/src/main/java/net/hollowcube/mql/value/MqlCallable.java index b9552271..4fffd1c8 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlCallable.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlCallable.java @@ -1,4 +1,4 @@ -package unnamed.mmo.mql.value; +package net.hollowcube.mql.value; import org.jetbrains.annotations.NotNull; diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlHolder.java b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlHolder.java similarity index 81% rename from modules/entity/src/main/java/unnamed/mmo/mql/value/MqlHolder.java rename to modules/entity/src/main/java/net/hollowcube/mql/value/MqlHolder.java index b1e35aae..356fc7f0 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlHolder.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlHolder.java @@ -1,4 +1,4 @@ -package unnamed.mmo.mql.value; +package net.hollowcube.mql.value; import org.jetbrains.annotations.NotNull; diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlIdentValue.java b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlIdentValue.java similarity index 84% rename from modules/entity/src/main/java/unnamed/mmo/mql/value/MqlIdentValue.java rename to modules/entity/src/main/java/net/hollowcube/mql/value/MqlIdentValue.java index 9c2bb4b8..6cf892ee 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlIdentValue.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlIdentValue.java @@ -1,4 +1,4 @@ -package unnamed.mmo.mql.value; +package net.hollowcube.mql.value; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNull.java b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlNull.java similarity index 79% rename from modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNull.java rename to modules/entity/src/main/java/net/hollowcube/mql/value/MqlNull.java index 150e8e62..4708183a 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNull.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlNull.java @@ -1,4 +1,4 @@ -package unnamed.mmo.mql.value; +package net.hollowcube.mql.value; final class MqlNull implements MqlValue { public static final MqlNull INSTANCE = new MqlNull(); diff --git a/modules/entity/src/main/java/net/hollowcube/mql/value/MqlNumberValue.java b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlNumberValue.java new file mode 100644 index 00000000..d0e499af --- /dev/null +++ b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlNumberValue.java @@ -0,0 +1,9 @@ +package net.hollowcube.mql.value; + +public record MqlNumberValue(double value) implements MqlValue { + + @Override + public String toString() { + return String.valueOf(value); + } +} diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlValue.java b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlValue.java similarity index 89% rename from modules/entity/src/main/java/unnamed/mmo/mql/value/MqlValue.java rename to modules/entity/src/main/java/net/hollowcube/mql/value/MqlValue.java index f4eb7c8f..85aba481 100644 --- a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlValue.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlValue.java @@ -1,7 +1,7 @@ -package unnamed.mmo.mql.value; +package net.hollowcube.mql.value; +import net.hollowcube.mql.runtime.MqlRuntimeError; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.mql.runtime.MqlRuntimeError; /** * Mutable marker for any possible mql value. diff --git a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNumberValue.java b/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNumberValue.java deleted file mode 100644 index 3b8d1dfc..00000000 --- a/modules/entity/src/main/java/unnamed/mmo/mql/value/MqlNumberValue.java +++ /dev/null @@ -1,4 +0,0 @@ -package unnamed.mmo.mql.value; - -public record MqlNumberValue(double value) implements MqlValue { -} diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestNavigatorBasicIntegration.java similarity index 92% rename from modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java rename to modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestNavigatorBasicIntegration.java index ccdaf171..09dd2a9a 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestNavigatorBasicIntegration.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestNavigatorBasicIntegration.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity.brain.navigator; +package net.hollowcube.entity.brain.navigator; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; @@ -19,7 +19,7 @@ public class TestNavigatorBasicIntegration { @ParameterizedTest(name = "{0}") - @MethodSource("unnamed.mmo.entity.brain.navigator.TestUtil#navigators") + @MethodSource("net.hollowcube.entity.brain.navigator.TestUtil#navigators") public void testBasicMovement(String name, Function newNavigator, Env env) { var entity = new Entity(EntityType.ZOMBIE); var navigator = newNavigator.apply(entity); @@ -42,7 +42,7 @@ public void testBasicMovement(String name, Function newNaviga } @ParameterizedTest(name = "{0}") - @MethodSource("unnamed.mmo.entity.brain.navigator.TestUtil#navigators") + @MethodSource("net.hollowcube.entity.brain.navigator.TestUtil#navigators") public void testBasicMovementAroundBlock(String name, Function newNavigator, Env env) { var entity = new Entity(EntityType.ZOMBIE); var navigator = newNavigator.apply(entity); diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestUtil.java similarity index 85% rename from modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java rename to modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestUtil.java index 4256618c..eae727a2 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/navigator/TestUtil.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestUtil.java @@ -1,8 +1,8 @@ -package unnamed.mmo.entity.brain.navigator; +package net.hollowcube.entity.brain.navigator; import net.minestom.server.entity.Entity; import org.junit.jupiter.params.provider.Arguments; -import unnamed.mmo.entity.motion.MotionNavigator; +import net.hollowcube.entity.motion.MotionNavigator; import java.util.function.Function; import java.util.stream.Stream; diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestFollowTargetTaskIntegration.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestFollowTargetTaskIntegration.java similarity index 87% rename from modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestFollowTargetTaskIntegration.java rename to modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestFollowTargetTaskIntegration.java index 1492bbd4..dfc72d2c 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestFollowTargetTaskIntegration.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestFollowTargetTaskIntegration.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity.brain.task; +package net.hollowcube.entity.brain.task; import net.minestom.server.test.Env; import net.minestom.server.test.EnvTest; diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestIdleTask.java similarity index 82% rename from modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java rename to modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestIdleTask.java index 2ce88baa..480bed79 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestIdleTask.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestIdleTask.java @@ -1,8 +1,8 @@ -package unnamed.mmo.entity.brain.task; +package net.hollowcube.entity.brain.task; +import net.hollowcube.data.number.NumberProvider; +import net.hollowcube.entity.brain.task.test.MockBrain; import org.junit.jupiter.api.Test; -import unnamed.mmo.data.number.NumberProvider; -import unnamed.mmo.entity.brain.task.test.MockBrain; import static com.google.common.truth.Truth.assertThat; diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestSequenceTask.java similarity index 75% rename from modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java rename to modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestSequenceTask.java index 7f5504b4..e8ecca9d 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/TestSequenceTask.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestSequenceTask.java @@ -1,13 +1,13 @@ -package unnamed.mmo.entity.brain.task; +package net.hollowcube.entity.brain.task; +import net.hollowcube.entity.brain.task.test.MockBrain; +import net.hollowcube.entity.brain.task.test.MockTask; +import net.hollowcube.entity.brain.task.test.TaskSubject; import org.junit.jupiter.api.Test; -import unnamed.mmo.entity.brain.task.test.MockBrain; -import unnamed.mmo.entity.brain.task.test.MockTask; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; -import static unnamed.mmo.entity.brain.task.test.TaskSubject.assertThat; public class TestSequenceTask { @Test @@ -34,8 +34,8 @@ public void testSingleTaskSuccess() { task.tick(brain, System.currentTimeMillis()); // Should have passed and mock1 should also have been run (passed) - assertThat(mock1).isComplete(); - assertThat(task).isComplete(); + TaskSubject.assertThat(mock1).isComplete(); + TaskSubject.assertThat(task).isComplete(); } @Test @@ -48,11 +48,11 @@ public void testMultiTaskSuccess() { task.start(brain); task.tick(brain, System.currentTimeMillis()); - assertThat(mock1).isComplete(); + TaskSubject.assertThat(mock1).isComplete(); task.tick(brain, System.currentTimeMillis()); - assertThat(mock2).isComplete(); + TaskSubject.assertThat(mock2).isComplete(); - assertThat(task).isComplete(); + TaskSubject.assertThat(task).isComplete(); } } diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockBrain.java similarity index 77% rename from modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java rename to modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockBrain.java index 212864f8..0e03dd27 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockBrain.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockBrain.java @@ -1,10 +1,10 @@ -package unnamed.mmo.entity.brain.task.test; +package net.hollowcube.entity.brain.task.test; +import net.hollowcube.entity.brain.navigator.Navigator; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.brain.Brain; -import unnamed.mmo.entity.brain.navigator.Navigator; +import net.hollowcube.entity.brain.Brain; public class MockBrain implements Brain { diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockTask.java similarity index 69% rename from modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java rename to modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockTask.java index e13b2151..2e636e0e 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/MockTask.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockTask.java @@ -1,9 +1,8 @@ -package unnamed.mmo.entity.brain.task.test; +package net.hollowcube.entity.brain.task.test; +import net.hollowcube.entity.brain.task.AbstractTask; import org.jetbrains.annotations.NotNull; -import unnamed.mmo.entity.brain.Brain; -import unnamed.mmo.entity.brain.task.AbstractTask; -import unnamed.mmo.entity.brain.task.Task; +import net.hollowcube.entity.brain.Brain; public class MockTask extends AbstractTask { private final Boolean pass; diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/TaskSubject.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/TaskSubject.java similarity index 90% rename from modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/TaskSubject.java rename to modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/TaskSubject.java index bb275ddc..5efa8c32 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/brain/task/test/TaskSubject.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/TaskSubject.java @@ -1,12 +1,12 @@ -package unnamed.mmo.entity.brain.task.test; +package net.hollowcube.entity.brain.task.test; import com.google.common.truth.Fact; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import com.google.common.truth.Truth; +import net.hollowcube.entity.brain.task.Task; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import unnamed.mmo.entity.brain.task.Task; public class TaskSubject extends Subject { private final Task actual; diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorLand.java b/modules/entity/src/test/java/net/hollowcube/entity/motion/TestPathGeneratorLand.java similarity index 97% rename from modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorLand.java rename to modules/entity/src/test/java/net/hollowcube/entity/motion/TestPathGeneratorLand.java index 673ffe5b..704e1d4b 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorLand.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/motion/TestPathGeneratorLand.java @@ -1,10 +1,10 @@ -package unnamed.mmo.entity.motion; +package net.hollowcube.entity.motion; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import org.junit.jupiter.api.Test; -import unnamed.mmo.test.MockBlockGetter; +import net.hollowcube.test.MockBlockGetter; import static com.google.common.truth.Truth.assertThat; diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorWater.java b/modules/entity/src/test/java/net/hollowcube/entity/motion/TestPathGeneratorWater.java similarity index 96% rename from modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorWater.java rename to modules/entity/src/test/java/net/hollowcube/entity/motion/TestPathGeneratorWater.java index f48159a8..619aedb1 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathGeneratorWater.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/motion/TestPathGeneratorWater.java @@ -1,10 +1,10 @@ -package unnamed.mmo.entity.motion; +package net.hollowcube.entity.motion; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import org.junit.jupiter.api.Test; -import unnamed.mmo.test.MockBlockGetter; +import net.hollowcube.test.MockBlockGetter; import static com.google.common.truth.Truth.assertThat; diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathOptimizerStringPull.java b/modules/entity/src/test/java/net/hollowcube/entity/motion/TestPathOptimizerStringPull.java similarity index 97% rename from modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathOptimizerStringPull.java rename to modules/entity/src/test/java/net/hollowcube/entity/motion/TestPathOptimizerStringPull.java index 5ad45ba5..88e78d14 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathOptimizerStringPull.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/motion/TestPathOptimizerStringPull.java @@ -1,10 +1,10 @@ -package unnamed.mmo.entity.motion; +package net.hollowcube.entity.motion; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import org.junit.jupiter.api.Test; -import unnamed.mmo.test.MockBlockGetter; +import net.hollowcube.test.MockBlockGetter; import java.util.List; diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathfinderAStar.java b/modules/entity/src/test/java/net/hollowcube/entity/motion/TestPathfinderAStar.java similarity index 96% rename from modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathfinderAStar.java rename to modules/entity/src/test/java/net/hollowcube/entity/motion/TestPathfinderAStar.java index ff0e865e..9763cb53 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/motion/TestPathfinderAStar.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/motion/TestPathfinderAStar.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity.motion; +package net.hollowcube.entity.motion; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; @@ -6,7 +6,7 @@ import net.minestom.server.instance.block.Block; import net.minestom.server.utils.Direction; import org.junit.jupiter.api.Test; -import unnamed.mmo.test.MockBlockGetter; +import net.hollowcube.test.MockBlockGetter; import java.util.ArrayList; import java.util.List; diff --git a/modules/entity/src/test/java/unnamed/mmo/entity/motion/util/TestPhysicsUtil.java b/modules/entity/src/test/java/net/hollowcube/entity/motion/util/TestPhysicsUtil.java similarity index 97% rename from modules/entity/src/test/java/unnamed/mmo/entity/motion/util/TestPhysicsUtil.java rename to modules/entity/src/test/java/net/hollowcube/entity/motion/util/TestPhysicsUtil.java index 4f1cb15a..bae0680f 100644 --- a/modules/entity/src/test/java/unnamed/mmo/entity/motion/util/TestPhysicsUtil.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/motion/util/TestPhysicsUtil.java @@ -1,4 +1,4 @@ -package unnamed.mmo.entity.motion.util; +package net.hollowcube.entity.motion.util; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; @@ -8,7 +8,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import unnamed.mmo.test.MockBlockGetter; +import net.hollowcube.test.MockBlockGetter; import java.util.stream.Stream; diff --git a/modules/entity/src/test/java/unnamed/mmo/mql/MqlTest.java b/modules/entity/src/test/java/net/hollowcube/mql/MqlTest.java similarity index 78% rename from modules/entity/src/test/java/unnamed/mmo/mql/MqlTest.java rename to modules/entity/src/test/java/net/hollowcube/mql/MqlTest.java index 372be604..0e96dd7c 100644 --- a/modules/entity/src/test/java/unnamed/mmo/mql/MqlTest.java +++ b/modules/entity/src/test/java/net/hollowcube/mql/MqlTest.java @@ -1,13 +1,13 @@ -package unnamed.mmo.mql; +package net.hollowcube.mql; +import net.hollowcube.mql.parser.MqlParser; +import net.hollowcube.mql.runtime.MqlRuntimeError; +import net.hollowcube.mql.runtime.MqlScope; +import net.hollowcube.mql.value.MqlHolder; +import net.hollowcube.mql.value.MqlNumberValue; +import net.hollowcube.mql.value.MqlValue; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; -import unnamed.mmo.mql.parser.MqlParser; -import unnamed.mmo.mql.runtime.MqlRuntimeError; -import unnamed.mmo.mql.runtime.MqlScope; -import unnamed.mmo.mql.value.MqlHolder; -import unnamed.mmo.mql.value.MqlNumberValue; -import unnamed.mmo.mql.value.MqlValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlLexer.java b/modules/entity/src/test/java/net/hollowcube/mql/parser/TestMqlLexer.java similarity index 97% rename from modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlLexer.java rename to modules/entity/src/test/java/net/hollowcube/mql/parser/TestMqlLexer.java index c53d151b..0962039c 100644 --- a/modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlLexer.java +++ b/modules/entity/src/test/java/net/hollowcube/mql/parser/TestMqlLexer.java @@ -1,4 +1,4 @@ -package unnamed.mmo.mql.parser; +package net.hollowcube.mql.parser; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; diff --git a/modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlParser.java b/modules/entity/src/test/java/net/hollowcube/mql/parser/TestMqlParser.java similarity index 94% rename from modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlParser.java rename to modules/entity/src/test/java/net/hollowcube/mql/parser/TestMqlParser.java index baffb99a..65cbeeaf 100644 --- a/modules/entity/src/test/java/unnamed/mmo/mql/parser/TestMqlParser.java +++ b/modules/entity/src/test/java/net/hollowcube/mql/parser/TestMqlParser.java @@ -1,9 +1,9 @@ -package unnamed.mmo.mql.parser; +package net.hollowcube.mql.parser; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import unnamed.mmo.mql.tree.MqlPrinter; +import net.hollowcube.mql.tree.MqlPrinter; import java.util.stream.Stream; diff --git a/modules/entity/src/test/java/unnamed/mmo/test/MockBlockGetter.java b/modules/entity/src/test/java/net/hollowcube/test/MockBlockGetter.java similarity index 98% rename from modules/entity/src/test/java/unnamed/mmo/test/MockBlockGetter.java rename to modules/entity/src/test/java/net/hollowcube/test/MockBlockGetter.java index 17c1787a..04d10088 100644 --- a/modules/entity/src/test/java/unnamed/mmo/test/MockBlockGetter.java +++ b/modules/entity/src/test/java/net/hollowcube/test/MockBlockGetter.java @@ -1,4 +1,4 @@ -package unnamed.mmo.test; +package net.hollowcube.test; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; From ff835874461d49f1005b73bdbd83e9694980563d Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 17 Sep 2022 11:25:53 -0400 Subject: [PATCH 23/25] mql improvement for defining query functions --- .../java/net/hollowcube/util/StringUtil.java | 14 ++ .../entity/EntityMqlQueryContext.java | 19 ++ .../hollowcube/entity/HeadRotationZombie.java | 64 ------ .../main/java/net/hollowcube/mql/README.md | 2 - .../mql/foreign/MqlForeignFunctions.java | 207 ++++++++++++++++++ .../mql/foreign/MqlForeignTypes.java | 58 +++++ .../net/hollowcube/mql/foreign/Query.java | 12 + .../net/hollowcube/mql/runtime/MqlMath.java | 201 +++++++++++++++++ .../net/hollowcube/mql/value/MqlCallable.java | 3 + .../net/hollowcube/mql/value/MqlNull.java | 8 - .../net/hollowcube/mql/value/MqlValue.java | 4 +- .../mql/foreign/TestMqlForeignFunctions.java | 75 +++++++ .../hollowcube/mql/runtime/TestMqlMath.java | 26 +++ 13 files changed, 617 insertions(+), 76 deletions(-) create mode 100644 modules/common/src/main/java/net/hollowcube/util/StringUtil.java delete mode 100644 modules/entity/src/main/java/net/hollowcube/entity/HeadRotationZombie.java create mode 100644 modules/entity/src/main/java/net/hollowcube/mql/foreign/MqlForeignFunctions.java create mode 100644 modules/entity/src/main/java/net/hollowcube/mql/foreign/MqlForeignTypes.java create mode 100644 modules/entity/src/main/java/net/hollowcube/mql/foreign/Query.java create mode 100644 modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlMath.java delete mode 100644 modules/entity/src/main/java/net/hollowcube/mql/value/MqlNull.java create mode 100644 modules/entity/src/test/java/net/hollowcube/mql/foreign/TestMqlForeignFunctions.java create mode 100644 modules/entity/src/test/java/net/hollowcube/mql/runtime/TestMqlMath.java diff --git a/modules/common/src/main/java/net/hollowcube/util/StringUtil.java b/modules/common/src/main/java/net/hollowcube/util/StringUtil.java new file mode 100644 index 00000000..43c3033c --- /dev/null +++ b/modules/common/src/main/java/net/hollowcube/util/StringUtil.java @@ -0,0 +1,14 @@ +package net.hollowcube.util; + +import org.intellij.lang.annotations.Language; + +public final class StringUtil { + private StringUtil() {} + + private static final @Language("regexp") String CAMEL_TO_SNAKE_CASE_REGEX = "([a-z])([A-Z]+)"; + private static final String CAMEL_TO_SNAKE_CASE_REPLACEMENT = "$1_$2"; + + public static String camelCaseToSnakeCase(String str) { + return str.replaceAll(CAMEL_TO_SNAKE_CASE_REGEX, CAMEL_TO_SNAKE_CASE_REPLACEMENT).toLowerCase(); + } +} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/EntityMqlQueryContext.java b/modules/entity/src/main/java/net/hollowcube/entity/EntityMqlQueryContext.java index 23ecac23..2e5aa7d5 100644 --- a/modules/entity/src/main/java/net/hollowcube/entity/EntityMqlQueryContext.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/EntityMqlQueryContext.java @@ -1,5 +1,6 @@ package net.hollowcube.entity; +import net.hollowcube.mql.foreign.Query; import net.hollowcube.mql.runtime.MqlRuntimeError; import net.hollowcube.mql.runtime.MqlScope; import net.hollowcube.mql.value.MqlHolder; @@ -21,4 +22,22 @@ public record EntityMqlQueryContext(@NotNull UnnamedEntity entity) implements Mq default -> throw new MqlRuntimeError("no such query function: " + queryFunction); }; } + + + @Query + public boolean isAlive() { + return !entity.isDead(); + } + + @Query + public boolean hasTarget() { + return entity.brain().getTarget() != null; + } + + // math.function_name Various math functions + // query.function_name Access to an entity's properties + // temp.variable_name Read/write temporary storage + // variable.variable_name Read/write storage on an actor + // context.variable_name Read-only storage provided by the game in certain scenarios + } diff --git a/modules/entity/src/main/java/net/hollowcube/entity/HeadRotationZombie.java b/modules/entity/src/main/java/net/hollowcube/entity/HeadRotationZombie.java deleted file mode 100644 index a79830bf..00000000 --- a/modules/entity/src/main/java/net/hollowcube/entity/HeadRotationZombie.java +++ /dev/null @@ -1,64 +0,0 @@ -package net.hollowcube.entity; - -import net.minestom.server.collision.CollisionUtils; -import net.minestom.server.coordinate.Point; -import net.minestom.server.coordinate.Pos; -import net.minestom.server.coordinate.Vec; -import net.minestom.server.entity.EntityType; -import net.minestom.server.entity.LivingEntity; -import net.minestom.server.utils.position.PositionUtils; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.ThreadLocalRandom; - -public class HeadRotationZombie extends LivingEntity { - private Point target; - - public HeadRotationZombie() { - super(EntityType.ZOMBIE); - onGround = false; - } - - @Override - public void tick(long time) { - super.tick(time); - - if (target == null || getPosition().distance(target) < 0.8) { - selectTarget(); - } - - moveTowards(target, 0.2f); - - } - - private void selectTarget() { - target = new Vec( - ThreadLocalRandom.current().nextInt(-10, 10), - 40, - ThreadLocalRandom.current().nextInt(-10, 10) - ); - } - - private void moveTowards(@NotNull Point direction, double speed) { - final Pos position = getPosition(); - final double dx = direction.x() - position.x(); - final double dy = direction.y() - position.y(); - final double dz = direction.z() - position.z(); - // the purpose of these few lines is to slow down entities when they reach their destination - final double distSquared = dx * dx + dy * dy + dz * dz; - if (speed > distSquared) { - speed = distSquared; - } - final double radians = Math.atan2(dz, dx); - final double speedX = Math.cos(radians) * speed; - final double speedY = dy * speed; - final double speedZ = Math.sin(radians) * speed; - final float yaw = PositionUtils.getLookYaw(dx, dz); - final float pitch = PositionUtils.getLookPitch(dx, dy, dz); - - // Prevent ghosting - final var physicsResult = CollisionUtils.handlePhysics(this, new Vec(speedX, speedY, speedZ)); - refreshPosition(Pos.fromPoint(physicsResult.newPosition()).withView(yaw, pitch)); - } -} - diff --git a/modules/entity/src/main/java/net/hollowcube/mql/README.md b/modules/entity/src/main/java/net/hollowcube/mql/README.md index 91858221..209c0169 100644 --- a/modules/entity/src/main/java/net/hollowcube/mql/README.md +++ b/modules/entity/src/main/java/net/hollowcube/mql/README.md @@ -1,7 +1,5 @@ # Minecraft Query Language (mql) -a.k.a MoLang Jr. - A subset of MoLang (may eventually be a full implementation). Currently implemented as a basic tree-walk interpreter, but may eventually be refactored to something more performant in the future. diff --git a/modules/entity/src/main/java/net/hollowcube/mql/foreign/MqlForeignFunctions.java b/modules/entity/src/main/java/net/hollowcube/mql/foreign/MqlForeignFunctions.java new file mode 100644 index 00000000..904d1371 --- /dev/null +++ b/modules/entity/src/main/java/net/hollowcube/mql/foreign/MqlForeignFunctions.java @@ -0,0 +1,207 @@ +package net.hollowcube.mql.foreign; + +import net.hollowcube.mql.runtime.MqlRuntimeError; +import net.hollowcube.mql.runtime.MqlScope; +import net.hollowcube.mql.value.MqlCallable; +import net.hollowcube.mql.value.MqlValue; +import net.hollowcube.util.StringUtil; +import net.minestom.server.utils.validate.Check; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; + +import java.lang.invoke.CallSite; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public class MqlForeignFunctions { + + public static @NotNull MqlScope create(@NotNull Class type, @Nullable T instance) { + // Construct a map with all static and non static @Query methods in the class + Map functions = new HashMap<>(); + for (Method method : type.getMethods()) { + Query annotation = method.getAnnotation(Query.class); + if (annotation == null) continue; + + boolean isStatic = (method.getModifiers() & Modifier.STATIC) != 0; + MqlCallable callable = createForeign(method, isStatic ? null : instance); + + String name = annotation.value(); + if (name.isEmpty()) name = StringUtil.camelCaseToSnakeCase(method.getName()); + + functions.put(name, callable); + } + + // Return the scope using the functions map + return name -> { + MqlCallable function = functions.get(name); + if (function == null) + throw new MqlRuntimeError("No such function: " + name); + + // 0 arg functions do not need an explicit call + if (function.arity() == 0) + return function.call(List.of()); + return function; + }; + } + + /** + * @param method The method to bind. Must be public, may be static. + * @param bindTo The instance of the method's class to bind to. If the method is static, this must be null. + * @return An {@link MqlCallable} representing an mql accessible function. + * + * @see FastInvokerFactory + */ + public static @NotNull MqlCallable createForeign(@NotNull Method method, @UnknownNullability Object bindTo) { + Check.argCondition((method.getModifiers() & Modifier.PUBLIC) == 0, "method must be public"); + Check.argCondition(bindTo != null && !method.getDeclaringClass().isInstance(bindTo), "bindTo must be an instance of the method class"); + + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + + Class[] erasedTypes = new Class[method.getParameterCount()]; + Arrays.fill(erasedTypes, Object.class); + Class erasedReturnType = method.getReturnType(); + if (erasedReturnType != void.class) { + erasedReturnType = Object.class; + } + + Class[] fixedTypes = new Class[method.getParameterCount()]; + int index = 0; + for (Class clazz : method.getParameterTypes()) { + fixedTypes[index++] = convertPrimitive(clazz); + } + Class fixedReturnType = method.getReturnType(); + if (fixedReturnType != void.class) { + fixedReturnType = convertPrimitive(fixedReturnType); + } + + boolean isVoid = erasedReturnType == void.class; + boolean isStatic = (method.getModifiers() & Modifier.STATIC) != 0; + + CallSite callsite = LambdaMetafactory.metafactory( + lookup, + "accept" + method.getParameterCount() + (isVoid ? "V" : "R"), + MethodType.methodType(NConsumer.class, isStatic ? new Class[0] : new Class[]{method.getDeclaringClass()}), + MethodType.methodType(erasedReturnType, erasedTypes), + lookup.unreflect(method), + MethodType.methodType(fixedReturnType, fixedTypes) + ); + + var handle = (NConsumer) (isStatic ? callsite.getTarget().invoke() : callsite.getTarget().bindTo(bindTo).invoke()); + + return new ForeignCallable(handle, fixedTypes, fixedReturnType); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private static Class convertPrimitive(Class in) { + if (in == int.class) { + return Integer.class; + } else if (in == float.class) { + return Float.class; + } else if (in == boolean.class) { + return Boolean.class; + } else if (in == double.class) { + return Double.class; + } else if (in == long.class) { + return Long.class; + } else { + return in; + } + } + + private record ForeignCallable( + NConsumer handle, + Class[] parameterTypes, + Class returnType + ) implements MqlCallable { + + @Override + public int arity() { + return parameterTypes.length; + } + + @Override + public @NotNull MqlValue call(@NotNull List args) { + if (args.size() != parameterTypes.length) { + //todo mql exception + throw new IllegalArgumentException("Expected " + parameterTypes.length + " arguments, got " + args.size()); + } + + Object[] javaArgs = new Object[args.size()]; + for (int i = 0; i < args.size(); i++) { + javaArgs[i] = MqlForeignTypes.fromMql(args.get(i), parameterTypes[i]); + } + + boolean isVoid = returnType == void.class; + if (isVoid) { + switch (args.size()) { + case 0 -> handle.accept0V(); + case 1 -> handle.accept1V(javaArgs[0]); + case 2 -> handle.accept2V(javaArgs[0], javaArgs[1]); + case 3 -> handle.accept3V(javaArgs[0], javaArgs[1], javaArgs[2]); + case 4 -> handle.accept4V(javaArgs[0], javaArgs[1], javaArgs[2], javaArgs[3]); + case 5 -> handle.accept5V(javaArgs[0], javaArgs[1], javaArgs[2], javaArgs[3], javaArgs[4]); + case 6 -> handle.accept6V(javaArgs[0], javaArgs[1], javaArgs[2], javaArgs[3], javaArgs[4], javaArgs[5]); + case 7 -> handle.accept7V(javaArgs[0], javaArgs[1], javaArgs[2], javaArgs[3], javaArgs[4], javaArgs[5], javaArgs[6]); + case 8 -> handle.accept8V(javaArgs[0], javaArgs[1], javaArgs[2], javaArgs[3], javaArgs[4], javaArgs[5], javaArgs[6], javaArgs[7]); + case 9 -> handle.accept9V(javaArgs[0], javaArgs[1], javaArgs[2], javaArgs[3], javaArgs[4], javaArgs[5], javaArgs[6], javaArgs[7], javaArgs[8]); + } + } else { + Object result = switch (args.size()) { + case 0 -> handle.accept0R(); + case 1 -> handle.accept1R(javaArgs[0]); + case 2 -> handle.accept2R(javaArgs[0], javaArgs[1]); + case 3 -> handle.accept3R(javaArgs[0], javaArgs[1], javaArgs[2]); + case 4 -> handle.accept4R(javaArgs[0], javaArgs[1], javaArgs[2], javaArgs[3]); + case 5 -> handle.accept5R(javaArgs[0], javaArgs[1], javaArgs[2], javaArgs[3], javaArgs[4]); + case 6 -> handle.accept6R(javaArgs[0], javaArgs[1], javaArgs[2], javaArgs[3], javaArgs[4], javaArgs[5]); + case 7 -> handle.accept7R(javaArgs[0], javaArgs[1], javaArgs[2], javaArgs[3], javaArgs[4], javaArgs[5], javaArgs[6]); + case 8 -> handle.accept8R(javaArgs[0], javaArgs[1], javaArgs[2], javaArgs[3], javaArgs[4], javaArgs[5], javaArgs[6], javaArgs[7]); + case 9 -> handle.accept9R(javaArgs[0], javaArgs[1], javaArgs[2], javaArgs[3], javaArgs[4], javaArgs[5], javaArgs[6], javaArgs[7], javaArgs[8]); + default -> throw new MqlRuntimeError("unreachable arg error"); + }; + + return MqlForeignTypes.toMql(result); + } + + return MqlValue.NULL; + } + } + + public interface NConsumer { + + R accept0R(); + R accept1R(P1 p1); + R accept2R(P1 p1, P2 p2); + R accept3R(P1 p1, P2 p2, P3 p3); + R accept4R(P1 p1, P2 p2, P3 p3, P4 p4); + R accept5R(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5); + R accept6R(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6); + R accept7R(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7); + R accept8R(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7, P8 p8); + R accept9R(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7, P8 p8, P9 p9); + + void accept0V(); + void accept1V(P1 p1); + void accept2V(P1 p1, P2 p2); + void accept3V(P1 p1, P2 p2, P3 p3); + void accept4V(P1 p1, P2 p2, P3 p3, P4 p4); + void accept5V(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5); + void accept6V(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6); + void accept7V(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7); + void accept8V(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7, P8 p8); + void accept9V(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7, P8 p8, P9 p9); + + } +} diff --git a/modules/entity/src/main/java/net/hollowcube/mql/foreign/MqlForeignTypes.java b/modules/entity/src/main/java/net/hollowcube/mql/foreign/MqlForeignTypes.java new file mode 100644 index 00000000..45c8d459 --- /dev/null +++ b/modules/entity/src/main/java/net/hollowcube/mql/foreign/MqlForeignTypes.java @@ -0,0 +1,58 @@ +package net.hollowcube.mql.foreign; + +import net.hollowcube.mql.value.MqlNumberValue; +import net.hollowcube.mql.value.MqlValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnknownNullability; + +/** + * Conversion utility between java types and MQL types. + *

    + * todo not sure if other types should be convertable. I think just doubles is fine. + */ +public class MqlForeignTypes { + + public static @NotNull MqlValue toMql(@UnknownNullability Object javaValue) { + if (javaValue == null) return MqlValue.NULL; + if (javaValue instanceof MqlValue value) + return value; + if (javaValue instanceof Double value) + return new MqlNumberValue(value); +// if (javaValue instanceof Float value) +// return new MqlNumberValue(value); +// if (javaValue instanceof Long value) +// return new MqlNumberValue(value); +// if (javaValue instanceof Integer value) +// return new MqlNumberValue(value); +// if (javaValue instanceof Short value) +// return new MqlNumberValue(value); +// if (javaValue instanceof Byte value) +// return new MqlNumberValue(value); + if (javaValue instanceof Boolean value) + return MqlValue.from(value); + throw new RuntimeException("cannot convert " + javaValue.getClass().getSimpleName() + " to mql value"); + } + + public static @UnknownNullability Object fromMql(@NotNull MqlValue value, @NotNull Class targetType) { + if (value instanceof MqlNumberValue numberValue) { + if (Double.class.equals(targetType) || double.class.equals(targetType)) { + return numberValue.value(); +// } else if (Float.class.equals(targetType) || float.class.equals(targetType)) { +// return (float) numberValue.value(); +// } else if (Long.class.equals(targetType) || long.class.equals(targetType)) { +// return (long) numberValue.value(); +// } else if (Integer.class.equals(targetType) || int.class.equals(targetType)) { +// return (int) numberValue.value(); +// } else if (Short.class.equals(targetType) || short.class.equals(targetType)) { +// return (short) numberValue.value(); +// } else if (Byte.class.equals(targetType) || byte.class.equals(targetType)) { +// return (byte) numberValue.value(); + } else if (Boolean.class.equals(targetType) || boolean.class.equals(targetType)) { + return numberValue.value() != 0; + } + throw new RuntimeException("cannot convert number " + targetType.getSimpleName()); + } + throw new RuntimeException("cannot convert " + value.getClass().getSimpleName() + " to " + targetType.getSimpleName()); + } + +} diff --git a/modules/entity/src/main/java/net/hollowcube/mql/foreign/Query.java b/modules/entity/src/main/java/net/hollowcube/mql/foreign/Query.java new file mode 100644 index 00000000..90b4dcf9 --- /dev/null +++ b/modules/entity/src/main/java/net/hollowcube/mql/foreign/Query.java @@ -0,0 +1,12 @@ +package net.hollowcube.mql.foreign; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Query { + String value() default ""; +} diff --git a/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlMath.java b/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlMath.java new file mode 100644 index 00000000..55c065e1 --- /dev/null +++ b/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlMath.java @@ -0,0 +1,201 @@ +package net.hollowcube.mql.runtime; + +import net.hollowcube.mql.foreign.MqlForeignFunctions; +import net.hollowcube.mql.foreign.Query; + +import java.util.concurrent.ThreadLocalRandom; + +public class MqlMath { + + public static final MqlScope INSTANCE = MqlForeignFunctions.create(MqlMath.class, null); + + private MqlMath() {} + + /** Absolute value of value */ + @Query + public static double abs(double value) { + return Math.abs(value); + } + + /** arccos of value */ + @Query + public static double acos(double value) { + return Math.acos(value); + } + + /** arcsin of value */ + @Query + public static double asin(double value) { + return Math.asin(value); + } + + /** arctan of value */ + @Query + public static double atan(double value) { + return Math.atan(value); + } + + /** arctan of y/x. NOTE: the order of arguments! */ + @Query + public static double atan2(double y, double x) { + return Math.atan2(y, x); + } + + /** Round value up to nearest integral number */ + @Query + public static double ceil(double value) { + return Math.ceil(value); + } + + /** Clamp value to between min and max inclusive */ + @Query + public static double clamp(double value, double min, double max) { + return Math.min(Math.max(value, min), max); + } + + /** Cosine (in degrees) of value */ + @Query + public static double cos(double value) { + return Math.cos(value); + } + + /** Returns the sum of 'num' random numbers, each with a value from low to high. Note: the generated random numbers are not integers like normal dice. For that, use math.die_roll_integer. */ + @Query + public static double dieRoll(double num, double low, double high) { + double total = 0; + for (int i = 0; i < num; i++) + total += random(low, high); + return total; + } + + /** Returns the sum of 'num' random integer numbers, each with a value from low to high. Note: the generated random numbers are integers like normal dice. */ + @Query + public static double dieRollInteger(double num, double low, double high) { + double total = 0; + for (int i = 0; i < num; i++) + total += randomInteger(low, high); + return total; + } + + /** Calculates e to the value 'nth' power */ + @Query + public static double exp(double value) { + return Math.exp(value); + } + + /** Round value down to nearest integral number */ + @Query + public static double floor(double value) { + return Math.floor(value); + } + + /** Useful for simple smooth curve interpolation using one of the Hermite Basis functions: 3t^2 - 2t^3. Note that while any valid float is a valid input, this function works best in the range [0,1]. */ + @Query + public static double hermiteBlend(double value) { + //todo: implement me + throw new MqlRuntimeError("hermite_blend not implemented"); + } + + /** Lerp from start to end via zeroToOne */ + @Query + public static double lerp(double start, double end, double zeroToOne) { + //todo test me + zeroToOne = clamp(zeroToOne, 0, 1); + return start * zeroToOne + end * (1D - zeroToOne); + } + + /** Lerp the shortest direction around a circle from start degrees to end degrees via zeroToOne */ + @Query + public static double lerprotate(double start, double end, double zeroToOne) { + //todo test me + zeroToOne = clamp(zeroToOne, 0, 1); + double diff = end - start; + if (diff > 180) diff -= 360; + else if (diff < -180) diff += 360; + return start + diff * zeroToOne; + } + + /** Natural logarithm of value */ + @Query + public static double ln(double value) { + return Math.log(value); + } + + /** Return highest value of A or B */ + @Query + public static double max(double a, double b) { + return Math.max(a, b); + } + + /** Return lowest value of A or B */ + @Query + public static double min(double a, double b) { + return Math.min(a, b); + } + + /** Minimize angle magnitude (in degrees) into the range [-180, 180) */ + @Query + public static double minAngle(double value) { + //todo: implement me + throw new MqlRuntimeError("hermite_blend not implemented"); + } + + /** Return the remainder of value / denominator */ + @Query + public static double mod(double value, double denominator) { + return value % denominator; + } + + /** Returns the float representation of the constant pi. */ + @Query + public static double pi() { + return Math.PI; + } + + /** Elevates base to the exponent'th power */ + @Query + public static double pow(double base, double exponent) { + return Math.pow(base, exponent); + } + + /** + * Random value between low (inclusive) and high (exclusive) + *

    + * Note: The original molang spec says that the range is inclusive, but this high end is exclusive. + */ + @Query + public static double random(double low, double high) { + return ThreadLocalRandom.current().nextDouble(low, high); + } + + /** Random integer value between low and high (inclusive) */ + @Query + public static double randomInteger(double low, double high) { + return ThreadLocalRandom.current().nextInt((int) low, (int) high + 1); + } + + /** Round value to nearest integral number */ + @Query + public static double round(double value) { + return Math.round(value); + } + + /** Sine (in degrees) of value */ + @Query + public static double sin(double value) { + return Math.sin(value); + } + + /** Square root of value */ + @Query + public static double sqrt(double value) { + return Math.sqrt(value); + } + + /** Round value towards zero */ + @Query + public static double trunc(double value) { + return value < 0 ? Math.ceil(value) : Math.floor(value); + } + +} diff --git a/modules/entity/src/main/java/net/hollowcube/mql/value/MqlCallable.java b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlCallable.java index 4fffd1c8..3255bdb2 100644 --- a/modules/entity/src/main/java/net/hollowcube/mql/value/MqlCallable.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlCallable.java @@ -7,6 +7,9 @@ @FunctionalInterface public non-sealed interface MqlCallable extends MqlValue { + /** Returns the arity of the function, or -1 if it is variadic/otherwise unknown */ + default int arity() { return -1; } + @NotNull MqlValue call(@NotNull List args); } diff --git a/modules/entity/src/main/java/net/hollowcube/mql/value/MqlNull.java b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlNull.java deleted file mode 100644 index 4708183a..00000000 --- a/modules/entity/src/main/java/net/hollowcube/mql/value/MqlNull.java +++ /dev/null @@ -1,8 +0,0 @@ -package net.hollowcube.mql.value; - -final class MqlNull implements MqlValue { - public static final MqlNull INSTANCE = new MqlNull(); - - private MqlNull() {} - -} diff --git a/modules/entity/src/main/java/net/hollowcube/mql/value/MqlValue.java b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlValue.java index 85aba481..7cabfa3c 100644 --- a/modules/entity/src/main/java/net/hollowcube/mql/value/MqlValue.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/value/MqlValue.java @@ -6,8 +6,8 @@ /** * Mutable marker for any possible mql value. */ -public sealed interface MqlValue permits MqlCallable, MqlHolder, MqlIdentValue, MqlNull, MqlNumberValue { - MqlValue NULL = MqlNull.INSTANCE; +public sealed interface MqlValue permits MqlCallable, MqlHolder, MqlIdentValue, MqlNumberValue { + MqlValue NULL = new MqlNumberValue(0.0); static @NotNull MqlValue from(boolean bool) { return new MqlNumberValue(bool ? 1 : 0); diff --git a/modules/entity/src/test/java/net/hollowcube/mql/foreign/TestMqlForeignFunctions.java b/modules/entity/src/test/java/net/hollowcube/mql/foreign/TestMqlForeignFunctions.java new file mode 100644 index 00000000..dcec1391 --- /dev/null +++ b/modules/entity/src/test/java/net/hollowcube/mql/foreign/TestMqlForeignFunctions.java @@ -0,0 +1,75 @@ +package net.hollowcube.mql.foreign; + +import net.hollowcube.mql.value.MqlCallable; +import net.hollowcube.mql.value.MqlValue; +import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.com.google.common.util.concurrent.AtomicDouble; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.google.common.truth.Truth.assertThat; + +public class TestMqlForeignFunctions { + + private static final AtomicBoolean test1Called = new AtomicBoolean(false); + + public static void test1() { + test1Called.set(true); + } + + @Test + public void emptyVoidFunction() throws Exception { + Method method = getClass().getMethod("test1"); + MqlCallable function = MqlForeignFunctions.createForeign(method, null); + assertThat(function.arity()).isEqualTo(0); + assertThat(function.call(List.of())).isEqualTo(MqlValue.NULL); + assertThat(test1Called.get()).isTrue(); + } + + private static final AtomicDouble test2Value = new AtomicDouble(0); + + public static void test2(double value) { + test2Value.set(value); + } + + @Test + public void singleArgVoidFunction() throws Exception { + Method method = getClass().getMethod("test2", double.class); + MqlCallable function = MqlForeignFunctions.createForeign(method, null); + MqlValue result = function.call(List.of(MqlValue.from(10.5))); + + assertThat(function.arity()).isEqualTo(1); + assertThat(result).isEqualTo(MqlValue.NULL); + assertThat(test2Value.get()).isEqualTo(10.5); + } + + public static double test3() { + return 10.5; + } + + @Test + public void emptyNonVoidFunction() throws Exception { + Method method = getClass().getMethod("test3"); + MqlCallable function = MqlForeignFunctions.createForeign(method, null); + MqlValue result = function.call(List.of()); + + assertThat(function.arity()).isEqualTo(0); + assertThat(result).isEqualTo(MqlValue.from(10.5)); + } + + public static double test4(double a, double b) { + return a + b; + } + + @Test + public void multiParamNonVoidFunction() throws Exception { + Method method = getClass().getMethod("test4", double.class, double.class); + MqlCallable function = MqlForeignFunctions.createForeign(method, null); + MqlValue result = function.call(List.of(MqlValue.from(10.5), MqlValue.from(20.5))); + + assertThat(function.arity()).isEqualTo(2); + assertThat(result).isEqualTo(MqlValue.from(31)); + } +} diff --git a/modules/entity/src/test/java/net/hollowcube/mql/runtime/TestMqlMath.java b/modules/entity/src/test/java/net/hollowcube/mql/runtime/TestMqlMath.java new file mode 100644 index 00000000..b9879cd6 --- /dev/null +++ b/modules/entity/src/test/java/net/hollowcube/mql/runtime/TestMqlMath.java @@ -0,0 +1,26 @@ +package net.hollowcube.mql.runtime; + +import net.hollowcube.mql.value.MqlCallable; +import net.hollowcube.mql.value.MqlNumberValue; +import net.hollowcube.mql.value.MqlValue; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static com.google.common.truth.Truth.assertThat; + +public class TestMqlMath { + + @Test + public void testForeignMath() { + MqlValue result = MqlMath.INSTANCE.get("sqrt").cast(MqlCallable.class) + .call(List.of(MqlValue.from(4))); + + assertThat(result).isEqualTo(MqlValue.from(2)); + } + + // hermiteBlend + // lerp + // lerprotate + +} From 04f59969971a23a1949208eabefa7a9b9c43661b Mon Sep 17 00:00:00 2001 From: mworzala Date: Thu, 22 Sep 2022 19:26:47 -0400 Subject: [PATCH 24/25] add gradle configuration cache --- gradle.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 gradle.properties diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..9f7a1314 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.unsafe.configuration-cache = true From 326e5b411b170d2625fc29a8f87bb7463ffc1c44 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sat, 24 Sep 2022 19:47:48 -0400 Subject: [PATCH 25/25] cleanup entity class, remove unused classes. --- .../blocks/ore/handler/OreBlockHandler.java | 7 +- modules/build.gradle.kts | 2 +- .../server/instance/TickTrackingInstance.java | 34 --- .../entity/EntityMqlQueryContext.java | 43 --- .../net/hollowcube/entity/EntityType.java | 27 ++ .../net/hollowcube/entity/SmartEntity.java | 84 ++++++ .../net/hollowcube/entity/UnnamedEntity.java | 55 ---- .../net/hollowcube/entity/brain/Brain.java | 31 --- .../entity/brain/SingleTaskBrain.java | 79 ------ .../brain/navigator/CustomNavigator.java | 30 -- .../entity/brain/stimuli/StimuliSource.java | 13 - .../entity/motion/MotionNavigator.java | 2 +- .../entity/motion/MotionNavigatorSlime.java | 2 +- .../navigator/HydrazineNavigator.java | 2 +- .../{brain => }/navigator/Navigator.java | 2 +- .../entity/pathfinding/PFNavigator.java | 261 ------------------ .../pathfinding/PFNeighborSupplier.java | 76 ----- .../hollowcube/entity/pathfinding/PFNode.java | 46 --- .../hollowcube/entity/pathfinding/PFPath.java | 44 --- .../entity/pathfinding/PFPathGenerator.java | 77 ------ .../entity/pathfinding/PFPathOptimizer.java | 73 ----- .../stimuli/NearbyEntityStimuliSource.java | 11 +- .../entity/stimuli/StimuliSource.java | 13 + .../entity/{brain => }/task/AbstractTask.java | 6 +- .../{brain => }/task/FollowTargetTask.java | 34 +-- .../entity/{brain => }/task/IdleTask.java | 16 +- .../entity/{brain => }/task/SelectorTask.java | 57 ++-- .../entity/{brain => }/task/SequenceTask.java | 22 +- .../entity/{brain => }/task/Task.java | 13 +- .../{brain => }/task/WanderInRegionTask.java | 20 +- .../net/hollowcube/mql/runtime/MqlScope.java | 11 +- .../hollowcube/mql/runtime/MqlScopeImpl.java | 27 ++ .../mql/runtime/MqlScriptScope.java | 30 ++ .../src/main/resources/data/behaviors.json | 2 +- .../src/main/resources/data/entities.json | 1 + .../TestNavigatorBasicIntegration.java | 1 + .../entity/brain/navigator/TestUtil.java | 1 + .../entity/brain/task/TestIdleTask.java | 2 + .../entity/brain/task/TestSequenceTask.java | 2 + .../entity/brain/task/test/MockBrain.java | 3 +- .../entity/brain/task/test/MockTask.java | 3 +- .../entity/brain/task/test/TaskSubject.java | 2 +- 42 files changed, 316 insertions(+), 951 deletions(-) delete mode 100644 modules/common/src/main/java/net/hollowcube/server/instance/TickTrackingInstance.java delete mode 100644 modules/entity/src/main/java/net/hollowcube/entity/EntityMqlQueryContext.java create mode 100644 modules/entity/src/main/java/net/hollowcube/entity/EntityType.java create mode 100644 modules/entity/src/main/java/net/hollowcube/entity/SmartEntity.java delete mode 100644 modules/entity/src/main/java/net/hollowcube/entity/UnnamedEntity.java delete mode 100644 modules/entity/src/main/java/net/hollowcube/entity/brain/Brain.java delete mode 100644 modules/entity/src/main/java/net/hollowcube/entity/brain/SingleTaskBrain.java delete mode 100644 modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/CustomNavigator.java delete mode 100644 modules/entity/src/main/java/net/hollowcube/entity/brain/stimuli/StimuliSource.java rename modules/entity/src/main/java/net/hollowcube/entity/{brain => }/navigator/HydrazineNavigator.java (95%) rename modules/entity/src/main/java/net/hollowcube/entity/{brain => }/navigator/Navigator.java (95%) delete mode 100644 modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNavigator.java delete mode 100644 modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNeighborSupplier.java delete mode 100644 modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNode.java delete mode 100644 modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPath.java delete mode 100644 modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathGenerator.java delete mode 100644 modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathOptimizer.java rename modules/entity/src/main/java/net/hollowcube/entity/{brain => }/stimuli/NearbyEntityStimuliSource.java (72%) create mode 100644 modules/entity/src/main/java/net/hollowcube/entity/stimuli/StimuliSource.java rename modules/entity/src/main/java/net/hollowcube/entity/{brain => }/task/AbstractTask.java (75%) rename modules/entity/src/main/java/net/hollowcube/entity/{brain => }/task/FollowTargetTask.java (58%) rename modules/entity/src/main/java/net/hollowcube/entity/{brain => }/task/IdleTask.java (77%) rename modules/entity/src/main/java/net/hollowcube/entity/{brain => }/task/SelectorTask.java (73%) rename modules/entity/src/main/java/net/hollowcube/entity/{brain => }/task/SequenceTask.java (81%) rename modules/entity/src/main/java/net/hollowcube/entity/{brain => }/task/Task.java (78%) rename modules/entity/src/main/java/net/hollowcube/entity/{brain => }/task/WanderInRegionTask.java (69%) create mode 100644 modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScopeImpl.java create mode 100644 modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScriptScope.java diff --git a/modules/block-interactions/src/main/java/net/hollowcube/blocks/ore/handler/OreBlockHandler.java b/modules/block-interactions/src/main/java/net/hollowcube/blocks/ore/handler/OreBlockHandler.java index d5711e8f..87603f26 100644 --- a/modules/block-interactions/src/main/java/net/hollowcube/blocks/ore/handler/OreBlockHandler.java +++ b/modules/block-interactions/src/main/java/net/hollowcube/blocks/ore/handler/OreBlockHandler.java @@ -7,6 +7,7 @@ import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; +import net.minestom.server.thread.TickThread; import net.minestom.server.utils.NamespaceID; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; @@ -15,7 +16,6 @@ import net.hollowcube.blocks.ore.Ore; import net.hollowcube.blocks.ore.event.PlayerOreBreakEvent; import net.hollowcube.loot.LootContext; -import net.hollowcube.server.instance.TickTrackingInstance; import net.hollowcube.util.BlockUtil; import net.hollowcube.util.FutureUtil; @@ -118,8 +118,9 @@ public void tick(@NotNull Tick tick) { // Only tick once a second. // This could be adjusted in the future if necessary. final Instance instance = tick.getInstance(); - long currentTick = ((TickTrackingInstance) instance).getTick(); - if (currentTick % 20 != 0) + final TickThread thread = TickThread.current(); + Check.notNull(thread, "TickThread.current() returned null"); + if (thread.getTick() % 20 != 0) return; final Point pos = tick.getBlockPosition(); diff --git a/modules/build.gradle.kts b/modules/build.gradle.kts index 98be44d9..8e1aa4e6 100644 --- a/modules/build.gradle.kts +++ b/modules/build.gradle.kts @@ -27,7 +27,7 @@ subprojects { implementation("com.google.auto.service:auto-service-annotations:1.0.1") // Minestom - implementation("com.github.minestommmo:Minestom:c6c97162a6") + implementation("com.github.minestommmo:Minestom:ec6035d939") // Testing testImplementation(project(":modules:test")) diff --git a/modules/common/src/main/java/net/hollowcube/server/instance/TickTrackingInstance.java b/modules/common/src/main/java/net/hollowcube/server/instance/TickTrackingInstance.java deleted file mode 100644 index df2deee1..00000000 --- a/modules/common/src/main/java/net/hollowcube/server/instance/TickTrackingInstance.java +++ /dev/null @@ -1,34 +0,0 @@ -package net.hollowcube.server.instance; - -import net.minestom.server.instance.IChunkLoader; -import net.minestom.server.instance.InstanceContainer; -import net.minestom.server.world.DimensionType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.UUID; - -public class TickTrackingInstance extends InstanceContainer { - //todo i (matt) really dont think this should be in common, but not sure where - - private long tick = 0; - - public TickTrackingInstance(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType, @Nullable IChunkLoader loader) { - super(uniqueId, dimensionType, loader); - } - - public TickTrackingInstance(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType) { - super(uniqueId, dimensionType); - } - - @Override - public void tick(long time) { - tick++; - - super.tick(time); - } - - public long getTick() { - return tick; - } -} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/EntityMqlQueryContext.java b/modules/entity/src/main/java/net/hollowcube/entity/EntityMqlQueryContext.java deleted file mode 100644 index 2e5aa7d5..00000000 --- a/modules/entity/src/main/java/net/hollowcube/entity/EntityMqlQueryContext.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.hollowcube.entity; - -import net.hollowcube.mql.foreign.Query; -import net.hollowcube.mql.runtime.MqlRuntimeError; -import net.hollowcube.mql.runtime.MqlScope; -import net.hollowcube.mql.value.MqlHolder; -import org.jetbrains.annotations.NotNull; -import net.hollowcube.mql.value.MqlValue; - -//todo this whole class sucks, need to work out mql querying better -public record EntityMqlQueryContext(@NotNull UnnamedEntity entity) implements MqlScope { - @Override - public @NotNull MqlValue get(@NotNull String name) { - if (!name.equals("q") && !name.equals("query")) - throw new MqlRuntimeError("unknown environment object: " + name); - return (MqlHolder) queryFunction -> switch (queryFunction) { - // Entity - case "is_alive" -> MqlValue.from(!entity.isDead()); - case "has_target" -> MqlValue.from(entity.brain().getTarget() != null); - // World - case "time_of_day" -> MqlValue.from(entity.getInstance().getTime() / 18_000D); - default -> throw new MqlRuntimeError("no such query function: " + queryFunction); - }; - } - - - @Query - public boolean isAlive() { - return !entity.isDead(); - } - - @Query - public boolean hasTarget() { - return entity.brain().getTarget() != null; - } - - // math.function_name Various math functions - // query.function_name Access to an entity's properties - // temp.variable_name Read/write temporary storage - // variable.variable_name Read/write storage on an actor - // context.variable_name Read-only storage provided by the game in certain scenarios - -} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/EntityType.java b/modules/entity/src/main/java/net/hollowcube/entity/EntityType.java new file mode 100644 index 00000000..69bdb9aa --- /dev/null +++ b/modules/entity/src/main/java/net/hollowcube/entity/EntityType.java @@ -0,0 +1,27 @@ +package net.hollowcube.entity; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.hollowcube.dfu.ExtraCodecs; +import net.hollowcube.entity.task.Task; +import net.hollowcube.registry.Registry; +import net.hollowcube.registry.Resource; +import net.minestom.server.utils.NamespaceID; +import org.jetbrains.annotations.NotNull; + +public record EntityType( + @NotNull NamespaceID namespace, + @NotNull NamespaceID model, + @NotNull Task.Spec behavior +) implements Resource { + + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + ExtraCodecs.NAMESPACE_ID.fieldOf("namespace").forGetter(EntityType::namespace), + ExtraCodecs.NAMESPACE_ID.fieldOf("model").forGetter(EntityType::model), + ExtraCodecs.NAMESPACE_ID.xmap(ns -> Task.Spec.REGISTRY.required(ns.asString()), Task.Spec::namespace) + .fieldOf("behavior").forGetter(EntityType::behavior) + ).apply(i, EntityType::new)); + + public static final Registry REGISTRY = Registry.codec("entities", CODEC); + +} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/SmartEntity.java b/modules/entity/src/main/java/net/hollowcube/entity/SmartEntity.java new file mode 100644 index 00000000..e2aad9e8 --- /dev/null +++ b/modules/entity/src/main/java/net/hollowcube/entity/SmartEntity.java @@ -0,0 +1,84 @@ +package net.hollowcube.entity; + +import net.hollowcube.entity.motion.MotionNavigator; +import net.hollowcube.entity.navigator.Navigator; +import net.hollowcube.entity.task.Task; +import net.hollowcube.mql.MqlScript; +import net.hollowcube.mql.foreign.MqlForeignFunctions; +import net.hollowcube.mql.runtime.MqlScope; +import net.hollowcube.mql.runtime.MqlScopeImpl; +import net.hollowcube.mql.runtime.MqlScriptScope; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.LivingEntity; +import net.minestom.server.event.EventDispatcher; +import net.minestom.server.event.entity.EntityAttackEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class SmartEntity extends LivingEntity { + + // Scripting + private final MqlScope queryScope = MqlForeignFunctions.create(SmartEntity.class, this); + private final MqlScope.Mutable actorScope = new MqlScopeImpl.Mutable(); + + // Brain + private final Navigator navigator; + private final Task rootTask; + + // Runtime state + private Entity target = null; + + public SmartEntity(@NotNull EntityType entityType) { + super(net.minestom.server.entity.EntityType.fromNamespaceId(entityType.model())); + + this.navigator = new MotionNavigator(this); + this.rootTask = entityType.behavior().create(); + } + + public @NotNull Navigator navigator() { + return navigator; + } + + public @Nullable Entity getTarget() { + return target; + } + + public void setTarget(@Nullable Entity target) { + this.target = target; + } + + @Override + public void update(long time) { + super.update(time); + + // Do not tick until it is in an instance. + if (!isActive()) return; + + navigator.tick(time); + switch (rootTask.getState()) { + case INIT, COMPLETE -> rootTask.start(this); + case RUNNING -> rootTask.tick(this, time); + case FAILED -> { + //todo this probably isnt the best way to handle this + remove(); + throw new RuntimeException("Entity root task failed: " + this); + } + } + } + + public void attack(@NotNull Entity target) { + swingMainHand(); + EntityAttackEvent attackEvent = new EntityAttackEvent(this, target); + EventDispatcher.call(attackEvent); + } + + // Scripting + + public double evalScript(@NotNull MqlScript script) { + return script.evaluate(new MqlScriptScope(queryScope, actorScope, MqlScope.EMPTY)); + } + + public boolean evalScriptBool(@NotNull MqlScript script) { + return script.evaluateToBool(new MqlScriptScope(queryScope, actorScope, MqlScope.EMPTY)); + } +} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/UnnamedEntity.java b/modules/entity/src/main/java/net/hollowcube/entity/UnnamedEntity.java deleted file mode 100644 index d1dc9cf4..00000000 --- a/modules/entity/src/main/java/net/hollowcube/entity/UnnamedEntity.java +++ /dev/null @@ -1,55 +0,0 @@ -package net.hollowcube.entity; - -import net.hollowcube.entity.brain.Brain; -import net.hollowcube.entity.brain.SingleTaskBrain; -import net.hollowcube.entity.brain.task.Task; -import net.minestom.server.coordinate.Pos; -import net.minestom.server.entity.Entity; -import net.minestom.server.entity.EntityType; -import net.minestom.server.entity.LivingEntity; -import net.minestom.server.entity.metadata.other.SlimeMeta; -import net.minestom.server.event.EventDispatcher; -import net.minestom.server.event.entity.EntityAttackEvent; -import net.minestom.server.instance.Instance; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.CompletableFuture; - -public class UnnamedEntity extends LivingEntity { - private final Brain brain; - - public UnnamedEntity(Task task) { - super(EntityType.ZOMBIE); - if (getEntityMeta() instanceof SlimeMeta slimeMeta) { - slimeMeta.setSize(1); - } - brain = new SingleTaskBrain(this, task); - } - - @Override - public CompletableFuture setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) { - return super.setInstance(instance, spawnPosition).thenAccept(unused -> { - brain.setInstance(instance); - }); - } - - @Override - public void update(long time) { - super.update(time); - - if (instance != null) { - brain.tick(time); - } - - } - - public @NotNull Brain brain() { - return brain; - } - - public void attack(@NotNull Entity target) { - swingMainHand(); - EntityAttackEvent attackEvent = new EntityAttackEvent(this, target); - EventDispatcher.call(attackEvent); - } -} \ No newline at end of file diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/Brain.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/Brain.java deleted file mode 100644 index 58c367d3..00000000 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/Brain.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.hollowcube.entity.brain; - -import net.hollowcube.entity.brain.navigator.Navigator; -import net.minestom.server.coordinate.Point; -import net.minestom.server.entity.Entity; -import net.minestom.server.instance.Instance; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public interface Brain { - - @NotNull Entity entity(); - - @NotNull Navigator navigator(); - - boolean setPathTo(@NotNull Point point); - - default void setInstance(Instance instance) {} - - void tick(long time); - - - - // Temp stuff im not in love with - - default @Nullable Entity getTarget() { - return null; - } - - -} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/SingleTaskBrain.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/SingleTaskBrain.java deleted file mode 100644 index cc7202db..00000000 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/SingleTaskBrain.java +++ /dev/null @@ -1,79 +0,0 @@ -package net.hollowcube.entity.brain; - -import net.hollowcube.entity.brain.navigator.Navigator; -import net.hollowcube.entity.brain.task.Task; -import net.minestom.server.coordinate.Point; -import net.minestom.server.entity.Entity; -import net.minestom.server.instance.Instance; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class SingleTaskBrain implements Brain { - - private final Entity entity; - private final Navigator navigator; - private final Task task; - - private Entity target = null; - - public SingleTaskBrain(@NotNull Entity entity, @NotNull Task task) { - this.entity = entity; - this.navigator = Navigator.motion(entity); - this.task = task; - } - - - @Override - public @NotNull Entity entity() { - return entity; - } - - @Override - public @NotNull Navigator navigator() { - return navigator; - } - - @Override - public boolean setPathTo(@NotNull Point point) { - return navigator.setPathTo(point); - } - - private boolean failed = false; - private Instance lastInstance = null; - - @Override - public void setInstance(Instance instance) { - navigator.setInstance(instance); - lastInstance = instance; - } - - @Override - public void tick(long time) { - if (lastInstance == null) return; - - //todo also player death or anything else? - if (target != null && target.isRemoved()) { - target = null; - } - - navigator.tick(time); - switch (task.getState()) { - case INIT, COMPLETE -> task.start(this); - case RUNNING -> task.tick(this, time); - case FAILED -> { - if (!failed) - System.out.println("Failed"); - failed = true; - } - } - } - - @Override - public @Nullable Entity getTarget() { - return target; - } - - public void setTarget(@NotNull Entity target) { - this.target = target; - } -} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/CustomNavigator.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/CustomNavigator.java deleted file mode 100644 index 492b6093..00000000 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/CustomNavigator.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.hollowcube.entity.brain.navigator; - -import net.minestom.server.coordinate.Point; -import net.minestom.server.entity.Entity; -import org.jetbrains.annotations.NotNull; -import net.hollowcube.entity.pathfinding.PFNavigator; - -final class CustomNavigator implements Navigator { - private final PFNavigator navigator; - - public CustomNavigator(@NotNull Entity entity) { - this.navigator = new PFNavigator(entity); - } - - - @Override - public boolean setPathTo(@NotNull Point point) { - return navigator.setPathTo(point, 0.5f); - } - - @Override - public boolean isActive() { - return !navigator.isComplete(); - } - - @Override - public void tick(long time) { - navigator.tick(time); - } -} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/stimuli/StimuliSource.java b/modules/entity/src/main/java/net/hollowcube/entity/brain/stimuli/StimuliSource.java deleted file mode 100644 index 7065da8c..00000000 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/stimuli/StimuliSource.java +++ /dev/null @@ -1,13 +0,0 @@ -package net.hollowcube.entity.brain.stimuli; - -import org.jetbrains.annotations.NotNull; -import net.hollowcube.entity.brain.Brain; - -public interface StimuliSource { - - void update(@NotNull Brain brain); - - - - -} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigator.java b/modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigator.java index 01980ad0..4609d838 100644 --- a/modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigator.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigator.java @@ -5,7 +5,7 @@ import com.mattworzala.debug.shape.Box; import com.mattworzala.debug.shape.Line; import com.mattworzala.debug.shape.OutlineBox; -import net.hollowcube.entity.brain.navigator.Navigator; +import net.hollowcube.entity.navigator.Navigator; import net.minestom.server.attribute.Attribute; import net.minestom.server.collision.CollisionUtils; import net.minestom.server.coordinate.Point; diff --git a/modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigatorSlime.java b/modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigatorSlime.java index 9440ed78..ef1d6d0e 100644 --- a/modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigatorSlime.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/motion/MotionNavigatorSlime.java @@ -1,6 +1,6 @@ package net.hollowcube.entity.motion; -import net.hollowcube.entity.brain.navigator.Navigator; +import net.hollowcube.entity.navigator.Navigator; import net.minestom.server.attribute.Attribute; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/HydrazineNavigator.java b/modules/entity/src/main/java/net/hollowcube/entity/navigator/HydrazineNavigator.java similarity index 95% rename from modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/HydrazineNavigator.java rename to modules/entity/src/main/java/net/hollowcube/entity/navigator/HydrazineNavigator.java index 05a69cba..a31b8329 100644 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/HydrazineNavigator.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/navigator/HydrazineNavigator.java @@ -1,4 +1,4 @@ -package net.hollowcube.entity.brain.navigator; +package net.hollowcube.entity.navigator; import com.extollit.gaming.ai.path.HydrazinePathFinder; import net.minestom.server.coordinate.Point; diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/Navigator.java b/modules/entity/src/main/java/net/hollowcube/entity/navigator/Navigator.java similarity index 95% rename from modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/Navigator.java rename to modules/entity/src/main/java/net/hollowcube/entity/navigator/Navigator.java index 9ee7618d..14216e02 100644 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/navigator/Navigator.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/navigator/Navigator.java @@ -1,4 +1,4 @@ -package net.hollowcube.entity.brain.navigator; +package net.hollowcube.entity.navigator; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; diff --git a/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNavigator.java b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNavigator.java deleted file mode 100644 index 86d404cd..00000000 --- a/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNavigator.java +++ /dev/null @@ -1,261 +0,0 @@ -package net.hollowcube.entity.pathfinding; - -import com.mattworzala.debug.DebugMessage; -import com.mattworzala.debug.Layer; -import com.mattworzala.debug.shape.Box; -import com.mattworzala.debug.shape.Line; -import com.mattworzala.debug.shape.OutlineBox; -import net.minestom.server.attribute.Attribute; -import net.minestom.server.collision.CollisionUtils; -import net.minestom.server.coordinate.Point; -import net.minestom.server.coordinate.Pos; -import net.minestom.server.coordinate.Vec; -import net.minestom.server.entity.Entity; -import net.minestom.server.entity.LivingEntity; -import net.minestom.server.instance.Chunk; -import net.minestom.server.instance.Instance; -import net.minestom.server.instance.WorldBorder; -import net.minestom.server.utils.chunk.ChunkUtils; -import net.minestom.server.utils.position.PositionUtils; -import net.minestom.server.utils.time.Cooldown; -import net.minestom.server.utils.time.TimeUnit; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.time.Duration; -import java.util.ArrayList; - -// TODO all pathfinding requests could be processed in another thread - -public final class PFNavigator { - private Point goalPosition; - private final Entity entity; - private PFPath path; - private final Cooldown jumpCooldown = new Cooldown(Duration.of(40, TimeUnit.SERVER_TICK)); - private double minimumDistance; - - public PFNavigator(@NotNull Entity entity) { - this.entity = entity; - } - - /** - * Used to move the entity toward {@code direction} in the X and Z axis - * Gravity is still applied but the entity will not attempt to jump - * Also update the yaw/pitch of the entity to look along 'direction' - * - * @param direction the targeted position - * @param speed define how far the entity will move - */ - public boolean moveTowards(@NotNull Point direction, double speed) { - final Pos position = entity.getPosition(); - final double dx = direction.x() - position.x(); - final double dy = direction.y() - position.y(); - final double dz = direction.z() - position.z(); - // the purpose of these few lines is to slow down entities when they reach their destination - final double distSquared = dx * dx + dy * dy + dz * dz; - if (speed > distSquared) { - speed = distSquared; - } - final double radians = Math.atan2(dz, dx); - final double speedX = Math.cos(radians) * speed; - final double speedY = dy * speed; - final double speedZ = Math.sin(radians) * speed; - final float yaw = PositionUtils.getLookYaw(dx, dz); - final float pitch = PositionUtils.getLookPitch(dx, dy, dz); - // Prevent ghosting - final var physicsResult = CollisionUtils.handlePhysics(entity, new Vec(speedX, speedY, speedZ)); - this.entity.refreshPosition(Pos.fromPoint(physicsResult.newPosition()).withView(yaw, pitch)); - return physicsResult.collisionX() || physicsResult.collisionY() | physicsResult.collisionZ(); - } - - public void jump(float height) { - // FIXME magic value - this.entity.setVelocity(new Vec(0, height * 2.5f, 0)); - } - - /** - * Retrieves the path to {@code position} and ask the entity to follow the path. - *

    - * Can be set to null to reset the pathfinder. - *

    - * The position is cloned, if you want the entity to continually follow this position object - * you need to call this when you want the path to update. - * - * @param point the position to find the path to, null to reset the pathfinder - * @param minimumDistance - * @return true if a path has been found - */ - public synchronized boolean setPathTo(@Nullable Point point, double minimumDistance) { - if (point != null && goalPosition != null && point.samePoint(goalPosition) && this.path != null) { - // Tried to set path to the same value position - return false; - } - final Instance instance = entity.getInstance(); - if (point == null) { - this.path = null; - return false; - } - // Can't path with a null instance. - if (instance == null) { - this.path = null; - return false; - } - // Can't path outside the world border - final WorldBorder worldBorder = instance.getWorldBorder(); - if (!worldBorder.isInside(point)) { - return false; - } - // Can't path in an unloaded chunk - final Chunk chunk = instance.getChunkAt(point); - if (!ChunkUtils.isLoaded(chunk)) { - return false; - } - - this.minimumDistance = minimumDistance; - if (this.entity.getPosition().distance(point) < minimumDistance) return false; - if (goalPosition != null && point.samePoint(goalPosition)) return false; - - this.path = PFPathGenerator.generate(instance, - this.entity.getPosition(), - point, - 100, - this.entity.getBoundingBox().depth() * 2, - this.entity.getBoundingBox(), - PFPathOptimizer.NOOP - ); - - final boolean success = path != null; - this.goalPosition = success ? point : null; - return success; - } - - private long tick = 0; - - @ApiStatus.Internal - public synchronized void tick(long tick) { - if (goalPosition == null) return; // No path - if (entity instanceof LivingEntity && ((LivingEntity) entity).isDead()) return; // No pathfinding tick for dead entities - if (path == null) return; - - if (this.tick++ % 5 == 0) { - sendDebugData(); - } - - - Point currentTarget = path.getCurrent(); - if (currentTarget == null) currentTarget = goalPosition; - float movementSpeed = 0.1f; - - if (this.entity.getDistance(goalPosition) < minimumDistance) return; - - if (entity instanceof LivingEntity living) { - movementSpeed = living.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue(); - } - - boolean isStuck = moveTowards(currentTarget, movementSpeed); - - if (isStuck) { - double heightChange = currentTarget.y() - entity.getPosition().y(); - if (heightChange > 0 && entity.getVelocity().y() <= 0) { - if (heightChange < 0.2) { - entity.setVelocity(new Vec(0, 2, 0)); - moveTowards(currentTarget, movementSpeed); - } else if (heightChange < 1) { - entity.setVelocity(new Vec(0, 4, 0)); - moveTowards(currentTarget, movementSpeed); - } else if (jumpCooldown.isReady(tick)) { - jumpCooldown.refreshLastUpdate(tick); - jump(3.5f); - moveTowards(currentTarget, movementSpeed); - } - } - } - - if (entity.getPosition().distanceSquared(currentTarget) < 0.4) { - path.next(); - } - } - - - /** - * Gets the value pathfinder position. - * - * @return the value pathfinder position, null if there is no one - */ - public @Nullable Point getGoalPosition() { - return goalPosition; - } - - public @NotNull Entity getEntity() { - return entity; - } - - public PFPath getPath() { - return path; - } - - private void reset() { - this.goalPosition = null; - this.path = null; - } - - public boolean isComplete() { - if (this.path == null) return true; - return goalPosition == null || entity.getPosition().distance(goalPosition) < 1; - } - - - - // SECTION: Debug rendering - // Eventually this should be only in the dev server. Just don't currently have a way to do a "mixin" here. - // Probably will have some way to set the entity provider somewhere. - - private @NotNull String debugNamespace(){ - return "debug_" + entity.getUuid(); - } - - private void sendDebugData() { - var builder = DebugMessage.builder() - .clear(debugNamespace()); - - addPathfinderDebugData(builder); - addTargetPoint(builder); - - builder.build() - .sendTo(entity.getViewersAsAudience()); - } - - private void addPathfinderDebugData(DebugMessage.Builder builder) { - if (path == null) return; - var nodes = path.nodes; - var linePoints = new ArrayList(); - - for (int i = path.index; i < nodes.size(); i++) { - var pos = Vec.fromPoint(nodes.get(i)); - builder.set( - debugNamespace() + ":pf_node_" + i, - new Box(pos.sub(0.4, 0.0, 0.4), pos.add(0.4, 0.1, 0.4), 0x331CB2F5, Layer.TOP) - ); - linePoints.add(pos.withY(y -> y + 0.05)); - } - builder.set( - debugNamespace() + ":pf_path", - new Line(linePoints, 10f, 0xFF1CB2F5, Layer.TOP) - ); - } - - private void addTargetPoint(DebugMessage.Builder builder) { - if (goalPosition == null) return; - builder.set( - debugNamespace() + ":pf_target", - new OutlineBox.Builder() - .block(goalPosition.blockX(), goalPosition.blockY(), goalPosition.blockZ(), 0) - .color(0x55FF0000) - .layer(Layer.TOP) - .colorLine(0xFFFF0000) - .layerLine(Layer.TOP) - .build() - ); - } -} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNeighborSupplier.java b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNeighborSupplier.java deleted file mode 100644 index 4ab33591..00000000 --- a/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNeighborSupplier.java +++ /dev/null @@ -1,76 +0,0 @@ -package net.hollowcube.entity.pathfinding; - -import net.minestom.server.collision.BoundingBox; -import net.minestom.server.coordinate.Point; -import net.minestom.server.coordinate.Vec; -import net.minestom.server.instance.block.Block; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Collection; - -public interface PFNeighborSupplier { - - //todo right now supplier cannot influence cost, that is probably not desirable behavior - @NotNull - Collection<@NotNull Point> getNeighbors(Block.Getter blockGetter, Point current, Point goal, @NotNull BoundingBox expandedBoundingBox); - - class Iam implements PFNeighborSupplier { - - @Override - public @NotNull Collection<@NotNull Point> getNeighbors(Block.Getter blockGetter, Point current, Point goal, @NotNull BoundingBox expandedBoundingBox) { - Collection nearby = new ArrayList<>(); - - int width = (int) Math.ceil(expandedBoundingBox.width() / 2); - - for (int x = -width; x <= width; x++) { - for (int z = -width; z <= width; z++) { - if (x == 0 && z == 0) continue; - - Point orgPoint = new Vec(current.blockX(), current.blockY(), current.blockZ()).add(x, 0, z).add(0.5, 0, 0.5); - Point point = PFNode.gravitySnap(blockGetter, orgPoint); -// Point point = orgPoint; - - if (point == null) continue; - - //todo may be worth adding something like this back later (more expensive to jump) -// float cost = orgPoint.blockY() == point.blockY() ? 0.9f : 4.5f; -// if (x != 0 && z != 0) cost -= 0.2; - - if (blockGetter.getBlock(point, Block.Getter.Condition.TYPE).isSolid()) continue; - if (!blockGetter.getBlock(point.sub(0, 1, 0), Block.Getter.Condition.TYPE).isSolid()) continue; - - //todo this stops my villager from finding a path between two blocks -// Collection overlapping = BoundingBoxUtilKt.getBlocks(expandedBoundingBox, point); -// -// boolean isInvalid = false; -// for (Point block : overlapping) { -// if (blockGetter.getBlock(block, Block.Getter.Condition.TYPE).isSolid()) { -// isInvalid = true; -// break; -// } -// } -// -// if (isInvalid) { -// // Check up 1 block -// boolean isInvalidUp = false; -// for (Point block : overlapping) { -// if (blockGetter.getBlock(block.add(0, 1, 0), Block.Getter.Condition.TYPE).isSolid()) { -// isInvalidUp = true; -// break; -// } -// } -// -// if (isInvalidUp) continue; -// point = point.add(0, 1, 0); -// } - - //todo a slight optimization would be to not add any visited points to the list - nearby.add(point); - } - } - - return nearby; - } - } -} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNode.java b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNode.java deleted file mode 100644 index 129fdba7..00000000 --- a/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFNode.java +++ /dev/null @@ -1,46 +0,0 @@ -package net.hollowcube.entity.pathfinding; - -import net.minestom.server.coordinate.Point; -import net.minestom.server.instance.block.Block; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -public record PFNode( - @NotNull Point point, - float cost -) { - - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PFNode pfNode = (PFNode) o; - return point.equals(pfNode.point); - } - - @Override - public int hashCode() { - return Objects.hash(point); - } - - - - private static Point findGround(Block.Getter blockGetter, Point point) { - Point ground = point.sub(0, 1, 0); - while (!blockGetter.getBlock(ground, Block.Getter.Condition.TYPE).isSolid()) { - ground = ground.sub(0, 1, 0); - if (Math.abs(ground.blockY() - point.blockY()) > 30) return null; - } - Point newPoint = ground.add(0, 1, 0); - return newPoint.withY(newPoint.blockY()); - } - - public static Point gravitySnap(Block.Getter blockGetter, Point orgPoint) { - var above = blockGetter.getBlock(orgPoint, Block.Getter.Condition.TYPE).isSolid(); - - if (above) return orgPoint.add(0, 1, 0); - return findGround(blockGetter, orgPoint); - } -} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPath.java b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPath.java deleted file mode 100644 index 39820853..00000000 --- a/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPath.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.hollowcube.entity.pathfinding; - -import net.minestom.server.collision.BoundingBox; -import net.minestom.server.collision.CollisionUtils; -import net.minestom.server.collision.PhysicsResult; -import net.minestom.server.coordinate.Point; -import net.minestom.server.coordinate.Pos; -import net.minestom.server.coordinate.Vec; -import net.minestom.server.instance.Instance; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; - -public class PFPath { - List nodes; - public int index = 0; - - public PFPath(List nodes) { - this.nodes = nodes; - } - - public List getNodes() { - return nodes; - } - - @Nullable - public Point getCurrent() { - if (index >= nodes.size()) return null; - var current = nodes.get(index); - return current; - } - - public void next() { - if (index >= nodes.size()) return; - index++; - } - - @Override - public String toString() { - return nodes.toString(); - } - -} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathGenerator.java b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathGenerator.java deleted file mode 100644 index 542b9d21..00000000 --- a/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathGenerator.java +++ /dev/null @@ -1,77 +0,0 @@ -package net.hollowcube.entity.pathfinding; - -import net.minestom.server.collision.BoundingBox; -import net.minestom.server.coordinate.Point; -import net.minestom.server.instance.Instance; - -import java.util.*; - -public class PFPathGenerator { - public static double heuristic (Point node, Point target) { - return node.distance(target); - } - - static Comparator pNodeComparator = (a, b) -> Float.compare(a.cost(), b.cost()); - public static PFPath generate(Instance instance, Point orgStart, Point orgTarget, double maxDistance, double closeDistance, BoundingBox boundingBox, PFPathOptimizer optimizer) { - Point start = PFNode.gravitySnap(instance, orgStart); - Point target = PFNode.gravitySnap(instance, orgTarget); - if (start == null || target == null) return null; - - BoundingBox expandedBoundingBox = boundingBox.expand(0.5, 0, 0.5); - PFNeighborSupplier neighborSupplier = new PFNeighborSupplier.Iam(); - - int maxSize = (int) Math.floor(maxDistance * 7); - - PFNode startNode = new PFNode(start, 0); - - Queue open = new PriorityQueue<>(pNodeComparator); - open.add(startNode); - - Map cameFrom = new HashMap<>(); - - Map minCost = new HashMap<>(); - minCost.put(start, 0f); - - while (!open.isEmpty()) { - PFNode current = open.peek(); - if (current.point().distance(target) < closeDistance) - break; // Path is found - - if (cameFrom.size() >= maxSize) // Max search depth reached - throw new RuntimeException("Ran out of nodes"); - - open.poll(); - for (Point neighbor : neighborSupplier.getNeighbors(instance, current.point(), target, expandedBoundingBox)) { - float neighborCost = (float) (minCost.get(current.point()) + current.point().distance(neighbor)); - if (neighborCost < minCost.getOrDefault(neighbor, Float.POSITIVE_INFINITY)) { - // New cheapest path, update indices - float neighborTotalCost = (float) (neighborCost + heuristic(neighbor, target)); - PFNode neighborNode = new PFNode(neighbor, neighborTotalCost); - cameFrom.put(neighborNode, current); - minCost.put(neighbor, neighborCost); - - if (open.contains(neighborNode)) { - open.remove(neighborNode); - } - open.offer(neighborNode); - } - - } -// var nearbyPoints = current.getNearby(instance, closed, value, expandedBoundingBox) -// .stream().filter(p -> p.point.distance(value) <= maxDistance).collect(Collectors.toSet()); - - } - - List result = new ArrayList<>(); - PFNode current = open.peek(); - if (current == null) return null; - result.add(current.point()); - while (cameFrom.containsKey(current)) { - current = cameFrom.get(current); - result.add(0, current.point()); - } - - return optimizer.optimize(new PFPath(result), instance, expandedBoundingBox); - } - -} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathOptimizer.java b/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathOptimizer.java deleted file mode 100644 index ab5af9b4..00000000 --- a/modules/entity/src/main/java/net/hollowcube/entity/pathfinding/PFPathOptimizer.java +++ /dev/null @@ -1,73 +0,0 @@ -package net.hollowcube.entity.pathfinding; - -import net.minestom.server.collision.BoundingBox; -import net.minestom.server.collision.CollisionUtils; -import net.minestom.server.collision.PhysicsResult; -import net.minestom.server.coordinate.Point; -import net.minestom.server.coordinate.Pos; -import net.minestom.server.coordinate.Vec; -import net.minestom.server.instance.Instance; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; - -@FunctionalInterface -public interface PFPathOptimizer { - - @NotNull PFPath optimize(@NotNull PFPath path, @NotNull Instance instance, @NotNull BoundingBox bb); - - PFPathOptimizer NOOP = (path, instance, bb) -> path; - - PFPathOptimizer IAM = (path, instance, bb) -> { - List newNodes = new ArrayList<>(); - - { - var nodes = path.nodes; - int current = 0; - int next = 1; - - for (int i = 0; i < nodes.size() - 1; i++) { - var currentNode = nodes.get(current); - var nextNode = nodes.get(next); - var diff = nextNode.sub(currentNode); - PhysicsResult res = CollisionUtils.handlePhysics(instance, instance.getChunkAt(currentNode), bb, Pos.fromPoint(currentNode), Vec.fromPoint(diff), null); - if (res.collisionX() || res.collisionY() || res.collisionZ()) { - newNodes.add(nodes.get(next - 1)); - newNodes.add(nodes.get(next)); - current = next; - } - next++; - } - if (nodes.size() > 0) - newNodes.add(nodes.get(nodes.size() - 1)); - - path = new PFPath(newNodes); - } - - { - var nodes = path.nodes; - newNodes = new ArrayList<>(); - for (int current = 0; current < nodes.size() - 1; current++) { - var currentNode = nodes.get(current); - var nextNode = nodes.get(current + 1); - newNodes.add(currentNode); - Point diff = nextNode.sub(currentNode); - PhysicsResult res = CollisionUtils.handlePhysics(instance, instance.getChunkAt(currentNode), bb, Pos.fromPoint(currentNode), Vec.fromPoint(diff), null); - if (res.collisionX() || res.collisionY() || res.collisionZ()) { - Point end = res.newPosition(); - if (nextNode.distance(end) > 0.1) { - newNodes.add(end); - } - } - } - if (nodes.size() > 0) - newNodes.add(nodes.get(nodes.size() - 1)); - - path = new PFPath(nodes); - } - - return path; - }; - -} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/stimuli/NearbyEntityStimuliSource.java b/modules/entity/src/main/java/net/hollowcube/entity/stimuli/NearbyEntityStimuliSource.java similarity index 72% rename from modules/entity/src/main/java/net/hollowcube/entity/brain/stimuli/NearbyEntityStimuliSource.java rename to modules/entity/src/main/java/net/hollowcube/entity/stimuli/NearbyEntityStimuliSource.java index 0113cd98..a0ddf3f1 100644 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/stimuli/NearbyEntityStimuliSource.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/stimuli/NearbyEntityStimuliSource.java @@ -1,12 +1,10 @@ -package net.hollowcube.entity.brain.stimuli; +package net.hollowcube.entity.stimuli; -import net.minestom.server.entity.Entity; +import net.hollowcube.entity.SmartEntity; import net.minestom.server.entity.Player; import net.minestom.server.instance.EntityTracker; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; -import net.hollowcube.entity.brain.Brain; -import net.hollowcube.entity.brain.SingleTaskBrain; import java.util.ArrayList; import java.util.List; @@ -14,8 +12,7 @@ public class NearbyEntityStimuliSource implements StimuliSource { @Override - public void update(@NotNull Brain brain) { - final Entity entity = brain.entity(); + public void update(@NotNull SmartEntity entity) { final Instance instance = entity.getInstance(); List nearby = new ArrayList<>(); @@ -31,7 +28,7 @@ public void update(@NotNull Brain brain) { } } - ((SingleTaskBrain) brain).setTarget(closest); + entity.setTarget(closest); } } diff --git a/modules/entity/src/main/java/net/hollowcube/entity/stimuli/StimuliSource.java b/modules/entity/src/main/java/net/hollowcube/entity/stimuli/StimuliSource.java new file mode 100644 index 00000000..15422b56 --- /dev/null +++ b/modules/entity/src/main/java/net/hollowcube/entity/stimuli/StimuliSource.java @@ -0,0 +1,13 @@ +package net.hollowcube.entity.stimuli; + +import net.hollowcube.entity.SmartEntity; +import org.jetbrains.annotations.NotNull; + +public interface StimuliSource { + + void update(@NotNull SmartEntity entity); + + + + +} diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/AbstractTask.java b/modules/entity/src/main/java/net/hollowcube/entity/task/AbstractTask.java similarity index 75% rename from modules/entity/src/main/java/net/hollowcube/entity/brain/task/AbstractTask.java rename to modules/entity/src/main/java/net/hollowcube/entity/task/AbstractTask.java index 7c9748b6..2380384e 100644 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/AbstractTask.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/task/AbstractTask.java @@ -1,7 +1,7 @@ -package net.hollowcube.entity.brain.task; +package net.hollowcube.entity.task; +import net.hollowcube.entity.SmartEntity; import org.jetbrains.annotations.NotNull; -import net.hollowcube.entity.brain.Brain; public abstract non-sealed class AbstractTask implements Task { private State state = State.INIT; @@ -12,7 +12,7 @@ public abstract non-sealed class AbstractTask implements Task { } @Override - public void start(@NotNull Brain brain) { + public void start(@NotNull SmartEntity entity) { this.state = State.RUNNING; } diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/FollowTargetTask.java b/modules/entity/src/main/java/net/hollowcube/entity/task/FollowTargetTask.java similarity index 58% rename from modules/entity/src/main/java/net/hollowcube/entity/brain/task/FollowTargetTask.java rename to modules/entity/src/main/java/net/hollowcube/entity/task/FollowTargetTask.java index b7163840..0161a405 100644 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/FollowTargetTask.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/task/FollowTargetTask.java @@ -1,44 +1,41 @@ -package net.hollowcube.entity.brain.task; +package net.hollowcube.entity.task; import com.google.auto.service.AutoService; import com.mojang.serialization.Codec; +import net.hollowcube.entity.SmartEntity; import net.minestom.server.entity.Entity; +import net.minestom.server.thread.TickThread; +import net.minestom.server.utils.NamespaceID; import net.minestom.server.utils.time.Cooldown; import net.minestom.server.utils.time.TimeUnit; +import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; -import net.hollowcube.entity.UnnamedEntity; -import net.hollowcube.entity.brain.Brain; import java.time.Duration; public class FollowTargetTask extends AbstractTask { - private int tick = 0; - private Cooldown attackCooldown = new Cooldown(Duration.of(1, TimeUnit.SECOND)); @Override - public void start(@NotNull Brain brain) { - super.start(brain); - } - - @Override - public void tick(@NotNull Brain brain, long time) { + public void tick(@NotNull SmartEntity entity, long time) { - final Entity target = brain.getTarget(); + final Entity target = entity.getTarget(); if (target == null) { end(true); return; } - if (tick++ % 5 == 0) { - brain.setPathTo(target.getPosition()); + TickThread thread = TickThread.current(); + Check.notNull(thread, "Task ticked outside of tick thread"); + if (thread.getTick() % 5 == 0) { + entity.navigator().setPathTo(target.getPosition()); } - double distance = brain.entity().getDistanceSquared(target); + double distance = entity.getDistanceSquared(target); if (distance < 4 && attackCooldown.isReady(time)) { attackCooldown.refreshLastUpdate(time); - ((UnnamedEntity) brain.entity()).attack(target); + entity.attack(target); } } @@ -50,6 +47,11 @@ public record Spec() implements Task.Spec { public @NotNull Task create() { return new FollowTargetTask(); } + + @Override + public @NotNull NamespaceID namespace() { + return NamespaceID.from("unnamed:follow_target"); + } } @AutoService(Task.Factory.class) diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/IdleTask.java b/modules/entity/src/main/java/net/hollowcube/entity/task/IdleTask.java similarity index 77% rename from modules/entity/src/main/java/net/hollowcube/entity/brain/task/IdleTask.java rename to modules/entity/src/main/java/net/hollowcube/entity/task/IdleTask.java index 28f85f73..63891655 100644 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/IdleTask.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/task/IdleTask.java @@ -1,12 +1,13 @@ -package net.hollowcube.entity.brain.task; +package net.hollowcube.entity.task; import com.google.auto.service.AutoService; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.hollowcube.data.NumberSource; import net.hollowcube.data.number.NumberProvider; +import net.hollowcube.entity.SmartEntity; +import net.minestom.server.utils.NamespaceID; import org.jetbrains.annotations.NotNull; -import net.hollowcube.entity.brain.Brain; public class IdleTask extends AbstractTask { private final Spec spec; @@ -17,15 +18,15 @@ public IdleTask(@NotNull Spec spec) { } @Override - public void start(@NotNull Brain brain) { - super.start(brain); + public void start(@NotNull SmartEntity entity) { + super.start(entity); //todo get number source from entity or something, this is inconvenient to test sleepTime = (int) spec.time().nextLong(NumberSource.threadLocalRandom()); } @Override - public void tick(@NotNull Brain brain, long time) { + public void tick(@NotNull SmartEntity entity, long time) { sleepTime -= 1; if (sleepTime < 1) { end(true); @@ -45,6 +46,11 @@ public record Spec( public @NotNull Task create() { return new IdleTask(this); } + + @Override + public @NotNull NamespaceID namespace() { + return NamespaceID.from("unnamed:idle"); + } } @AutoService(Task.Factory.class) diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/SelectorTask.java b/modules/entity/src/main/java/net/hollowcube/entity/task/SelectorTask.java similarity index 73% rename from modules/entity/src/main/java/net/hollowcube/entity/brain/task/SelectorTask.java rename to modules/entity/src/main/java/net/hollowcube/entity/task/SelectorTask.java index 11aafb76..ce20eb84 100644 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/SelectorTask.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/task/SelectorTask.java @@ -1,16 +1,16 @@ -package net.hollowcube.entity.brain.task; +package net.hollowcube.entity.task; +import com.google.auto.service.AutoService; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; -import net.hollowcube.entity.brain.stimuli.NearbyEntityStimuliSource; -import net.hollowcube.entity.brain.stimuli.StimuliSource; +import net.hollowcube.entity.SmartEntity; +import net.hollowcube.entity.stimuli.NearbyEntityStimuliSource; +import net.hollowcube.entity.stimuli.StimuliSource; +import net.minestom.server.utils.NamespaceID; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.hollowcube.entity.EntityMqlQueryContext; -import net.hollowcube.entity.UnnamedEntity; -import net.hollowcube.entity.brain.Brain; import net.hollowcube.mql.MqlScript; import java.util.ArrayList; @@ -24,7 +24,6 @@ public class SelectorTask extends AbstractTask { private final Spec spec; private final List children = new ArrayList<>(); - private EntityMqlQueryContext queryContext; private int activeTask = -1; //todo temp @@ -39,21 +38,20 @@ public SelectorTask(Spec spec) { @Override - public void start(@NotNull Brain brain) { - super.start(brain); - this.queryContext = new EntityMqlQueryContext((UnnamedEntity) brain.entity()); + public void start(@NotNull SmartEntity entity) { + super.start(entity); - evaluate(brain); + evaluate(entity); } @Override - public void tick(@NotNull Brain brain, long time) { - stimuli.update(brain); //todo not sure these should update every tick? + public void tick(@NotNull SmartEntity entity, long time) { + stimuli.update(entity); //todo not sure these should update every tick? // Try to tick the current task, if present if (activeTask != -1) { Task active = children.get(activeTask); - active.tick(brain, time); + active.tick(entity, time); // Check if task is complete switch (active.getState()) { @@ -61,7 +59,7 @@ public void tick(@NotNull Brain brain, long time) { case COMPLETE -> { // Current task finished with success, select a new task. activeTask = -1; // Reset the active task so evaluate restarts it if relevant - evaluate(brain); + evaluate(entity); return; } // Otherwise do nothing and continue as normal @@ -74,15 +72,15 @@ public void tick(@NotNull Brain brain, long time) { // Attempt to choose a new task. //todo do not test for change every tick in the future - evaluate(brain); + evaluate(entity); } /** Evaluate each task in order, choosing the first matching one. */ - private void evaluate(@NotNull Brain brain) { + private void evaluate(@NotNull SmartEntity entity) { for (int i = 0; i < children.size(); i++) { Descriptor child = spec.children.get(i); // Try to evaluate this child - if (!child.script.evaluateToBool(queryContext)) + if (!entity.evalScriptBool(child.script)) continue; // If the selected task is also the current task, do nothing @@ -90,13 +88,13 @@ private void evaluate(@NotNull Brain brain) { // Cancel the old task //todo should do this better - brain.navigator().setPathTo(null); + entity.navigator().setPathTo(null); // Change task and start the new one LOGGER.info("starting new task: {}", i); activeTask = i; Task newTask = children.get(i); - newTask.start(brain); + newTask.start(entity); return; } } @@ -113,6 +111,11 @@ public record Descriptor( public @NotNull Task create() { return task.create(); } + + @Override + public @NotNull NamespaceID namespace() { + return task.namespace(); + } } public record Spec( @@ -122,7 +125,7 @@ public record Spec( private static final Codec> TASK_MIXIN = Codec.pair( lazy(() -> Task.Spec.CODEC), RecordCodecBuilder.create(i -> i.group( - Codec.BOOL.optionalFieldOf("canInterrupt", false).forGetter(b -> b) + Codec.BOOL.optionalFieldOf("interrupt", false).forGetter(b -> b) ).apply(i, b -> b)) ); @@ -139,8 +142,18 @@ public record Spec( public @NotNull Task create() { return new SelectorTask(this); } - } + @Override + public @NotNull NamespaceID namespace() { + return NamespaceID.from("unnamed:selector"); + } + } + @AutoService(Task.Factory.class) + public static final class Factory extends Task.Factory { + public Factory() { + super("starlight:selector", Spec.class, Spec.CODEC); + } + } } diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/SequenceTask.java b/modules/entity/src/main/java/net/hollowcube/entity/task/SequenceTask.java similarity index 81% rename from modules/entity/src/main/java/net/hollowcube/entity/brain/task/SequenceTask.java rename to modules/entity/src/main/java/net/hollowcube/entity/task/SequenceTask.java index 829c6a02..b0d532f1 100644 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/SequenceTask.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/task/SequenceTask.java @@ -1,10 +1,11 @@ -package net.hollowcube.entity.brain.task; +package net.hollowcube.entity.task; import com.google.auto.service.AutoService; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.hollowcube.entity.SmartEntity; +import net.minestom.server.utils.NamespaceID; import org.jetbrains.annotations.NotNull; -import net.hollowcube.entity.brain.Brain; import java.util.List; @@ -25,8 +26,8 @@ public SequenceTask(@NotNull Spec spec) { } @Override - public void start(@NotNull Brain brain) { - super.start(brain); + public void start(@NotNull SmartEntity entity) { + super.start(entity); reset(); if (!hasNext()) { @@ -34,13 +35,13 @@ public void start(@NotNull Brain brain) { return; } - current().start(brain); + current().start(entity); } @Override - public void tick(@NotNull Brain brain, long time) { + public void tick(@NotNull SmartEntity entity, long time) { final Task current = current(); - current.tick(brain, time); + current.tick(entity, time); // Do nothing if current task is still running if (current.getState() == State.RUNNING) return; @@ -58,7 +59,7 @@ public void tick(@NotNull Brain brain, long time) { } // Next task - next().start(brain); + next().start(entity); } @SuppressWarnings("BooleanMethodIsAlwaysInverted") @@ -90,6 +91,11 @@ public record Spec( public @NotNull Task create() { return new SequenceTask(this); } + + @Override + public @NotNull NamespaceID namespace() { + return NamespaceID.from("unnamed:sequence"); + } } @AutoService(Task.Factory.class) diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/Task.java b/modules/entity/src/main/java/net/hollowcube/entity/task/Task.java similarity index 78% rename from modules/entity/src/main/java/net/hollowcube/entity/brain/task/Task.java rename to modules/entity/src/main/java/net/hollowcube/entity/task/Task.java index c305c7f4..4d055674 100644 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/Task.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/task/Task.java @@ -1,8 +1,9 @@ -package net.hollowcube.entity.brain.task; +package net.hollowcube.entity.task; import com.mojang.serialization.Codec; +import net.hollowcube.entity.SmartEntity; +import net.hollowcube.registry.Resource; import org.jetbrains.annotations.NotNull; -import net.hollowcube.entity.brain.Brain; import net.hollowcube.registry.Registry; import net.hollowcube.registry.ResourceFactory; @@ -10,17 +11,19 @@ public sealed interface Task permits AbstractTask { @NotNull State getState(); - void start(@NotNull Brain brain); + void start(@NotNull SmartEntity entity); - void tick(@NotNull Brain brain, long time); + void tick(@NotNull SmartEntity entity, long time); enum State { INIT, RUNNING, COMPLETE, FAILED } - interface Spec { + interface Spec extends Resource { Codec CODEC = Factory.CODEC.dispatch(Factory::from, Factory::codec); + Registry REGISTRY = Registry.codec("behaviors", CODEC); + @NotNull Task create(); } diff --git a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/WanderInRegionTask.java b/modules/entity/src/main/java/net/hollowcube/entity/task/WanderInRegionTask.java similarity index 69% rename from modules/entity/src/main/java/net/hollowcube/entity/brain/task/WanderInRegionTask.java rename to modules/entity/src/main/java/net/hollowcube/entity/task/WanderInRegionTask.java index 7864707a..ab23635b 100644 --- a/modules/entity/src/main/java/net/hollowcube/entity/brain/task/WanderInRegionTask.java +++ b/modules/entity/src/main/java/net/hollowcube/entity/task/WanderInRegionTask.java @@ -1,10 +1,11 @@ -package net.hollowcube.entity.brain.task; +package net.hollowcube.entity.task; import com.google.auto.service.AutoService; import com.mojang.serialization.Codec; +import net.hollowcube.entity.SmartEntity; import net.minestom.server.coordinate.Vec; +import net.minestom.server.utils.NamespaceID; import org.jetbrains.annotations.NotNull; -import net.hollowcube.entity.brain.Brain; import java.util.concurrent.ThreadLocalRandom; @@ -16,8 +17,8 @@ public WanderInRegionTask(@NotNull Spec spec) { } @Override - public void start(@NotNull Brain brain) { - super.start(brain); + public void start(@NotNull SmartEntity entity) { + super.start(entity); var target = new Vec( ThreadLocalRandom.current().nextInt(-10, 10), @@ -26,15 +27,15 @@ public void start(@NotNull Brain brain) { ); System.out.println("PF to " + target); - boolean result = brain.setPathTo(target); + boolean result = entity.navigator().setPathTo(target); System.out.println("Result: " + result); if (!result) end(false); } @Override - public void tick(@NotNull Brain brain, long time) { - if (brain.navigator().isActive()) return; + public void tick(@NotNull SmartEntity entity, long time) { + if (entity.navigator().isActive()) return; end(true); } @@ -46,6 +47,11 @@ public record Spec() implements Task.Spec { public @NotNull Task create() { return new WanderInRegionTask(this); } + + @Override + public @NotNull NamespaceID namespace() { + return NamespaceID.from("unnamed:wander_in_region"); + } } @AutoService(Task.Factory.class) diff --git a/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScope.java b/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScope.java index 564397d1..00fb9f26 100644 --- a/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScope.java +++ b/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScope.java @@ -1,12 +1,19 @@ package net.hollowcube.mql.runtime; +import net.hollowcube.mql.value.MqlHolder; import org.jetbrains.annotations.NotNull; import net.hollowcube.mql.value.MqlValue; -public interface MqlScope { +public interface MqlScope extends MqlHolder { + + MqlScope EMPTY = unused -> MqlValue.NULL; @NotNull MqlValue get(@NotNull String name); - //todo setter as well i guess + interface Mutable extends MqlScope { + + void set(@NotNull String name, @NotNull MqlValue value); + + } } diff --git a/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScopeImpl.java b/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScopeImpl.java new file mode 100644 index 00000000..701d2c56 --- /dev/null +++ b/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScopeImpl.java @@ -0,0 +1,27 @@ +package net.hollowcube.mql.runtime; + +import net.hollowcube.mql.value.MqlValue; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +public class MqlScopeImpl implements MqlScope { + protected final Map data = new HashMap<>(); + + @Override + public @NotNull MqlValue get(@NotNull String name) { + return data.getOrDefault(name, MqlValue.NULL); + } + + + public static class Mutable extends MqlScopeImpl implements MqlScope.Mutable { + + @Override + public void set(@NotNull String name, @NotNull MqlValue value) { + data.put(name, value); + } + + } + +} diff --git a/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScriptScope.java b/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScriptScope.java new file mode 100644 index 00000000..c6a64bc7 --- /dev/null +++ b/modules/entity/src/main/java/net/hollowcube/mql/runtime/MqlScriptScope.java @@ -0,0 +1,30 @@ +package net.hollowcube.mql.runtime; + +import net.hollowcube.mql.value.MqlValue; +import org.jetbrains.annotations.NotNull; + +public class MqlScriptScope implements MqlScope { + + private final MqlScope query; + private final MqlScope.Mutable actor; + private final MqlScope context; + private final MqlScope.Mutable temp = new MqlScopeImpl.Mutable(); + + public MqlScriptScope(@NotNull MqlScope query, @NotNull Mutable actor, @NotNull MqlScope context) { + this.query = query; + this.actor = actor; + this.context = context; + } + + @Override + public @NotNull MqlValue get(@NotNull String name) { + return switch (name) { + case "math", "m" -> MqlMath.INSTANCE; + case "query", "q" -> query; + case "temp", "t" -> temp; + case "variable", "v" -> actor; + case "context", "c" -> context; + default -> throw new MqlRuntimeError("unknown environment object: " + name); + }; + } +} diff --git a/modules/entity/src/main/resources/data/behaviors.json b/modules/entity/src/main/resources/data/behaviors.json index 2f46c2f2..7c5bd75f 100644 --- a/modules/entity/src/main/resources/data/behaviors.json +++ b/modules/entity/src/main/resources/data/behaviors.json @@ -25,7 +25,7 @@ "type": "unnamed:wander_in_region" } ], - "can_interrupt": true + "interrupt": true } } } diff --git a/modules/entity/src/main/resources/data/entities.json b/modules/entity/src/main/resources/data/entities.json index 78c0de0e..aa0a77c6 100644 --- a/modules/entity/src/main/resources/data/entities.json +++ b/modules/entity/src/main/resources/data/entities.json @@ -1,6 +1,7 @@ [ { "id": "unnamed:slime", + "model": "minecraft:slime", "behavior": "unnamed:slime_generic" } ] \ No newline at end of file diff --git a/modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestNavigatorBasicIntegration.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestNavigatorBasicIntegration.java index 09dd2a9a..07730e2a 100644 --- a/modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestNavigatorBasicIntegration.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestNavigatorBasicIntegration.java @@ -1,5 +1,6 @@ package net.hollowcube.entity.brain.navigator; +import net.hollowcube.entity.navigator.Navigator; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; diff --git a/modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestUtil.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestUtil.java index eae727a2..3163a35b 100644 --- a/modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestUtil.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/navigator/TestUtil.java @@ -1,5 +1,6 @@ package net.hollowcube.entity.brain.navigator; +import net.hollowcube.entity.navigator.Navigator; import net.minestom.server.entity.Entity; import org.junit.jupiter.params.provider.Arguments; import net.hollowcube.entity.motion.MotionNavigator; diff --git a/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestIdleTask.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestIdleTask.java index 480bed79..b0a720e1 100644 --- a/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestIdleTask.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestIdleTask.java @@ -2,6 +2,8 @@ import net.hollowcube.data.number.NumberProvider; import net.hollowcube.entity.brain.task.test.MockBrain; +import net.hollowcube.entity.task.IdleTask; +import net.hollowcube.entity.task.Task; import org.junit.jupiter.api.Test; import static com.google.common.truth.Truth.assertThat; diff --git a/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestSequenceTask.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestSequenceTask.java index e8ecca9d..c9adbc44 100644 --- a/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestSequenceTask.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/TestSequenceTask.java @@ -3,6 +3,8 @@ import net.hollowcube.entity.brain.task.test.MockBrain; import net.hollowcube.entity.brain.task.test.MockTask; import net.hollowcube.entity.brain.task.test.TaskSubject; +import net.hollowcube.entity.task.SequenceTask; +import net.hollowcube.entity.task.Task; import org.junit.jupiter.api.Test; import java.util.List; diff --git a/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockBrain.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockBrain.java index 0e03dd27..769ef104 100644 --- a/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockBrain.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockBrain.java @@ -1,10 +1,9 @@ package net.hollowcube.entity.brain.task.test; -import net.hollowcube.entity.brain.navigator.Navigator; +import net.hollowcube.entity.navigator.Navigator; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import org.jetbrains.annotations.NotNull; -import net.hollowcube.entity.brain.Brain; public class MockBrain implements Brain { diff --git a/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockTask.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockTask.java index 2e636e0e..3c2c6b33 100644 --- a/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockTask.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/MockTask.java @@ -1,8 +1,7 @@ package net.hollowcube.entity.brain.task.test; -import net.hollowcube.entity.brain.task.AbstractTask; +import net.hollowcube.entity.task.AbstractTask; import org.jetbrains.annotations.NotNull; -import net.hollowcube.entity.brain.Brain; public class MockTask extends AbstractTask { private final Boolean pass; diff --git a/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/TaskSubject.java b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/TaskSubject.java index 5efa8c32..fcdeadee 100644 --- a/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/TaskSubject.java +++ b/modules/entity/src/test/java/net/hollowcube/entity/brain/task/test/TaskSubject.java @@ -4,7 +4,7 @@ import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import com.google.common.truth.Truth; -import net.hollowcube.entity.brain.task.Task; +import net.hollowcube.entity.task.Task; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable;