Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions endstone/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ __all__ = [
"__version__",
"ColorFormat",
"GameMode",
"Identifier",
"Logger",
"OfflinePlayer",
"Player",
Expand Down Expand Up @@ -780,21 +781,44 @@ class Skin:

__minecraft_version__ = "26.12"

class Identifier(typing.Generic[_T]):
"""
Represents a namespaced identifier consisting of a namespace and a key.
"""
def __init__(self, id: str) -> None: ...
@property
def namespace(self) -> str:
"""
The namespace component of this identifier.
"""
...
@property
def key(self) -> str:
"""
The key component of this identifier.
"""
...
def __str__(self) -> str: ...
def __repr__(self) -> str: ...
def __hash__(self) -> int: ...
def __eq__(self, other: object) -> bool: ...
def __ne__(self, other: object) -> bool: ...

class Registry(typing.Generic[_T]):
"""
Presents a registry
"""
def get(self, key: str) -> _T | None:
def get(self, id: Identifier[_T] | str) -> _T | None:
"""
Get the object by its key.
"""
...
def get_or_throw(self, key: str) -> _T:
def get_or_throw(self, id: Identifier[_T] | str) -> _T:
"""
Get the object by its key or throw if missing.
"""
...
def __getitem__(self, key: str) -> _T: ...
def __getitem__(self, id: Identifier[_T] | str) -> _T: ...
def __iter__(self) -> list: ...
def __contains__(self, key: str) -> bool: ...
def __contains__(self, id: Identifier[_T] | str) -> bool: ...
def __len__(self) -> int: ...
8 changes: 3 additions & 5 deletions scripts/stubgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,14 @@ def main() -> None:
module = load(module_name)
package = module.package

# endstone
module.members = {"_T": package.get_member("_T"), **module.members}
for member in list(module.members.keys()):
if member.endswith("Registry"):
module.members.pop(member)
module.exports.remove(member)
module.set_member("Registry", package.get_member("Registry"))
module.exports.append("Registry")
module.set_member("Identifier", package.get_member("Identifier"))
module.exports.append("Identifier")
module.exports = sorted(module.exports)
module["Server.get_registry"] = package["Server.get_registry"]

module.set_member("__version__", package.get_member("__version__"))
module.imports.setdefault("__version__", package.imports.get("__version__"))
module.exports = ["__version__"] + module.exports
Expand Down
2 changes: 1 addition & 1 deletion src/endstone/python/block.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ void init_block(py::module_ &m, py::class_<Block> &block)
"Creates a new BlockData instance for this block type, with all properties initialized to defaults.")
.def_static("get", &BlockType::get, py::arg("name"), "Attempts to get the BlockType with the given name.",
py::return_value_policy::reference)
.def("__str__", &BlockType::getId)
.def("__str__", [](const BlockType &self) { return std::string(self.getId()); })
.def("__repr__", [](const BlockType &self) { return fmt::format("BlockType({})", self.getId()); });

py::class_<BlockState>(m, "BlockState",
Expand Down
2 changes: 1 addition & 1 deletion src/endstone/python/enchantments.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ void init_enchantments(py::module_ &m)
"with any enchantments already applied to the item.")
.def_static("get", &Enchantment::get, py::arg("name"), "Attempts to get the Enchantment with the given name.",
py::return_value_policy::reference)
.def("__str__", &Enchantment::getId)
.def("__str__", [](const Enchantment &self) { return std::string(self.getId()); })
.def("__hash__", [](const Enchantment &self) { return std::hash<EnchantmentId>{}(self.getId()); })
.def(py::self == py::self)
.def(py::self != py::self)
Expand Down
40 changes: 40 additions & 0 deletions src/endstone/python/endstone_python.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,46 @@ PYBIND11_MODULE(_python, m) // NOLINT(*-use-anonymous-namespace)
py::options options;
options.disable_enum_members_docstring();

// Identifier (registered early, before classes that use it via type caster)
py::class_<PyIdentifier>(m, "Identifier",
"Represents a namespaced identifier consisting of a namespace and a key.")
.def(py::init<std::string>(), py::arg("id"), "Create an Identifier from a string like 'namespace:key'.")
.def(py::init<std::string, std::string>(), py::arg("namespace_"), py::arg("key"),
"Create an Identifier from separate namespace and key.")
.def_property_readonly(
"namespace", [](const PyIdentifier &self) { return self.namespace_; },
"The namespace component of this identifier.")
.def_property_readonly(
"key", [](const PyIdentifier &self) { return self.key_; }, "The key component of this identifier.")
.def("__str__", &PyIdentifier::str)
.def("__repr__",
[](const PyIdentifier &self) { return "Identifier(" + self.str() + ")"; })
.def("__hash__",
[](const PyIdentifier &self) { return py::hash(py::str(self.str())); })
.def("__eq__",
[](const PyIdentifier &self, const py::object &other) {
if (py::isinstance<PyIdentifier>(other)) {
return self == other.cast<PyIdentifier>();
}
if (py::isinstance<py::str>(other)) {
return self.str() == other.cast<std::string>();
}
return false;
})
.def("__ne__",
[](const PyIdentifier &self, const py::object &other) {
if (py::isinstance<PyIdentifier>(other)) {
return !(self == other.cast<PyIdentifier>());
}
if (py::isinstance<py::str>(other)) {
return self.str() != other.cast<std::string>();
}
return true;
})
.def_static(
"__class_getitem__", [](const py::object &) { return py::type::of<PyIdentifier>(); }, py::arg("item"));
py::implicitly_convertible<std::string, PyIdentifier>();

// Submodules
auto m_actor =
m.def_submodule("actor", "Classes relating to actors (entities) that can exist in a world, including all "
Expand Down
2 changes: 1 addition & 1 deletion src/endstone/python/inventory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ void init_inventory(py::module_ &m, py::class_<ItemStack> &item_stack)
"Constructs a new ItemStack with this item type.")
.def_static("get", &ItemType::get, py::arg("name"), "Attempts to get the ItemType with the given name.",
py::return_value_policy::reference)
.def("__str__", &ItemType::getId)
.def("__str__", [](const ItemType &self) { return std::string(self.getId()); })
.def(py::self == py::self)
.def(py::self != py::self)
.def(py::self == std::string_view())
Expand Down
52 changes: 46 additions & 6 deletions src/endstone/python/registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,64 @@ namespace py = pybind11;

namespace endstone::python {

struct PyIdentifier {
std::string namespace_;
std::string key_;

PyIdentifier() = default;

PyIdentifier(std::string ns, std::string key) : namespace_(std::move(ns)), key_(std::move(key))
{
if (namespace_.empty() || key_.empty()) {
throw std::invalid_argument("Identifier namespace and key must not be empty.");
}
}

explicit PyIdentifier(const std::string &full)
{
if (full.empty()) {
throw std::invalid_argument("Identifier string must not be empty.");
}
const auto pos = full.rfind(':');
if (pos == std::string::npos) {
namespace_ = "minecraft";
key_ = full;
}
else {
namespace_ = full.substr(0, pos);
key_ = full.substr(pos + 1);
}
if (namespace_.empty() || key_.empty()) {
throw std::invalid_argument("Identifier namespace and key must not be empty.");
}
}

[[nodiscard]] std::string str() const { return namespace_ + ":" + key_; }

bool operator==(const PyIdentifier &other) const
{
return namespace_ == other.namespace_ && key_ == other.key_;
}
};

class PyRegistry {
public:
explicit PyRegistry(const IRegistry &registry) : registry_(registry) {}

[[nodiscard]] py::object get(const std::string &id) const
[[nodiscard]] py::object get(const PyIdentifier &id) const
{
if (const auto *p = registry_.get0(id)) {
if (const auto *p = registry_.get0(id.str())) {
return cast(p);
}
return py::none();
}

[[nodiscard]] py::object getOrThrow(const std::string &id) const
[[nodiscard]] py::object getOrThrow(const PyIdentifier &id) const
{
if (const auto *p = registry_.get0(id)) {
if (const auto *p = registry_.get0(id.str())) {
return cast(p);
}
throw py::key_error(fmt::format("No registry entry found for identifier: {}", id));
throw py::key_error(fmt::format("No registry entry found for identifier: {}", id.str()));
}

[[nodiscard]] py::iterator iter() const
Expand All @@ -54,7 +94,7 @@ class PyRegistry {
return py::iter(items);
}

[[nodiscard]] bool contains(const std::string &id) const { return registry_.get0(id) != nullptr; }
[[nodiscard]] bool contains(const PyIdentifier &id) const { return registry_.get0(id.str()) != nullptr; }

[[nodiscard]] std::size_t size() const { return registry_.size(); }

Expand Down
45 changes: 31 additions & 14 deletions src/endstone/python/type_caster.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <pybind11/pybind11.h>

#include "endstone/endstone.hpp"
#include "registry.h"

namespace pybind11::detail {
template <>
Expand Down Expand Up @@ -266,32 +267,48 @@ class type_caster<endstone::Color> {
template <typename T>
class type_caster<endstone::Identifier<T>> {
public:
using value_conv = make_caster<T>;
explicit type_caster() : value("") {}
// Python -> C++
bool load(handle src, bool convert)
{
make_caster<std::string_view> str_caster;
if (!str_caster.load(src, convert)) {
return false;
}
try {
value = static_cast<std::string_view &>(str_caster);
// Accept PyIdentifier objects
if (isinstance<endstone::python::PyIdentifier>(src)) {
auto &py_id = src.cast<endstone::python::PyIdentifier &>();
storage_ = py_id.str();
value = endstone::Identifier<T>(std::string_view(storage_));
return true;
}
catch (const std::exception &e) {
PyErr_SetString(PyExc_ValueError, e.what());
return false;
// Accept strings
make_caster<std::string> str_caster;
if (str_caster.load(src, convert)) {
try {
storage_ = cast_op<std::string>(std::move(str_caster));
value = endstone::Identifier<T>(std::string_view(storage_));
return true;
}
catch (const std::exception &e) {
PyErr_SetString(PyExc_ValueError, e.what());
return false;
}
}
return false;
}

// C++ -> Python
static handle cast(endstone::Identifier<T> src, return_value_policy policy, handle parent)
// C++ -> Python: return PyIdentifier object
static handle cast(endstone::Identifier<T> src, return_value_policy /*policy*/, handle /*parent*/)
{
make_caster<std::string> str_caster;
return str_caster.cast(src, policy, parent);
endstone::python::PyIdentifier id(std::string(src.getNamespace()), std::string(src.getKey()));
return pybind11::cast(std::move(id)).release();
}

PYBIND11_TYPE_CASTER(endstone::Identifier<T>, const_name(PYBIND11_STRING_NAME));
// PYBIND11_TYPE_CASTER(endstone::Identifier<T>, const_name("@Identifier[") + value_conv::name +
// const_name("] | str@Identifier[") + value_conv::name +
// const_name("]@"));
PYBIND11_TYPE_CASTER(endstone::Identifier<T>, const_name("endstone.Identifier[") + value_conv::name + const_name("]"));

private:
std::string storage_;
};

template <>
Expand Down
3 changes: 2 additions & 1 deletion tests/endstone_test/src/endstone_test/tests/test_registry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
from endstone import Server
from endstone._python import Identifier
from endstone.actor import ActorType
from endstone.enchantments import Enchantment
from endstone.inventory import ItemType
Expand All @@ -13,7 +14,7 @@ def _get_enum_constants(cls):
return {
name: getattr(cls, name)
for name in dir(cls)
if not name.startswith("_") and isinstance(getattr(cls, name), str)
if not name.startswith("_") and isinstance(getattr(cls, name), Identifier)
}


Expand Down
Loading