From 366349b2ab350ff35e20e0b2f3628722fa93a0ea Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 6 Mar 2026 17:04:00 +0000 Subject: [PATCH 1/9] feat: add debug shape API for visualizing wireframe primitives Add debug shape value types (box, sphere, circle, line, arrow, text) and dimension methods for adding/removing/clearing debug shapes. --- include/endstone/debug/shape.h | 30 +++++++++ include/endstone/debug/shape/arrow.h | 91 +++++++++++++++++++++++++++ include/endstone/debug/shape/box.h | 46 ++++++++++++++ include/endstone/debug/shape/circle.h | 29 +++++++++ include/endstone/debug/shape/line.h | 44 +++++++++++++ include/endstone/debug/shape/shape.h | 61 ++++++++++++++++++ include/endstone/debug/shape/sphere.h | 29 +++++++++ include/endstone/debug/shape/text.h | 48 ++++++++++++++ include/endstone/endstone.hpp | 8 +++ include/endstone/level/dimension.h | 34 ++++++++++ 10 files changed, 420 insertions(+) create mode 100644 include/endstone/debug/shape.h create mode 100644 include/endstone/debug/shape/arrow.h create mode 100644 include/endstone/debug/shape/box.h create mode 100644 include/endstone/debug/shape/circle.h create mode 100644 include/endstone/debug/shape/line.h create mode 100644 include/endstone/debug/shape/shape.h create mode 100644 include/endstone/debug/shape/sphere.h create mode 100644 include/endstone/debug/shape/text.h diff --git a/include/endstone/debug/shape.h b/include/endstone/debug/shape.h new file mode 100644 index 0000000000..4ccd06027d --- /dev/null +++ b/include/endstone/debug/shape.h @@ -0,0 +1,30 @@ +// Copyright (c) 2024, The Endstone Project. (https://endstone.dev) All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "endstone/debug/shape/arrow.h" +#include "endstone/debug/shape/box.h" +#include "endstone/debug/shape/circle.h" +#include "endstone/debug/shape/line.h" +#include "endstone/debug/shape/sphere.h" +#include "endstone/debug/shape/text.h" + +namespace endstone { + +using DebugShapeVariant = std::variant; + +} // namespace endstone diff --git a/include/endstone/debug/shape/arrow.h b/include/endstone/debug/shape/arrow.h new file mode 100644 index 0000000000..c7fa8f18fc --- /dev/null +++ b/include/endstone/debug/shape/arrow.h @@ -0,0 +1,91 @@ +// Copyright (c) 2024, The Endstone Project. (https://endstone.dev) All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "endstone/debug/shape/shape.h" + +namespace endstone { + +/** + * @brief Represents a debug arrow shape. + * An arrow is a line segment with a cone head at the end point. + */ +class DebugArrow : public DebugShape { +public: + /** + * @brief Gets the length of the arrow. + */ + [[nodiscard]] float getLength() const { return length_; } + + /** + * @brief Sets the length of the arrow. + */ + DebugArrow &setLength(float length) + { + length_ = length; + return *this; + } + + /** + * @brief Gets the length of the arrow's head. + */ + [[nodiscard]] float getHeadLength() const { return head_length_; } + + /** + * @brief Sets the length of the arrow's head. + */ + DebugArrow &setHeadLength(float length) + { + head_length_ = length; + return *this; + } + + /** + * @brief Gets the radius of the arrow's head. + */ + [[nodiscard]] float getHeadRadius() const { return head_radius_; } + + /** + * @brief Sets the radius of the arrow's head. + */ + DebugArrow &setHeadRadius(float radius) + { + head_radius_ = radius; + return *this; + } + + /** + * @brief Gets the number of segments for the arrow head's base circle. + */ + [[nodiscard]] int getHeadSegments() const { return head_segments_; } + + /** + * @brief Sets the number of segments for the arrow head's base circle. + * @param segments value between 3 and 128, default is 4 + */ + DebugArrow &setHeadSegments(int segments) + { + head_segments_ = segments; + return *this; + } + +private: + float length_{1.0F}; + float head_length_{0.5F}; + float head_radius_{0.25F}; + int head_segments_{4}; +}; + +} // namespace endstone diff --git a/include/endstone/debug/shape/box.h b/include/endstone/debug/shape/box.h new file mode 100644 index 0000000000..26b88b781b --- /dev/null +++ b/include/endstone/debug/shape/box.h @@ -0,0 +1,46 @@ +// Copyright (c) 2024, The Endstone Project. (https://endstone.dev) All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "endstone/debug/shape/shape.h" +#include "endstone/util/vector.h" + +namespace endstone { + +/** + * @brief Represents a debug box (cuboid) shape. + * Final rendered size = bound * scale. + */ +class DebugBox : public DebugShape { +public: + /** + * @brief Gets the bounding size of the box. + */ + [[nodiscard]] Vector getBound() const { return bound_; } + + /** + * @brief Sets the bounding size of the box. + */ + DebugBox &setBound(Vector bound) + { + bound_ = bound; + return *this; + } + +private: + Vector bound_{1.0F, 1.0F, 1.0F}; +}; + +} // namespace endstone diff --git a/include/endstone/debug/shape/circle.h b/include/endstone/debug/shape/circle.h new file mode 100644 index 0000000000..6492549094 --- /dev/null +++ b/include/endstone/debug/shape/circle.h @@ -0,0 +1,29 @@ +// Copyright (c) 2024, The Endstone Project. (https://endstone.dev) All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "endstone/debug/shape/shape.h" + +namespace endstone { + +/** + * @brief Represents a debug 2D circle shape. + * Circle radius is controlled via scale. + */ +class DebugCircle : public DebugShape { +public: +}; + +} // namespace endstone diff --git a/include/endstone/debug/shape/line.h b/include/endstone/debug/shape/line.h new file mode 100644 index 0000000000..f15c77e36f --- /dev/null +++ b/include/endstone/debug/shape/line.h @@ -0,0 +1,44 @@ +// Copyright (c) 2024, The Endstone Project. (https://endstone.dev) All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "endstone/debug/shape/shape.h" + +namespace endstone { + +/** + * @brief Represents a debug line segment shape. + */ +class DebugLine : public DebugShape { +public: + /** + * @brief Gets the length of the line segment. + */ + [[nodiscard]] float getLength() const { return length_; } + + /** + * @brief Sets the length of the line segment. + */ + DebugLine &setLength(float length) + { + length_ = length; + return *this; + } + +private: + float length_{1.0F}; +}; + +} // namespace endstone diff --git a/include/endstone/debug/shape/shape.h b/include/endstone/debug/shape/shape.h new file mode 100644 index 0000000000..fa86f3658c --- /dev/null +++ b/include/endstone/debug/shape/shape.h @@ -0,0 +1,61 @@ +// Copyright (c) 2024, The Endstone Project. (https://endstone.dev) All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "endstone/util/color.h" + +namespace endstone { + +/** + * @brief Base class for debug shapes rendered in world space. + */ +template +class DebugShape { +public: + /** + * @brief Gets the color of this shape. + */ + [[nodiscard]] Color getColor() const { return color_; } + + /** + * @brief Sets the color of this shape. + */ + T &setColor(Color color) + { + color_ = color; + return *static_cast(this); + } + + /** + * @brief Gets the uniform scale factor. + */ + [[nodiscard]] float getScale() const { return scale_; } + + /** + * @brief Sets the uniform scale factor. + */ + T &setScale(float scale) + { + scale_ = scale; + return *static_cast(this); + } + +protected: + Color color_{255, 255, 255}; + float scale_{1.0F}; + +}; + +} // namespace endstone diff --git a/include/endstone/debug/shape/sphere.h b/include/endstone/debug/shape/sphere.h new file mode 100644 index 0000000000..2a08c4e251 --- /dev/null +++ b/include/endstone/debug/shape/sphere.h @@ -0,0 +1,29 @@ +// Copyright (c) 2024, The Endstone Project. (https://endstone.dev) All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "endstone/debug/shape/shape.h" + +namespace endstone { + +/** + * @brief Represents a debug sphere shape. + * Sphere radius is controlled via scale. + */ +class DebugSphere : public DebugShape { +public: +}; + +} // namespace endstone diff --git a/include/endstone/debug/shape/text.h b/include/endstone/debug/shape/text.h new file mode 100644 index 0000000000..3ff8a7002b --- /dev/null +++ b/include/endstone/debug/shape/text.h @@ -0,0 +1,48 @@ +// Copyright (c) 2024, The Endstone Project. (https://endstone.dev) All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "endstone/debug/shape/shape.h" + +namespace endstone { + +/** + * @brief Represents a debug billboard text label. + * The label automatically faces the player's camera. + */ +class DebugText : public DebugShape { +public: + /** + * @brief Gets the text displayed by this label. + */ + [[nodiscard]] std::string getText() const { return text_; } + + /** + * @brief Sets the text displayed by this label. + */ + DebugText &setText(std::string text) + { + text_ = std::move(text); + return *this; + } + +private: + std::string text_; +}; + +} // namespace endstone diff --git a/include/endstone/endstone.hpp b/include/endstone/endstone.hpp index b4d6c0ff3f..8f58eaa2de 100644 --- a/include/endstone/endstone.hpp +++ b/include/endstone/endstone.hpp @@ -54,6 +54,14 @@ static_assert(_ITERATOR_DEBUG_LEVEL == 0, #include "command/console_command_sender.h" #include "command/plugin_command.h" #include "damage/damage_source.h" +#include "debug/shape.h" +#include "debug/shape/arrow.h" +#include "debug/shape/box.h" +#include "debug/shape/circle.h" +#include "debug/shape/line.h" +#include "debug/shape/shape.h" +#include "debug/shape/sphere.h" +#include "debug/shape/text.h" #include "effect/effect_type.h" #include "enchantments/enchantment.h" #include "event/actor/actor_damage_event.h" diff --git a/include/endstone/level/dimension.h b/include/endstone/level/dimension.h index cc4b43ec72..be95549c04 100644 --- a/include/endstone/level/dimension.h +++ b/include/endstone/level/dimension.h @@ -14,11 +14,13 @@ #pragma once +#include #include #include #include #include "endstone/block/block.h" +#include "endstone/debug/shape.h" #include "endstone/inventory/item_stack.h" #include "endstone/level/chunk.h" #include "endstone/util/result.h" @@ -139,6 +141,38 @@ class Dimension { * @return A List of all actors currently residing in this dimension */ [[nodiscard]] virtual std::vector getActors() const = 0; + + /** + * @brief Adds a debug shape to this dimension, visible to all players in this dimension. + * + * @param location the location to place the shape + * @param shape the shape to add + * @return the unique id assigned to the shape + */ + virtual std::uint64_t addDebugShape(Location location, DebugShapeVariant shape) = 0; + + /** + * @brief Adds a debug shape with a duration to this dimension, visible to all players in this dimension. + * The shape is automatically removed after the specified duration. + * + * @param location the location to place the shape + * @param shape the shape to add + * @param duration the duration in seconds before auto-removal + * @return the unique id assigned to the shape + */ + virtual std::uint64_t addDebugShape(Location location, DebugShapeVariant shape, float duration) = 0; + + /** + * @brief Removes a specific debug shape from this dimension by its id. + * + * @param id the id returned by addDebugShape() + */ + virtual void removeDebugShape(std::uint64_t id) = 0; + + /** + * @brief Clears all debug shapes from this dimension. + */ + virtual void clearDebugShapes() = 0; }; inline std::unique_ptr Location::getBlock() const From d2e42b1113aa691497b1928532a13520f2adb41d Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 6 Mar 2026 17:27:57 +0000 Subject: [PATCH 2/9] feat: add Python bindings and stub implementation for debug shape API --- endstone/__init__.pyi | 2 + endstone/debug/__init__.py | 8 + endstone/debug/__init__.pyi | 195 ++++++++++++++++++++++++ endstone/level/__init__.pyi | 18 +++ include/endstone/level/dimension.h | 12 +- src/endstone/core/level/dimension.cpp | 16 ++ src/endstone/core/level/dimension.h | 3 + src/endstone/python/CMakeLists.txt | 1 + src/endstone/python/debug.cpp | 65 ++++++++ src/endstone/python/endstone_python.cpp | 4 + src/endstone/python/level.cpp | 9 +- 11 files changed, 321 insertions(+), 12 deletions(-) create mode 100644 endstone/debug/__init__.py create mode 100644 endstone/debug/__init__.pyi create mode 100644 src/endstone/python/debug.cpp diff --git a/endstone/__init__.pyi b/endstone/__init__.pyi index d8b87f44e6..d17a0cc394 100644 --- a/endstone/__init__.pyi +++ b/endstone/__init__.pyi @@ -14,6 +14,7 @@ from . import ( boss, command, damage, + debug, effect, enchantments, event, @@ -64,6 +65,7 @@ __all__ = [ "boss", "command", "damage", + "debug", "effect", "enchantments", "event", diff --git a/endstone/debug/__init__.py b/endstone/debug/__init__.py new file mode 100644 index 0000000000..4806d6fb36 --- /dev/null +++ b/endstone/debug/__init__.py @@ -0,0 +1,8 @@ +import lazy_loader as lazy + +__getattr__, __dir__, __all__ = lazy.attach( + "endstone._python", + submod_attrs={ + "debug": ["DebugBox", "DebugSphere", "DebugCircle", "DebugLine", "DebugArrow", "DebugText"], + }, +) diff --git a/endstone/debug/__init__.pyi b/endstone/debug/__init__.pyi new file mode 100644 index 0000000000..9df915c392 --- /dev/null +++ b/endstone/debug/__init__.pyi @@ -0,0 +1,195 @@ +""" +Classes relating to debug shape drawing utilities. +""" + +from endstone.util import Vector + +__all__ = ["DebugArrow", "DebugBox", "DebugCircle", "DebugLine", "DebugSphere", "DebugText"] + +class DebugBox: + """ + Represents a debug box (cuboid) shape. + """ + def __init__(self) -> None: ... + @property + def color(self) -> tuple[int, ...]: + """ + The color of this shape. + """ + ... + @color.setter + def color(self, arg1: tuple[int, ...]) -> DebugBox: ... + @property + def scale(self) -> float: + """ + The uniform scale factor. + """ + ... + @scale.setter + def scale(self, arg1: float) -> DebugBox: ... + @property + def bound(self) -> Vector: + """ + The bounding size of the box. Final size = bound * scale. + """ + ... + @bound.setter + def bound(self, arg1: Vector) -> DebugBox: ... + +class DebugSphere: + """ + Represents a debug sphere shape. Radius is controlled via scale. + """ + def __init__(self) -> None: ... + @property + def color(self) -> tuple[int, ...]: + """ + The color of this shape. + """ + ... + @color.setter + def color(self, arg1: tuple[int, ...]) -> DebugSphere: ... + @property + def scale(self) -> float: + """ + The uniform scale factor. + """ + ... + @scale.setter + def scale(self, arg1: float) -> DebugSphere: ... + +class DebugCircle: + """ + Represents a debug 2D circle shape. Radius is controlled via scale. + """ + def __init__(self) -> None: ... + @property + def color(self) -> tuple[int, ...]: + """ + The color of this shape. + """ + ... + @color.setter + def color(self, arg1: tuple[int, ...]) -> DebugCircle: ... + @property + def scale(self) -> float: + """ + The uniform scale factor. + """ + ... + @scale.setter + def scale(self, arg1: float) -> DebugCircle: ... + +class DebugLine: + """ + Represents a debug line segment shape. + """ + def __init__(self) -> None: ... + @property + def color(self) -> tuple[int, ...]: + """ + The color of this shape. + """ + ... + @color.setter + def color(self, arg1: tuple[int, ...]) -> DebugLine: ... + @property + def scale(self) -> float: + """ + The uniform scale factor. + """ + ... + @scale.setter + def scale(self, arg1: float) -> DebugLine: ... + @property + def length(self) -> float: + """ + The length of the line segment. + """ + ... + @length.setter + def length(self, arg1: float) -> DebugLine: ... + +class DebugArrow: + """ + Represents a debug arrow shape. + """ + def __init__(self) -> None: ... + @property + def color(self) -> tuple[int, ...]: + """ + The color of this shape. + """ + ... + @color.setter + def color(self, arg1: tuple[int, ...]) -> DebugArrow: ... + @property + def scale(self) -> float: + """ + The uniform scale factor. + """ + ... + @scale.setter + def scale(self, arg1: float) -> DebugArrow: ... + @property + def length(self) -> float: + """ + The length of the arrow. + """ + ... + @length.setter + def length(self, arg1: float) -> DebugArrow: ... + @property + def head_length(self) -> float: + """ + The length of the arrow's head. + """ + ... + @head_length.setter + def head_length(self, arg1: float) -> DebugArrow: ... + @property + def head_radius(self) -> float: + """ + The radius of the arrow's head. + """ + ... + @head_radius.setter + def head_radius(self, arg1: float) -> DebugArrow: ... + @property + def head_segments(self) -> int: + """ + The number of segments for the arrow head's base circle. + """ + ... + @head_segments.setter + def head_segments(self, arg1: int) -> DebugArrow: ... + +class DebugText: + """ + Represents a debug billboard text label. + """ + def __init__(self) -> None: ... + @property + def color(self) -> tuple[int, ...]: + """ + The color of this shape. + """ + ... + @color.setter + def color(self, arg1: tuple[int, ...]) -> DebugText: ... + @property + def scale(self) -> float: + """ + The uniform scale factor. + """ + ... + @scale.setter + def scale(self, arg1: float) -> DebugText: ... + @property + def text(self) -> str: + """ + The text displayed by this label. + """ + ... + @text.setter + def text(self, arg1: str) -> DebugText: ... diff --git a/endstone/level/__init__.pyi b/endstone/level/__init__.pyi index 598a4fe225..1bcb931de7 100644 --- a/endstone/level/__init__.pyi +++ b/endstone/level/__init__.pyi @@ -3,6 +3,7 @@ import typing from endstone.actor import Actor, Item from endstone.block import Block +from endstone.debug import DebugArrow, DebugBox, DebugCircle, DebugLine, DebugSphere, DebugText from endstone.inventory import ItemStack from endstone.util import Vector @@ -134,6 +135,23 @@ class Dimension: Get a list of all actors in this dimension """ ... + def add_debug_shape( + self, location: Location, shape: DebugBox | DebugSphere | DebugCircle | DebugLine | DebugArrow | DebugText + ) -> int: + """ + Adds a debug shape to this dimension, visible to all players. + """ + ... + def remove_debug_shape(self, id: int) -> None: + """ + Removes a specific debug shape from this dimension by its id. + """ + ... + def clear_debug_shapes(self) -> None: + """ + Clears all debug shapes from this dimension. + """ + ... class Location: """ diff --git a/include/endstone/level/dimension.h b/include/endstone/level/dimension.h index be95549c04..158ffa6066 100644 --- a/include/endstone/level/dimension.h +++ b/include/endstone/level/dimension.h @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -151,17 +152,6 @@ class Dimension { */ virtual std::uint64_t addDebugShape(Location location, DebugShapeVariant shape) = 0; - /** - * @brief Adds a debug shape with a duration to this dimension, visible to all players in this dimension. - * The shape is automatically removed after the specified duration. - * - * @param location the location to place the shape - * @param shape the shape to add - * @param duration the duration in seconds before auto-removal - * @return the unique id assigned to the shape - */ - virtual std::uint64_t addDebugShape(Location location, DebugShapeVariant shape, float duration) = 0; - /** * @brief Removes a specific debug shape from this dimension by its id. * diff --git a/src/endstone/core/level/dimension.cpp b/src/endstone/core/level/dimension.cpp index b5cdbb511f..8532fc23b8 100644 --- a/src/endstone/core/level/dimension.cpp +++ b/src/endstone/core/level/dimension.cpp @@ -133,6 +133,22 @@ std::vector EndstoneDimension::getActors() const return result; } +std::uint64_t EndstoneDimension::addDebugShape(Location location, DebugShapeVariant shape) +{ + // TODO(endstone): implement + return 0; +} + +void EndstoneDimension::removeDebugShape(std::uint64_t id) +{ + // TODO(endstone): implement +} + +void EndstoneDimension::clearDebugShapes() +{ + // TODO(endstone): implement +} + ::Dimension &EndstoneDimension::getHandle() const { if (!dimension_.isSet()) { diff --git a/src/endstone/core/level/dimension.h b/src/endstone/core/level/dimension.h index de7256b85f..45f1d173a7 100644 --- a/src/endstone/core/level/dimension.h +++ b/src/endstone/core/level/dimension.h @@ -36,6 +36,9 @@ class EndstoneDimension : public Dimension { [[nodiscard]] Item &dropItem(Location location, const ItemStack &item) override; [[nodiscard]] Actor *spawnActor(Location location, std::string type) override; [[nodiscard]] std::vector getActors() const override; + std::uint64_t addDebugShape(Location location, DebugShapeVariant shape) override; + void removeDebugShape(std::uint64_t id) override; + void clearDebugShapes() override; [[nodiscard]] ::Dimension &getHandle() const; diff --git a/src/endstone/python/CMakeLists.txt b/src/endstone/python/CMakeLists.txt index 1a196ee4c9..d50e02a588 100644 --- a/src/endstone/python/CMakeLists.txt +++ b/src/endstone/python/CMakeLists.txt @@ -12,6 +12,7 @@ pybind11_add_module(_python MODULE boss.cpp command.cpp damage.cpp + debug.cpp effect.cpp enchantments.cpp endstone_python.cpp diff --git a/src/endstone/python/debug.cpp b/src/endstone/python/debug.cpp new file mode 100644 index 0000000000..ffb12fa7ab --- /dev/null +++ b/src/endstone/python/debug.cpp @@ -0,0 +1,65 @@ +// Copyright (c) 2024, The Endstone Project. (https://endstone.dev) All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "endstone_python.h" + +namespace py = pybind11; + +namespace endstone::python { + +void init_debug(py::module_ &m) +{ + py::class_(m, "DebugBox", "Represents a debug box (cuboid) shape.") + .def(py::init<>()) + .def_property("color", &DebugBox::getColor, &DebugBox::setColor, "The color of this shape.") + .def_property("scale", &DebugBox::getScale, &DebugBox::setScale, "The uniform scale factor.") + .def_property("bound", &DebugBox::getBound, &DebugBox::setBound, + "The bounding size of the box. Final size = bound * scale."); + + py::class_(m, "DebugSphere", "Represents a debug sphere shape. Radius is controlled via scale.") + .def(py::init<>()) + .def_property("color", &DebugSphere::getColor, &DebugSphere::setColor, "The color of this shape.") + .def_property("scale", &DebugSphere::getScale, &DebugSphere::setScale, "The uniform scale factor."); + + py::class_(m, "DebugCircle", "Represents a debug 2D circle shape. Radius is controlled via scale.") + .def(py::init<>()) + .def_property("color", &DebugCircle::getColor, &DebugCircle::setColor, "The color of this shape.") + .def_property("scale", &DebugCircle::getScale, &DebugCircle::setScale, "The uniform scale factor."); + + py::class_(m, "DebugLine", "Represents a debug line segment shape.") + .def(py::init<>()) + .def_property("color", &DebugLine::getColor, &DebugLine::setColor, "The color of this shape.") + .def_property("scale", &DebugLine::getScale, &DebugLine::setScale, "The uniform scale factor.") + .def_property("length", &DebugLine::getLength, &DebugLine::setLength, "The length of the line segment."); + + py::class_(m, "DebugArrow", "Represents a debug arrow shape.") + .def(py::init<>()) + .def_property("color", &DebugArrow::getColor, &DebugArrow::setColor, "The color of this shape.") + .def_property("scale", &DebugArrow::getScale, &DebugArrow::setScale, "The uniform scale factor.") + .def_property("length", &DebugArrow::getLength, &DebugArrow::setLength, "The length of the arrow.") + .def_property("head_length", &DebugArrow::getHeadLength, &DebugArrow::setHeadLength, + "The length of the arrow's head.") + .def_property("head_radius", &DebugArrow::getHeadRadius, &DebugArrow::setHeadRadius, + "The radius of the arrow's head.") + .def_property("head_segments", &DebugArrow::getHeadSegments, &DebugArrow::setHeadSegments, + "The number of segments for the arrow head's base circle."); + + py::class_(m, "DebugText", "Represents a debug billboard text label.") + .def(py::init<>()) + .def_property("color", &DebugText::getColor, &DebugText::setColor, "The color of this shape.") + .def_property("scale", &DebugText::getScale, &DebugText::setScale, "The uniform scale factor.") + .def_property("text", &DebugText::getText, &DebugText::setText, "The text displayed by this label."); +} + +} // namespace endstone::python diff --git a/src/endstone/python/endstone_python.cpp b/src/endstone/python/endstone_python.cpp index 51a298565b..bb23234411 100644 --- a/src/endstone/python/endstone_python.cpp +++ b/src/endstone/python/endstone_python.cpp @@ -30,6 +30,7 @@ void init_boss(py::module_ &); void init_color_format(py::module_ &); void init_command(py::module &, py_class &command_sender); void init_damage(py::module_ &); +void init_debug(py::module_ &); void init_effect(py::module_ &); void init_enchantments(py::module_ &); void init_event(py::module_ &, py::class_ &event); @@ -69,6 +70,8 @@ PYBIND11_MODULE(_python, m) // NOLINT(*-use-anonymous-namespace) auto m_command = m.def_submodule("command", "Classes relating to handling specialized non-chat player input."); auto m_damage = m.def_submodule("damage", "Classes relating to damage types and sources applicable to mobs (living entities)."); + auto m_debug = + m.def_submodule("debug", "Classes relating to debug shape drawing utilities."); auto m_effect = m.def_submodule("effect", "Classes relating to the effects that can be applied to entities."); auto m_enchantments = m.def_submodule("enchantments", "Classes relating to the specialized enhancements to ItemStacks."); @@ -152,6 +155,7 @@ PYBIND11_MODULE(_python, m) // NOLINT(*-use-anonymous-namespace) init_attribute(m_attribute); init_color_format(m); init_damage(m_damage); + init_debug(m_debug); init_game_mode(m); init_logger(m); init_lang(m_lang); diff --git a/src/endstone/python/level.cpp b/src/endstone/python/level.cpp index 42e4b6373b..8ccb60094e 100644 --- a/src/endstone/python/level.cpp +++ b/src/endstone/python/level.cpp @@ -65,7 +65,14 @@ void init_level(py::module_ &m, py::class_ &level, py::class_ .def("spawn_actor", &Dimension::spawnActor, py::arg("location"), py::arg("type"), py::return_value_policy::reference, "Creates an actor at the given Location") .def_property_readonly("actors", &Dimension::getActors, py::return_value_policy::reference_internal, - "Get a list of all actors in this dimension"); + "Get a list of all actors in this dimension") + .def("add_debug_shape", &Dimension::addDebugShape, + py::arg("location"), py::arg("shape"), + "Adds a debug shape to this dimension, visible to all players.") + .def("remove_debug_shape", &Dimension::removeDebugShape, py::arg("id"), + "Removes a specific debug shape from this dimension by its id.") + .def("clear_debug_shapes", &Dimension::clearDebugShapes, + "Clears all debug shapes from this dimension."); level.def_property_readonly("name", &Level::getName, "Gets the unique name of this level") .def_property_readonly("actors", &Level::getActors, "Get a list of all actors in this level", From e9587b88cc39742163601996383315fd2268287d Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 6 Mar 2026 17:38:20 +0000 Subject: [PATCH 3/9] feat: move debug shape methods from Dimension to Player interface --- endstone/__init__.pyi | 17 +++++++++++++++++ endstone/level/__init__.pyi | 18 ------------------ include/endstone/level/dimension.h | 23 ----------------------- include/endstone/player.h | 22 ++++++++++++++++++++++ src/endstone/core/level/dimension.cpp | 16 ---------------- src/endstone/core/level/dimension.h | 4 ---- src/endstone/core/player.cpp | 16 ++++++++++++++++ src/endstone/core/player.h | 3 +++ src/endstone/python/endstone_python.cpp | 14 +++++++++----- src/endstone/python/level.cpp | 9 +-------- 10 files changed, 68 insertions(+), 74 deletions(-) diff --git a/endstone/__init__.pyi b/endstone/__init__.pyi index d17a0cc394..06de39234e 100644 --- a/endstone/__init__.pyi +++ b/endstone/__init__.pyi @@ -626,6 +626,23 @@ class Player(Mob): Sends a packet to the player. """ ... + def add_debug_shape( + self, location: Location, shape: debug.DebugBox | debug.DebugSphere | debug.DebugCircle | debug.DebugLine | debug.DebugArrow | debug.DebugText + ) -> int: + """ + Adds a debug shape visible only to this player. + """ + ... + def remove_debug_shape(self, id: int) -> None: + """ + Removes a specific debug shape from this player by its id. + """ + ... + def remove_debug_shapes(self) -> None: + """ + Removes all debug shapes visible to this player. + """ + ... class ColorFormat: """ diff --git a/endstone/level/__init__.pyi b/endstone/level/__init__.pyi index 1bcb931de7..598a4fe225 100644 --- a/endstone/level/__init__.pyi +++ b/endstone/level/__init__.pyi @@ -3,7 +3,6 @@ import typing from endstone.actor import Actor, Item from endstone.block import Block -from endstone.debug import DebugArrow, DebugBox, DebugCircle, DebugLine, DebugSphere, DebugText from endstone.inventory import ItemStack from endstone.util import Vector @@ -135,23 +134,6 @@ class Dimension: Get a list of all actors in this dimension """ ... - def add_debug_shape( - self, location: Location, shape: DebugBox | DebugSphere | DebugCircle | DebugLine | DebugArrow | DebugText - ) -> int: - """ - Adds a debug shape to this dimension, visible to all players. - """ - ... - def remove_debug_shape(self, id: int) -> None: - """ - Removes a specific debug shape from this dimension by its id. - """ - ... - def clear_debug_shapes(self) -> None: - """ - Clears all debug shapes from this dimension. - """ - ... class Location: """ diff --git a/include/endstone/level/dimension.h b/include/endstone/level/dimension.h index 158ffa6066..6d89bb4af2 100644 --- a/include/endstone/level/dimension.h +++ b/include/endstone/level/dimension.h @@ -14,14 +14,11 @@ #pragma once -#include #include -#include #include #include #include "endstone/block/block.h" -#include "endstone/debug/shape.h" #include "endstone/inventory/item_stack.h" #include "endstone/level/chunk.h" #include "endstone/util/result.h" @@ -143,26 +140,6 @@ class Dimension { */ [[nodiscard]] virtual std::vector getActors() const = 0; - /** - * @brief Adds a debug shape to this dimension, visible to all players in this dimension. - * - * @param location the location to place the shape - * @param shape the shape to add - * @return the unique id assigned to the shape - */ - virtual std::uint64_t addDebugShape(Location location, DebugShapeVariant shape) = 0; - - /** - * @brief Removes a specific debug shape from this dimension by its id. - * - * @param id the id returned by addDebugShape() - */ - virtual void removeDebugShape(std::uint64_t id) = 0; - - /** - * @brief Clears all debug shapes from this dimension. - */ - virtual void clearDebugShapes() = 0; }; inline std::unique_ptr Location::getBlock() const diff --git a/include/endstone/player.h b/include/endstone/player.h index 02d58eb4c2..ab68d9a02d 100644 --- a/include/endstone/player.h +++ b/include/endstone/player.h @@ -21,6 +21,7 @@ #include #include "endstone/actor/mob.h" +#include "endstone/debug/shape.h" #include "endstone/form/action_form.h" #include "endstone/form/message_form.h" #include "endstone/form/modal_form.h" @@ -487,6 +488,27 @@ class Player : public Mob { * @param map The map to send */ virtual void sendMap(MapView &map) = 0; + + /** + * @brief Adds a debug shape visible only to this player. + * + * @param location the location to place the shape + * @param shape the shape to add + * @return the unique id assigned to the shape + */ + virtual std::uint64_t addDebugShape(Location location, DebugShapeVariant shape) = 0; + + /** + * @brief Removes a specific debug shape from this player by its id. + * + * @param id the id returned by addDebugShape() + */ + virtual void removeDebugShape(std::uint64_t id) = 0; + + /** + * @brief Removes all debug shapes visible to this player. + */ + virtual void removeDebugShapes() = 0; }; } // namespace endstone diff --git a/src/endstone/core/level/dimension.cpp b/src/endstone/core/level/dimension.cpp index 8532fc23b8..b5cdbb511f 100644 --- a/src/endstone/core/level/dimension.cpp +++ b/src/endstone/core/level/dimension.cpp @@ -133,22 +133,6 @@ std::vector EndstoneDimension::getActors() const return result; } -std::uint64_t EndstoneDimension::addDebugShape(Location location, DebugShapeVariant shape) -{ - // TODO(endstone): implement - return 0; -} - -void EndstoneDimension::removeDebugShape(std::uint64_t id) -{ - // TODO(endstone): implement -} - -void EndstoneDimension::clearDebugShapes() -{ - // TODO(endstone): implement -} - ::Dimension &EndstoneDimension::getHandle() const { if (!dimension_.isSet()) { diff --git a/src/endstone/core/level/dimension.h b/src/endstone/core/level/dimension.h index 45f1d173a7..8289cd1a39 100644 --- a/src/endstone/core/level/dimension.h +++ b/src/endstone/core/level/dimension.h @@ -36,10 +36,6 @@ class EndstoneDimension : public Dimension { [[nodiscard]] Item &dropItem(Location location, const ItemStack &item) override; [[nodiscard]] Actor *spawnActor(Location location, std::string type) override; [[nodiscard]] std::vector getActors() const override; - std::uint64_t addDebugShape(Location location, DebugShapeVariant shape) override; - void removeDebugShape(std::uint64_t id) override; - void clearDebugShapes() override; - [[nodiscard]] ::Dimension &getHandle() const; private: diff --git a/src/endstone/core/player.cpp b/src/endstone/core/player.cpp index 717d7dac9e..e16e162fb4 100644 --- a/src/endstone/core/player.cpp +++ b/src/endstone/core/player.cpp @@ -629,6 +629,22 @@ void EndstonePlayer::sendMap(MapView &map) getHandle().sendNetworkPacket(*packet); } +std::uint64_t EndstonePlayer::addDebugShape(Location location, DebugShapeVariant shape) +{ + // TODO(endstone): implement + return 0; +} + +void EndstonePlayer::removeDebugShape(std::uint64_t id) +{ + // TODO(endstone): implement +} + +void EndstonePlayer::removeDebugShapes() +{ + // TODO(endstone): implement +} + bool EndstonePlayer::handlePacket(Packet &packet) { switch (packet.getId()) { diff --git a/src/endstone/core/player.h b/src/endstone/core/player.h index d4305d4ac6..125cdb2966 100644 --- a/src/endstone/core/player.h +++ b/src/endstone/core/player.h @@ -120,6 +120,9 @@ class EndstonePlayer : public EndstoneMobBase { void closeForm() override; void sendPacket(int packet_id, std::string_view payload) const override; void sendMap(MapView &map) override; + std::uint64_t addDebugShape(Location location, DebugShapeVariant shape) override; + void removeDebugShape(std::uint64_t id) override; + void removeDebugShapes() override; bool handlePacket(Packet &packet); void onFormClose(std::uint32_t form_id, PlayerFormCloseReason reason); diff --git a/src/endstone/python/endstone_python.cpp b/src/endstone/python/endstone_python.cpp index bb23234411..1f40ceb78d 100644 --- a/src/endstone/python/endstone_python.cpp +++ b/src/endstone/python/endstone_python.cpp @@ -70,8 +70,7 @@ PYBIND11_MODULE(_python, m) // NOLINT(*-use-anonymous-namespace) auto m_command = m.def_submodule("command", "Classes relating to handling specialized non-chat player input."); auto m_damage = m.def_submodule("damage", "Classes relating to damage types and sources applicable to mobs (living entities)."); - auto m_debug = - m.def_submodule("debug", "Classes relating to debug shape drawing utilities."); + auto m_debug = m.def_submodule("debug", "Classes relating to debug shape drawing utilities."); auto m_effect = m.def_submodule("effect", "Classes relating to the effects that can be applied to entities."); auto m_enchantments = m.def_submodule("enchantments", "Classes relating to the specialized enhancements to ItemStacks."); @@ -136,8 +135,8 @@ PYBIND11_MODULE(_python, m) // NOLINT(*-use-anonymous-namespace) auto block = py::class_(m_block, "Block", "Represents a block."); auto command_sender = py_class(m_command, "CommandSender", "Represents a command sender."); auto actor = py_class(m_actor, "Actor", "Represents a base actor in the level."); - auto mob = py_class(m_actor, "Mob", - "Represents a mobile entity (i.e. living entity), such as a monster or player."); + auto mob = + py_class(m_actor, "Mob", "Represents a mobile entity (i.e. living entity), such as a monster or player."); py::class_( m, "OfflinePlayer", "Represents a reference to a player identity and the data belonging to a player that is stored on the disk and " @@ -473,7 +472,12 @@ void init_player(py::module_ &m, py_class &player) [](const Player &self, const int packet_id, const py::bytes &payload) { return self.sendPacket(packet_id, payload); }, - py::arg("packet_id"), py::arg("payload"), "Sends a packet to the player."); + py::arg("packet_id"), py::arg("payload"), "Sends a packet to the player.") + .def("add_debug_shape", &Player::addDebugShape, py::arg("location"), py::arg("shape"), + "Adds a debug shape visible only to this player.") + .def("remove_debug_shape", &Player::removeDebugShape, py::arg("id"), + "Removes a specific debug shape from this player by its id.") + .def("remove_debug_shapes", &Player::removeDebugShapes, "Removes all debug shapes visible to this player."); } } // namespace endstone::python diff --git a/src/endstone/python/level.cpp b/src/endstone/python/level.cpp index 8ccb60094e..42e4b6373b 100644 --- a/src/endstone/python/level.cpp +++ b/src/endstone/python/level.cpp @@ -65,14 +65,7 @@ void init_level(py::module_ &m, py::class_ &level, py::class_ .def("spawn_actor", &Dimension::spawnActor, py::arg("location"), py::arg("type"), py::return_value_policy::reference, "Creates an actor at the given Location") .def_property_readonly("actors", &Dimension::getActors, py::return_value_policy::reference_internal, - "Get a list of all actors in this dimension") - .def("add_debug_shape", &Dimension::addDebugShape, - py::arg("location"), py::arg("shape"), - "Adds a debug shape to this dimension, visible to all players.") - .def("remove_debug_shape", &Dimension::removeDebugShape, py::arg("id"), - "Removes a specific debug shape from this dimension by its id.") - .def("clear_debug_shapes", &Dimension::clearDebugShapes, - "Clears all debug shapes from this dimension."); + "Get a list of all actors in this dimension"); level.def_property_readonly("name", &Level::getName, "Gets the unique name of this level") .def_property_readonly("actors", &Level::getActors, "Get a list of all actors in this level", From 9f0a1781973fc12bfb7e243f2a61f3eb006b92bf Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 6 Mar 2026 17:41:39 +0000 Subject: [PATCH 4/9] feat: add debug shape value types to Player API --- endstone/__init__.pyi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/endstone/__init__.pyi b/endstone/__init__.pyi index 06de39234e..d57e3c4b5a 100644 --- a/endstone/__init__.pyi +++ b/endstone/__init__.pyi @@ -37,6 +37,7 @@ from .ban import IpBanList, PlayerBanList from .block import BlockData from .boss import BarColor, BarFlag, BarStyle, BossBar from .command import CommandSender, ConsoleCommandSender +from .debug import DebugArrow, DebugBox, DebugCircle, DebugLine, DebugSphere, DebugText from .form import ActionForm, MessageForm, ModalForm from .inventory import Inventory, ItemFactory, PlayerInventory from .lang import Language, Translatable @@ -627,7 +628,7 @@ class Player(Mob): """ ... def add_debug_shape( - self, location: Location, shape: debug.DebugBox | debug.DebugSphere | debug.DebugCircle | debug.DebugLine | debug.DebugArrow | debug.DebugText + self, location: Location, shape: DebugBox | DebugSphere | DebugCircle | DebugLine | DebugArrow | DebugText ) -> int: """ Adds a debug shape visible only to this player. From 3220852fb68a8b7fdb2c30730138d7dd873ef900 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 6 Mar 2026 17:58:27 +0000 Subject: [PATCH 5/9] feat: add debug shape ID tracker to player and DebugDrawerPacket header --- .../network/packet/debug_drawer_packet.h | 94 +++++++++++++++++++ src/endstone/core/player.cpp | 4 +- src/endstone/core/player.h | 2 + 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/bedrock/network/packet/debug_drawer_packet.h diff --git a/src/bedrock/network/packet/debug_drawer_packet.h b/src/bedrock/network/packet/debug_drawer_packet.h new file mode 100644 index 0000000000..517bb49cf3 --- /dev/null +++ b/src/bedrock/network/packet/debug_drawer_packet.h @@ -0,0 +1,94 @@ +// Copyright (c) 2024, The Endstone Project. (https://endstone.dev) All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "bedrock/core/math/color.h" +#include "bedrock/core/math/vec3.h" +#include "bedrock/network/packet.h" +#include "bedrock/world/actor/actor_runtime_id.h" +#include "bedrock/world/level/dimension/dimension_type.h" + +namespace ScriptModuleDebugUtilities { +enum class ScriptDebugShapeType : uint8_t { + Line = 0, + Box = 1, + Sphere = 2, + Circle = 3, + Text = 4, + Arrow = 5, + NumShapeTypes = 6, +}; +class ScriptDebugShape; +} // namespace ScriptModuleDebugUtilities + +namespace cereal { +struct NullType {}; +} // namespace cereal + +struct ArrowDataPayload { + std::optional end_location; + std::optional arrow_head_length; + std::optional arrow_head_radius; + std::optional num_segments; +}; + +struct TextDataPayload { + std::string text; +}; + +struct BoxDataPayload { + Vec3 box_bound; +}; + +struct LineDataPayload { + Vec3 end_location; +}; + +struct SphereDataPayload { + std::byte num_segments; +}; + +struct ShapeDataPayload { + std::uint64_t network_id; + std::optional shape_type; + std::optional location; + std::optional rotation; + std::optional scale; + std::optional color; + std::optional time_left_total_sec; + std::optional> dimension_id; + std::optional attached_to_id; + std::variant + extra_data_payload; +}; + +struct DebugDrawerPacketPayload { + std::vector shapes; +}; + +class DebugDrawerPacket : public Packet { +public: + static constexpr bool SHARE_WITH_HANDLER = false; + DebugDrawerPacketPayload payload; + SerializationMode serialization_mode; +}; diff --git a/src/endstone/core/player.cpp b/src/endstone/core/player.cpp index e16e162fb4..0b7cf13a2e 100644 --- a/src/endstone/core/player.cpp +++ b/src/endstone/core/player.cpp @@ -20,6 +20,7 @@ #include "bedrock/entity/components/user_entity_identifier_component.h" #include "bedrock/network/packet.h" #include "bedrock/network/packet/clientbound_map_item_data_packet.h" +#include "bedrock/network/packet/debug_drawer_packet.h" #include "bedrock/network/packet/emote_packet.h" #include "bedrock/network/packet/mob_equipment_packet.h" #include "bedrock/network/packet/modal_form_request_packet.h" @@ -631,8 +632,9 @@ void EndstonePlayer::sendMap(MapView &map) std::uint64_t EndstonePlayer::addDebugShape(Location location, DebugShapeVariant shape) { + auto id = debug_shape_ids_--; // TODO(endstone): implement - return 0; + return id; } void EndstonePlayer::removeDebugShape(std::uint64_t id) diff --git a/src/endstone/core/player.h b/src/endstone/core/player.h index 125cdb2966..a9e575c783 100644 --- a/src/endstone/core/player.h +++ b/src/endstone/core/player.h @@ -14,6 +14,7 @@ #pragma once +#include #include #include @@ -146,6 +147,7 @@ class EndstonePlayer : public EndstoneMobBase { std::string game_version_; std::uint32_t form_ids_ = 0xffff; // Set to a large value to avoid collision with forms created by script api std::unordered_map forms_; + std::uint64_t debug_shape_ids_ = std::numeric_limits::max(); // Decrement to avoid collision with script api bool spawned_ = false; bool last_op_status_ = false; }; From fb9e191a557df1e5446f46ae91afefb1703cf0b3 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 6 Mar 2026 18:04:52 +0000 Subject: [PATCH 6/9] feat: implement player debug shape methods via DebugDrawerPacket --- .../network/packet/debug_drawer_packet.h | 6 +- src/endstone/core/player.cpp | 83 ++++++++++++++++++- src/endstone/core/player.h | 2 + 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/bedrock/network/packet/debug_drawer_packet.h b/src/bedrock/network/packet/debug_drawer_packet.h index 517bb49cf3..78ec6237ce 100644 --- a/src/bedrock/network/packet/debug_drawer_packet.h +++ b/src/bedrock/network/packet/debug_drawer_packet.h @@ -28,7 +28,7 @@ #include "bedrock/world/level/dimension/dimension_type.h" namespace ScriptModuleDebugUtilities { -enum class ScriptDebugShapeType : uint8_t { +enum class ScriptDebugShapeType : std::uint8_t { Line = 0, Box = 1, Sphere = 2, @@ -48,7 +48,7 @@ struct ArrowDataPayload { std::optional end_location; std::optional arrow_head_length; std::optional arrow_head_radius; - std::optional num_segments; + std::optional num_segments; }; struct TextDataPayload { @@ -75,7 +75,7 @@ struct ShapeDataPayload { std::optional scale; std::optional color; std::optional time_left_total_sec; - std::optional> dimension_id; + std::optional dimension_id; std::optional attached_to_id; std::variant diff --git a/src/endstone/core/player.cpp b/src/endstone/core/player.cpp index 0b7cf13a2e..318719e2ae 100644 --- a/src/endstone/core/player.cpp +++ b/src/endstone/core/player.cpp @@ -44,6 +44,7 @@ #include "endstone/core/game_mode.h" #include "endstone/core/inventory/item_stack.h" #include "endstone/core/inventory/player_inventory.h" +#include "endstone/core/level/dimension.h" #include "endstone/core/map/map_view.h" #include "endstone/core/message.h" #include "endstone/core/network/data_packet.h" @@ -632,19 +633,95 @@ void EndstonePlayer::sendMap(MapView &map) std::uint64_t EndstonePlayer::addDebugShape(Location location, DebugShapeVariant shape) { + using ShapeType = ScriptModuleDebugUtilities::ScriptDebugShapeType; + auto id = debug_shape_ids_--; - // TODO(endstone): implement + auto &dimension = static_cast(location.getDimension()); + + ShapeDataPayload data; + data.network_id = id; + data.location = Vec3{location.getX(), location.getY(), location.getZ()}; + data.rotation = Vec3{location.getPitch(), location.getYaw(), 0}; + data.dimension_id = dimension.getHandle().getDimensionId(); + + std::visit( + [&data](auto &&s) { + using T = std::decay_t; + auto c = s.getColor(); + data.color = + mce::Color(static_cast(c.getRed()) / 255.0F, static_cast(c.getGreen()) / 255.0F, + static_cast(c.getBlue()) / 255.0F, static_cast(c.getAlpha()) / 255.0F); + data.scale = s.getScale(); + + if constexpr (std::is_same_v) { + data.shape_type = ShapeType::Box; + auto b = s.getBound(); + data.extra_data_payload = BoxDataPayload{Vec3{b.getX(), b.getY(), b.getZ()}}; + } + else if constexpr (std::is_same_v) { + data.shape_type = ShapeType::Sphere; + } + else if constexpr (std::is_same_v) { + data.shape_type = ShapeType::Circle; + } + else if constexpr (std::is_same_v) { + data.shape_type = ShapeType::Line; + // Compute end location from location + direction * length + // For line, we store the end location relative offset + data.extra_data_payload = LineDataPayload{Vec3{0, 0, s.getLength()}}; + } + else if constexpr (std::is_same_v) { + data.shape_type = ShapeType::Arrow; + ArrowDataPayload arrow; + arrow.end_location = Vec3{0, 0, s.getLength()}; + arrow.arrow_head_length = s.getHeadLength(); + arrow.arrow_head_radius = s.getHeadRadius(); + arrow.num_segments = static_cast(s.getHeadSegments()); + data.extra_data_payload = arrow; + } + else if constexpr (std::is_same_v) { + data.shape_type = ShapeType::Text; + data.extra_data_payload = TextDataPayload{s.getText()}; + } + }, + shape); + + auto packet = MinecraftPackets::createPacket(MinecraftPacketIds::ServerScriptDebugDrawerPacket); + auto &pk = static_cast(*packet); + pk.payload.shapes.emplace_back(std::move(data)); + getHandle().sendNetworkPacket(*packet); + debug_shapes_.insert(id); return id; } void EndstonePlayer::removeDebugShape(std::uint64_t id) { - // TODO(endstone): implement + if (debug_shapes_.erase(id) == 0) { + return; + } + + ShapeDataPayload shape_data; + shape_data.network_id = id; + shape_data.time_left_total_sec = 0; + auto packet = MinecraftPackets::createPacket(MinecraftPacketIds::ServerScriptDebugDrawerPacket); + auto &pk = static_cast(*packet); + pk.payload.shapes.emplace_back(std::move(shape_data)); + getHandle().sendNetworkPacket(*packet); } void EndstonePlayer::removeDebugShapes() { - // TODO(endstone): implement + for (auto id : debug_shapes_) { + ShapeDataPayload shape_data; + shape_data.network_id = id; + shape_data.time_left_total_sec = 0; + + auto packet = MinecraftPackets::createPacket(MinecraftPacketIds::ServerScriptDebugDrawerPacket); + auto &pk = static_cast(*packet); + pk.payload.shapes.push_back(std::move(shape_data)); + getHandle().sendNetworkPacket(*packet); + } + debug_shapes_.clear(); } bool EndstonePlayer::handlePacket(Packet &packet) diff --git a/src/endstone/core/player.h b/src/endstone/core/player.h index a9e575c783..fcfa2a677b 100644 --- a/src/endstone/core/player.h +++ b/src/endstone/core/player.h @@ -16,6 +16,7 @@ #include #include +#include #include @@ -148,6 +149,7 @@ class EndstonePlayer : public EndstoneMobBase { std::uint32_t form_ids_ = 0xffff; // Set to a large value to avoid collision with forms created by script api std::unordered_map forms_; std::uint64_t debug_shape_ids_ = std::numeric_limits::max(); // Decrement to avoid collision with script api + std::unordered_set debug_shapes_; bool spawned_ = false; bool last_op_status_ = false; }; From ebac981efa1b46c0de8823c25bf1042c17a9d499 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 6 Mar 2026 18:10:17 +0000 Subject: [PATCH 7/9] fix: compute end location for line/arrow using direction vector --- src/endstone/core/player.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/endstone/core/player.cpp b/src/endstone/core/player.cpp index 318719e2ae..3c90f0a0f8 100644 --- a/src/endstone/core/player.cpp +++ b/src/endstone/core/player.cpp @@ -645,7 +645,7 @@ std::uint64_t EndstonePlayer::addDebugShape(Location location, DebugShapeVariant data.dimension_id = dimension.getHandle().getDimensionId(); std::visit( - [&data](auto &&s) { + [&data, &location](auto &&s) { using T = std::decay_t; auto c = s.getColor(); data.color = @@ -666,14 +666,20 @@ std::uint64_t EndstonePlayer::addDebugShape(Location location, DebugShapeVariant } else if constexpr (std::is_same_v) { data.shape_type = ShapeType::Line; - // Compute end location from location + direction * length - // For line, we store the end location relative offset - data.extra_data_payload = LineDataPayload{Vec3{0, 0, s.getLength()}}; + auto dir = location.getDirection(); + auto end = Vec3{location.getX() + dir.getX() * s.getLength(), + location.getY() + dir.getY() * s.getLength(), + location.getZ() + dir.getZ() * s.getLength()}; + data.extra_data_payload = LineDataPayload{end}; } else if constexpr (std::is_same_v) { data.shape_type = ShapeType::Arrow; + auto dir = location.getDirection(); + auto end = Vec3{location.getX() + dir.getX() * s.getLength(), + location.getY() + dir.getY() * s.getLength(), + location.getZ() + dir.getZ() * s.getLength()}; ArrowDataPayload arrow; - arrow.end_location = Vec3{0, 0, s.getLength()}; + arrow.end_location = end; arrow.arrow_head_length = s.getHeadLength(); arrow.arrow_head_radius = s.getHeadRadius(); arrow.num_segments = static_cast(s.getHeadSegments()); From b03353ac49b323064a166239f83dae4b5e99890b Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 6 Mar 2026 18:17:07 +0000 Subject: [PATCH 8/9] fix: batch removeDebugShapes into single packet and clamp head segments --- include/endstone/debug/shape/arrow.h | 4 +++- src/endstone/core/player.cpp | 13 ++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/include/endstone/debug/shape/arrow.h b/include/endstone/debug/shape/arrow.h index c7fa8f18fc..4acf12cf92 100644 --- a/include/endstone/debug/shape/arrow.h +++ b/include/endstone/debug/shape/arrow.h @@ -14,6 +14,8 @@ #pragma once +#include + #include "endstone/debug/shape/shape.h" namespace endstone { @@ -77,7 +79,7 @@ class DebugArrow : public DebugShape { */ DebugArrow &setHeadSegments(int segments) { - head_segments_ = segments; + head_segments_ = std::clamp(segments, 3, 128); return *this; } diff --git a/src/endstone/core/player.cpp b/src/endstone/core/player.cpp index 3c90f0a0f8..713609c565 100644 --- a/src/endstone/core/player.cpp +++ b/src/endstone/core/player.cpp @@ -717,16 +717,19 @@ void EndstonePlayer::removeDebugShape(std::uint64_t id) void EndstonePlayer::removeDebugShapes() { + if (debug_shapes_.empty()) { + return; + } + + auto packet = MinecraftPackets::createPacket(MinecraftPacketIds::ServerScriptDebugDrawerPacket); + auto &pk = static_cast(*packet); for (auto id : debug_shapes_) { ShapeDataPayload shape_data; shape_data.network_id = id; shape_data.time_left_total_sec = 0; - - auto packet = MinecraftPackets::createPacket(MinecraftPacketIds::ServerScriptDebugDrawerPacket); - auto &pk = static_cast(*packet); - pk.payload.shapes.push_back(std::move(shape_data)); - getHandle().sendNetworkPacket(*packet); + pk.payload.shapes.emplace_back(std::move(shape_data)); } + getHandle().sendNetworkPacket(*packet); debug_shapes_.clear(); } From c208acb10b8b1bf9eb0a8ceb0eeb9af7dfbe7308 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 19 Mar 2026 17:15:44 +0000 Subject: [PATCH 9/9] feat: add rotation property to debug shapes and update related computations Add support for Euler angle-based rotation to debug shapes (box, sphere, circle, line, arrow, text). Update line and arrow end location computations to use rotation instead of direction vector. Extend Python bindings to expose the new rotation property. --- endstone/debug/__init__.pyi | 48 ++++++++++++++++++++++++++++ include/endstone/debug/shape/shape.h | 16 ++++++++++ src/endstone/core/player.cpp | 30 +++++++++++------ src/endstone/python/debug.cpp | 16 ++++++++-- 4 files changed, 99 insertions(+), 11 deletions(-) diff --git a/endstone/debug/__init__.pyi b/endstone/debug/__init__.pyi index 9df915c392..6482c9306e 100644 --- a/endstone/debug/__init__.pyi +++ b/endstone/debug/__init__.pyi @@ -28,6 +28,14 @@ class DebugBox: @scale.setter def scale(self, arg1: float) -> DebugBox: ... @property + def rotation(self) -> Vector: + """ + The rotation of this shape as Euler angles (pitch, yaw, roll). + """ + ... + @rotation.setter + def rotation(self, arg1: Vector) -> DebugBox: ... + @property def bound(self) -> Vector: """ The bounding size of the box. Final size = bound * scale. @@ -57,6 +65,14 @@ class DebugSphere: ... @scale.setter def scale(self, arg1: float) -> DebugSphere: ... + @property + def rotation(self) -> Vector: + """ + The rotation of this shape as Euler angles (pitch, yaw, roll). + """ + ... + @rotation.setter + def rotation(self, arg1: Vector) -> DebugSphere: ... class DebugCircle: """ @@ -79,6 +95,14 @@ class DebugCircle: ... @scale.setter def scale(self, arg1: float) -> DebugCircle: ... + @property + def rotation(self) -> Vector: + """ + The rotation of this shape as Euler angles (pitch, yaw, roll). + """ + ... + @rotation.setter + def rotation(self, arg1: Vector) -> DebugCircle: ... class DebugLine: """ @@ -102,6 +126,14 @@ class DebugLine: @scale.setter def scale(self, arg1: float) -> DebugLine: ... @property + def rotation(self) -> Vector: + """ + The rotation of this shape as Euler angles (pitch, yaw, roll). + """ + ... + @rotation.setter + def rotation(self, arg1: Vector) -> DebugLine: ... + @property def length(self) -> float: """ The length of the line segment. @@ -132,6 +164,14 @@ class DebugArrow: @scale.setter def scale(self, arg1: float) -> DebugArrow: ... @property + def rotation(self) -> Vector: + """ + The rotation of this shape as Euler angles (pitch, yaw, roll). + """ + ... + @rotation.setter + def rotation(self, arg1: Vector) -> DebugArrow: ... + @property def length(self) -> float: """ The length of the arrow. @@ -186,6 +226,14 @@ class DebugText: @scale.setter def scale(self, arg1: float) -> DebugText: ... @property + def rotation(self) -> Vector: + """ + The rotation of this shape as Euler angles (pitch, yaw, roll). + """ + ... + @rotation.setter + def rotation(self, arg1: Vector) -> DebugText: ... + @property def text(self) -> str: """ The text displayed by this label. diff --git a/include/endstone/debug/shape/shape.h b/include/endstone/debug/shape/shape.h index fa86f3658c..19771ce013 100644 --- a/include/endstone/debug/shape/shape.h +++ b/include/endstone/debug/shape/shape.h @@ -15,6 +15,7 @@ #pragma once #include "endstone/util/color.h" +#include "endstone/util/vector.h" namespace endstone { @@ -52,9 +53,24 @@ class DebugShape { return *static_cast(this); } + /** + * @brief Gets the rotation of this shape as Euler angles (pitch, yaw, roll). + */ + [[nodiscard]] Vector getRotation() const { return rotation_; } + + /** + * @brief Sets the rotation of this shape as Euler angles (pitch, yaw, roll). + */ + T &setRotation(Vector rotation) + { + rotation_ = rotation; + return *static_cast(this); + } + protected: Color color_{255, 255, 255}; float scale_{1.0F}; + Vector rotation_{}; }; diff --git a/src/endstone/core/player.cpp b/src/endstone/core/player.cpp index 713609c565..847b4962cf 100644 --- a/src/endstone/core/player.cpp +++ b/src/endstone/core/player.cpp @@ -641,7 +641,6 @@ std::uint64_t EndstonePlayer::addDebugShape(Location location, DebugShapeVariant ShapeDataPayload data; data.network_id = id; data.location = Vec3{location.getX(), location.getY(), location.getZ()}; - data.rotation = Vec3{location.getPitch(), location.getYaw(), 0}; data.dimension_id = dimension.getHandle().getDimensionId(); std::visit( @@ -652,6 +651,8 @@ std::uint64_t EndstonePlayer::addDebugShape(Location location, DebugShapeVariant mce::Color(static_cast(c.getRed()) / 255.0F, static_cast(c.getGreen()) / 255.0F, static_cast(c.getBlue()) / 255.0F, static_cast(c.getAlpha()) / 255.0F); data.scale = s.getScale(); + auto r = s.getRotation(); + data.rotation = Vec3{r.getX(), r.getY(), r.getZ()}; if constexpr (std::is_same_v) { data.shape_type = ShapeType::Box; @@ -666,18 +667,29 @@ std::uint64_t EndstonePlayer::addDebugShape(Location location, DebugShapeVariant } else if constexpr (std::is_same_v) { data.shape_type = ShapeType::Line; - auto dir = location.getDirection(); - auto end = Vec3{location.getX() + dir.getX() * s.getLength(), - location.getY() + dir.getY() * s.getLength(), - location.getZ() + dir.getZ() * s.getLength()}; + // Compute direction from rotation (pitch=x, yaw=y), same as Location::getDirection + auto rot_x = r.getY() * std::numbers::pi_v / 180.0F; + auto rot_y = r.getX() * std::numbers::pi_v / 180.0F; + auto xz = std::cos(rot_y); + auto dx = -xz * std::sin(rot_x); + auto dy = -std::sin(rot_y); + auto dz = xz * std::cos(rot_x); + auto end = Vec3{location.getX() + dx * s.getLength(), + location.getY() + dy * s.getLength(), + location.getZ() + dz * s.getLength()}; data.extra_data_payload = LineDataPayload{end}; } else if constexpr (std::is_same_v) { data.shape_type = ShapeType::Arrow; - auto dir = location.getDirection(); - auto end = Vec3{location.getX() + dir.getX() * s.getLength(), - location.getY() + dir.getY() * s.getLength(), - location.getZ() + dir.getZ() * s.getLength()}; + auto rot_x = r.getY() * std::numbers::pi_v / 180.0F; + auto rot_y = r.getX() * std::numbers::pi_v / 180.0F; + auto xz = std::cos(rot_y); + auto dx = -xz * std::sin(rot_x); + auto dy = -std::sin(rot_y); + auto dz = xz * std::cos(rot_x); + auto end = Vec3{location.getX() + dx * s.getLength(), + location.getY() + dy * s.getLength(), + location.getZ() + dz * s.getLength()}; ArrowDataPayload arrow; arrow.end_location = end; arrow.arrow_head_length = s.getHeadLength(); diff --git a/src/endstone/python/debug.cpp b/src/endstone/python/debug.cpp index ffb12fa7ab..32f25789bf 100644 --- a/src/endstone/python/debug.cpp +++ b/src/endstone/python/debug.cpp @@ -24,29 +24,39 @@ void init_debug(py::module_ &m) .def(py::init<>()) .def_property("color", &DebugBox::getColor, &DebugBox::setColor, "The color of this shape.") .def_property("scale", &DebugBox::getScale, &DebugBox::setScale, "The uniform scale factor.") + .def_property("rotation", &DebugBox::getRotation, &DebugBox::setRotation, + "The rotation of this shape as Euler angles (pitch, yaw, roll).") .def_property("bound", &DebugBox::getBound, &DebugBox::setBound, "The bounding size of the box. Final size = bound * scale."); py::class_(m, "DebugSphere", "Represents a debug sphere shape. Radius is controlled via scale.") .def(py::init<>()) .def_property("color", &DebugSphere::getColor, &DebugSphere::setColor, "The color of this shape.") - .def_property("scale", &DebugSphere::getScale, &DebugSphere::setScale, "The uniform scale factor."); + .def_property("scale", &DebugSphere::getScale, &DebugSphere::setScale, "The uniform scale factor.") + .def_property("rotation", &DebugSphere::getRotation, &DebugSphere::setRotation, + "The rotation of this shape as Euler angles (pitch, yaw, roll)."); py::class_(m, "DebugCircle", "Represents a debug 2D circle shape. Radius is controlled via scale.") .def(py::init<>()) .def_property("color", &DebugCircle::getColor, &DebugCircle::setColor, "The color of this shape.") - .def_property("scale", &DebugCircle::getScale, &DebugCircle::setScale, "The uniform scale factor."); + .def_property("scale", &DebugCircle::getScale, &DebugCircle::setScale, "The uniform scale factor.") + .def_property("rotation", &DebugCircle::getRotation, &DebugCircle::setRotation, + "The rotation of this shape as Euler angles (pitch, yaw, roll)."); py::class_(m, "DebugLine", "Represents a debug line segment shape.") .def(py::init<>()) .def_property("color", &DebugLine::getColor, &DebugLine::setColor, "The color of this shape.") .def_property("scale", &DebugLine::getScale, &DebugLine::setScale, "The uniform scale factor.") + .def_property("rotation", &DebugLine::getRotation, &DebugLine::setRotation, + "The rotation of this shape as Euler angles (pitch, yaw, roll).") .def_property("length", &DebugLine::getLength, &DebugLine::setLength, "The length of the line segment."); py::class_(m, "DebugArrow", "Represents a debug arrow shape.") .def(py::init<>()) .def_property("color", &DebugArrow::getColor, &DebugArrow::setColor, "The color of this shape.") .def_property("scale", &DebugArrow::getScale, &DebugArrow::setScale, "The uniform scale factor.") + .def_property("rotation", &DebugArrow::getRotation, &DebugArrow::setRotation, + "The rotation of this shape as Euler angles (pitch, yaw, roll).") .def_property("length", &DebugArrow::getLength, &DebugArrow::setLength, "The length of the arrow.") .def_property("head_length", &DebugArrow::getHeadLength, &DebugArrow::setHeadLength, "The length of the arrow's head.") @@ -59,6 +69,8 @@ void init_debug(py::module_ &m) .def(py::init<>()) .def_property("color", &DebugText::getColor, &DebugText::setColor, "The color of this shape.") .def_property("scale", &DebugText::getScale, &DebugText::setScale, "The uniform scale factor.") + .def_property("rotation", &DebugText::getRotation, &DebugText::setRotation, + "The rotation of this shape as Euler angles (pitch, yaw, roll).") .def_property("text", &DebugText::getText, &DebugText::setText, "The text displayed by this label."); }