endstone-papi 0.2.0__cp315-cp315-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,59 @@
1
+ """Endstone PlaceholderAPI.
2
+
3
+ A native C++20 PlaceholderAPI framework for Endstone and Minecraft Bedrock
4
+ Dedicated Server. Provides a bracket placeholder parser, an owner-aware
5
+ expansion registry, and a service that resolves ``{identifier:params}``
6
+ placeholders through expansions supplied by C++ or Python plugins.
7
+ """
8
+
9
+
10
+ # start delvewheel patch
11
+ def _delvewheel_patch_1_13_1():
12
+ import os
13
+ if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'endstone_papi.libs'))):
14
+ os.add_dll_directory(libs_dir)
15
+
16
+
17
+ _delvewheel_patch_1_13_1()
18
+ del _delvewheel_patch_1_13_1
19
+ # end delvewheel patch
20
+
21
+ # Load PAPI's native extension before Endstone's Python bindings so provider
22
+ # exceptions are translated by the module that owns them.
23
+ try:
24
+ from ._native_loader import load_native as _load_native
25
+ from ._native_loader import should_use_shadow as _should_use_shadow
26
+
27
+ if _should_use_shadow(__file__):
28
+ _load_native(__name__, __file__)
29
+
30
+ from ._papi import (
31
+ SERVICE_NAME,
32
+ ExpansionInfo,
33
+ ExpansionRegisteredEvent,
34
+ ExpansionUnregisteredEvent,
35
+ PlaceholderAPI,
36
+ PlaceholderExpansion,
37
+ UnregisterReason,
38
+ __version__,
39
+ )
40
+ except ImportError as _e:
41
+ raise ImportError(
42
+ f"Failed to load the native PAPI extension: {_e}. "
43
+ "PAPI requires Endstone >=0.11.8,<0.12 (API 0.11). "
44
+ "After updating PAPI or Endstone, restart the server before trying again."
45
+ ) from _e
46
+
47
+ from .plugin import PlaceholderAPIPlugin
48
+
49
+ __all__ = [
50
+ "SERVICE_NAME",
51
+ "ExpansionInfo",
52
+ "ExpansionRegisteredEvent",
53
+ "ExpansionUnregisteredEvent",
54
+ "PlaceholderAPI",
55
+ "PlaceholderAPIPlugin",
56
+ "PlaceholderExpansion",
57
+ "UnregisterReason",
58
+ "__version__",
59
+ ]
@@ -0,0 +1,185 @@
1
+ """Windows content-addressed loading for the native PAPI extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import importlib.machinery
7
+ import importlib.util
8
+ import os
9
+ import re
10
+ import sys
11
+ import types
12
+ from pathlib import Path
13
+
14
+ PERSISTENT_MODULE_NAME = "_endstone_papi_native._papi"
15
+ PERSISTENT_DIGEST_ATTRIBUTE = "_papi_payload_sha256"
16
+ _PERSISTENT_PARENT_NAME = "_endstone_papi_native"
17
+ _PACKAGE_SUFFIX = ("plugins", ".local", "Lib", "site-packages", "endstone_papi")
18
+ _RESTART_REQUIRED = "native binary changed while server runs; restart server to apply update"
19
+ _SHADOW_TARGET_RE = re.compile(r"endstone_papi-[0-9a-f]{64}\.pyd\Z", re.IGNORECASE)
20
+
21
+
22
+ def _package_dir(package_path: str | os.PathLike[str]) -> Path:
23
+ path = Path(package_path)
24
+ if path.name == "__init__.py" or (path.suffix and not path.is_dir()):
25
+ path = path.parent
26
+ return path.resolve()
27
+
28
+
29
+ def shadow_root(package_path: str | os.PathLike[str]) -> Path | None:
30
+ """Return the Endstone local-plugin root for an eligible package path."""
31
+ package_dir = _package_dir(package_path)
32
+ expected = tuple(part.casefold() for part in _PACKAGE_SUFFIX)
33
+ actual = tuple(part.casefold() for part in package_dir.parts[-len(expected) :])
34
+ if actual != expected:
35
+ return None
36
+ return package_dir.parents[2]
37
+
38
+
39
+ def should_use_shadow(package_path: str | os.PathLike[str]) -> bool:
40
+ return os.name == "nt" and shadow_root(package_path) is not None
41
+
42
+
43
+ def _find_native_payload(package_dir: Path) -> Path:
44
+ matches: list[Path] = []
45
+ seen: set[str] = set()
46
+ for suffix in importlib.machinery.EXTENSION_SUFFIXES:
47
+ candidate = package_dir / f"_papi{suffix}"
48
+ key = str(candidate).casefold()
49
+ if key in seen or not candidate.is_file():
50
+ continue
51
+ seen.add(key)
52
+ matches.append(candidate)
53
+
54
+ if not matches:
55
+ suffixes = ", ".join(importlib.machinery.EXTENSION_SUFFIXES)
56
+ raise ImportError(f"could not find packaged native PAPI payload (_papi + one of: {suffixes})")
57
+ if len(matches) != 1:
58
+ names = ", ".join(str(path) for path in matches)
59
+ raise ImportError(f"ambiguous packaged native PAPI payloads: {names}")
60
+ return matches[0]
61
+
62
+
63
+ def _payload_digest(payload: bytes) -> str:
64
+ return hashlib.sha256(payload).hexdigest()
65
+
66
+
67
+ def _target_path(root: Path, digest: str) -> Path:
68
+ return root / f"endstone_papi-{digest}.pyd"
69
+
70
+
71
+ def _ensure_shadow_copy(target: Path, payload: bytes, digest: str) -> None:
72
+ target.parent.mkdir(parents=True, exist_ok=True)
73
+ try:
74
+ stream = target.open("xb")
75
+ except FileExistsError:
76
+ try:
77
+ existing = target.read_bytes()
78
+ except OSError as error:
79
+ raise ImportError(f"could not verify existing native shadow target {target}") from error
80
+ if _payload_digest(existing) != digest:
81
+ raise ImportError(f"existing native shadow target does not match payload digest: {target}")
82
+ return
83
+
84
+ try:
85
+ with stream:
86
+ stream.write(payload)
87
+ stream.flush()
88
+ except Exception as error:
89
+ try:
90
+ target.unlink()
91
+ except OSError:
92
+ pass
93
+ raise ImportError(f"failed to create native shadow target {target}") from error
94
+
95
+
96
+ def _make_persistent_parent() -> tuple[types.ModuleType, bool]:
97
+ existing = sys.modules.get(_PERSISTENT_PARENT_NAME)
98
+ if existing is not None:
99
+ if not isinstance(existing, types.ModuleType):
100
+ raise ImportError(f"persistent native namespace is not a module: {_PERSISTENT_PARENT_NAME}")
101
+ return existing, False
102
+
103
+ parent = types.ModuleType(_PERSISTENT_PARENT_NAME)
104
+ parent.__path__ = []
105
+ parent.__package__ = _PERSISTENT_PARENT_NAME
106
+ sys.modules[_PERSISTENT_PARENT_NAME] = parent
107
+ return parent, True
108
+
109
+
110
+ def _execute_extension(target: Path) -> types.ModuleType:
111
+ loader = importlib.machinery.ExtensionFileLoader(PERSISTENT_MODULE_NAME, str(target))
112
+ spec = importlib.util.spec_from_file_location(PERSISTENT_MODULE_NAME, str(target), loader=loader)
113
+ if spec is None:
114
+ raise ImportError(f"could not create import spec for native PAPI payload: {target}")
115
+
116
+ parent, parent_created = _make_persistent_parent()
117
+ module: types.ModuleType | None = None
118
+ try:
119
+ module = importlib.util.module_from_spec(spec)
120
+ sys.modules[PERSISTENT_MODULE_NAME] = module
121
+ loader.exec_module(module)
122
+ return module
123
+ except BaseException:
124
+ if module is not None and sys.modules.get(PERSISTENT_MODULE_NAME) is module:
125
+ del sys.modules[PERSISTENT_MODULE_NAME]
126
+ if parent_created and sys.modules.get(_PERSISTENT_PARENT_NAME) is parent:
127
+ del sys.modules[_PERSISTENT_PARENT_NAME]
128
+ raise
129
+
130
+
131
+ def _cleanup_stale_targets(root: Path, current: Path) -> None:
132
+ current_key = str(current.absolute()).casefold()
133
+ try:
134
+ candidates = root.glob("endstone_papi-*.pyd")
135
+ for candidate in candidates:
136
+ if _SHADOW_TARGET_RE.fullmatch(candidate.name) is None:
137
+ continue
138
+ if str(candidate.absolute()).casefold() == current_key:
139
+ continue
140
+ try:
141
+ candidate.unlink()
142
+ except OSError:
143
+ pass
144
+ except OSError:
145
+ pass
146
+
147
+
148
+ def _mismatch_error() -> ImportError:
149
+ return ImportError(_RESTART_REQUIRED)
150
+
151
+
152
+ def load_native(package_name: str, package_path: str | os.PathLike[str]) -> types.ModuleType:
153
+ """Load and alias the native extension for an eligible Windows package."""
154
+ package_dir = _package_dir(package_path)
155
+ root = shadow_root(package_dir)
156
+ if os.name != "nt" or root is None:
157
+ raise ImportError("native shadow loading is only available for Endstone local plugins on Windows")
158
+
159
+ source = _find_native_payload(package_dir)
160
+ payload = source.read_bytes()
161
+ digest = _payload_digest(payload)
162
+
163
+ persistent = sys.modules.get(PERSISTENT_MODULE_NAME)
164
+ if persistent is not None:
165
+ if getattr(persistent, PERSISTENT_DIGEST_ATTRIBUTE, None) != digest:
166
+ raise _mismatch_error()
167
+ module = persistent
168
+ else:
169
+ target = _target_path(root, digest)
170
+ _ensure_shadow_copy(target, payload, digest)
171
+ module = _execute_extension(target)
172
+ _make_persistent_parent()
173
+ sys.modules[PERSISTENT_MODULE_NAME] = module
174
+ setattr(module, PERSISTENT_DIGEST_ATTRIBUTE, digest)
175
+ _cleanup_stale_targets(root, target)
176
+
177
+ alias_name = f"{package_name}._papi"
178
+ sys.modules[alias_name] = module
179
+ package = sys.modules.get(package_name)
180
+ if package is not None:
181
+ package._papi = module
182
+ parent = sys.modules.get(_PERSISTENT_PARENT_NAME)
183
+ if parent is not None:
184
+ parent._papi = module
185
+ return module
Binary file
@@ -0,0 +1,215 @@
1
+ """Type stubs for the native PlaceholderAPI core."""
2
+
3
+ import enum
4
+
5
+ from endstone import OfflinePlayer, Player
6
+ from endstone.event import Event
7
+ from endstone.plugin import Plugin, Service, ServiceManager
8
+
9
+ __all__ = [
10
+ "SERVICE_NAME",
11
+ "ExpansionInfo",
12
+ "ExpansionRegisteredEvent",
13
+ "ExpansionUnregisteredEvent",
14
+ "PlaceholderAPI",
15
+ "PlaceholderExpansion",
16
+ "UnregisterReason",
17
+ "__version__",
18
+ ]
19
+
20
+ __version__: str
21
+
22
+ SERVICE_NAME: str
23
+
24
+ class UnregisterReason(enum.Enum):
25
+ """Why an expansion was removed from the registry."""
26
+
27
+ EXPLICIT = 0
28
+ OWNER_DISABLED = 1
29
+ REQUIRED_PLUGIN_DISABLED = 2
30
+ PAPI_SHUTDOWN = 3
31
+
32
+ class ExpansionInfo:
33
+ """An immutable copy of a registered expansion's metadata.
34
+
35
+ Stays valid and unchanged after the expansion it describes is unregistered and
36
+ destroyed. It holds no reference to the expansion, its owner, or the registry.
37
+ """
38
+
39
+ @property
40
+ def identifier(self) -> str:
41
+ """The canonical, lowercase identifier."""
42
+
43
+ @property
44
+ def name(self) -> str:
45
+ """The expansion's display name."""
46
+
47
+ @property
48
+ def author(self) -> str:
49
+ """The expansion's author."""
50
+
51
+ @property
52
+ def version(self) -> str:
53
+ """The expansion's version."""
54
+
55
+ @property
56
+ def owner(self) -> str:
57
+ """The name of the plugin that registered the expansion."""
58
+
59
+ @property
60
+ def required_plugin(self) -> str | None:
61
+ """The plugin the expansion requires, or None."""
62
+
63
+ @property
64
+ def relational(self) -> bool:
65
+ """Whether the expansion handles relational placeholders."""
66
+
67
+ class PlaceholderExpansion:
68
+ """Base class for placeholder providers.
69
+
70
+ Subclass this and register it through the PlaceholderAPI service. C++ and Python
71
+ expansions share one registry, so a consumer never learns which language a
72
+ provider was written in.
73
+
74
+ Metadata is read once at registration and copied, so an implementation must report
75
+ stable values. Arguments are borrowed for the duration of a call only; do not keep
76
+ the player past the callback.
77
+ """
78
+
79
+ def __init__(self) -> None: ...
80
+ @property
81
+ def identifier(self) -> str:
82
+ """The identifier this expansion answers to.
83
+
84
+ Must match ``[A-Za-z0-9][A-Za-z0-9-]*``, and is canonicalized to lowercase.
85
+ The colon separates the identifier from parameters. Dots and underscores may
86
+ be used in parameters, but are not allowed in an identifier.
87
+ """
88
+
89
+ @property
90
+ def author(self) -> str:
91
+ """The expansion's author. Must not be empty."""
92
+
93
+ @property
94
+ def version(self) -> str:
95
+ """The expansion's version. Must not be empty."""
96
+
97
+ @property
98
+ def name(self) -> str:
99
+ """The expansion's display name. Defaults to the identifier."""
100
+
101
+ @property
102
+ def required_plugin(self) -> str | None:
103
+ """The plugin this expansion requires, or None.
104
+
105
+ Matched case-sensitively. It must be enabled at registration, and the expansion
106
+ is removed automatically if it is later disabled.
107
+ """
108
+
109
+ def can_register(self) -> bool:
110
+ """Final chance to refuse registration. Defaults to True."""
111
+
112
+ def supports_relational_placeholders(self) -> bool:
113
+ """Whether ``on_relational_request`` is implemented. Defaults to False."""
114
+
115
+ def supports_player_cleanup(self) -> bool:
116
+ """Whether ``on_player_quit`` is implemented. Defaults to False."""
117
+
118
+ def on_request(self, player: OfflinePlayer | None, params: str) -> str | None:
119
+ """Resolves an ordinary ``{identifier:params}`` placeholder.
120
+
121
+ ``params`` contains everything after the first colon exactly as written.
122
+ Return a ``str``, or None to leave the placeholder text untouched. Any other
123
+ type is a provider error and is not coerced.
124
+ """
125
+
126
+ def on_relational_request(self, one: Player, two: Player, params: str) -> str | None:
127
+ """Resolves a relational ``{rel:identifier:params}`` placeholder.
128
+
129
+ ``params`` contains everything after the second colon exactly as written.
130
+ """
131
+
132
+ def on_player_quit(self, player: Player) -> None:
133
+ """Called when a player leaves, if this expansion opted into cleanup."""
134
+
135
+ def on_unregister(self, reason: UnregisterReason) -> None:
136
+ """Called once after this expansion has been removed from the registry."""
137
+
138
+ class PlaceholderAPI(Service):
139
+ """Resolves placeholders through registered expansions.
140
+
141
+ Obtained from Endstone's service manager; it cannot be constructed or subclassed in
142
+ Python. A retained instance becomes permanently inert once PAPI is disabled:
143
+ ``active`` turns False, parsing returns its input unchanged, queries come back
144
+ empty, and mutations fail.
145
+
146
+ Parsing and register/unregister mutations require the primary server thread, and
147
+ provider callbacks execute there. ``contains_placeholders``, ``active``, and registry
148
+ queries that return copied state are safe from any thread.
149
+ """
150
+
151
+ @staticmethod
152
+ def load(service_manager: ServiceManager) -> PlaceholderAPI | None:
153
+ """Load the active PlaceholderAPI service, or None when it is unavailable."""
154
+
155
+ @property
156
+ def active(self) -> bool:
157
+ """Whether this service is still usable."""
158
+
159
+ def set_placeholders(self, player: OfflinePlayer | None, text: str) -> str:
160
+ """Replaces every resolvable ``{identifier:params}`` in text."""
161
+
162
+ def set_relational_placeholders(self, one: Player, two: Player, text: str) -> str:
163
+ """Replaces every resolvable ``{rel:identifier:params}`` in text."""
164
+
165
+ def contains_placeholders(self, text: str) -> bool:
166
+ """Whether text lexically contains a placeholder-shaped substring."""
167
+
168
+ def is_registered(self, identifier: str) -> bool:
169
+ """Whether an identifier currently has an expansion registered.
170
+
171
+ ``rel`` is reserved by relational syntax and cannot be registered.
172
+ """
173
+
174
+ @property
175
+ def registered_identifiers(self) -> tuple[str, ...]:
176
+ """Every registered identifier, sorted canonically."""
177
+
178
+ @property
179
+ def expansions(self) -> tuple[ExpansionInfo, ...]:
180
+ """Metadata for every registered expansion, sorted by identifier."""
181
+
182
+ def register_expansion(self, owner: Plugin, expansion: PlaceholderExpansion) -> bool:
183
+ """Registers an expansion on behalf of a plugin."""
184
+
185
+ def unregister_expansion(self, owner: Plugin, identifier: str) -> bool:
186
+ """Unregisters one expansion owned by a plugin."""
187
+
188
+ def unregister_expansions(self, owner: Plugin) -> int:
189
+ """Unregisters every expansion owned by a plugin."""
190
+
191
+ class ExpansionRegisteredEvent(Event):
192
+ """Fired after an expansion has been added to the registry."""
193
+
194
+ @property
195
+ def expansion_info(self) -> ExpansionInfo:
196
+ """Metadata describing the expansion that was registered."""
197
+
198
+ class ExpansionUnregisteredEvent(Event):
199
+ """Fired after an expansion has been removed from the registry."""
200
+
201
+ @property
202
+ def expansion_info(self) -> ExpansionInfo:
203
+ """Metadata describing the expansion that was removed."""
204
+
205
+ @property
206
+ def reason(self) -> UnregisterReason:
207
+ """Why the expansion was removed."""
208
+
209
+ class _PapiBootstrap:
210
+ """Internal native lifecycle state owned by the PAPI plugin."""
211
+
212
+ def start(self, plugin: Plugin) -> bool: ...
213
+ def stop(self) -> None: ...
214
+ @property
215
+ def service(self) -> PlaceholderAPI | None: ...
@@ -0,0 +1,4 @@
1
+ compiler_id=Clang
2
+ compiler_version=20.1.8
3
+ compiler_frontend=MSVC
4
+ cxx_standard=20
@@ -0,0 +1,71 @@
1
+ #pragma once
2
+
3
+ #include <utility>
4
+
5
+ #include <endstone/event/event.h>
6
+ #include <endstone/event/server/server_event.h>
7
+
8
+ #include "endstone_papi/expansion_info.h"
9
+ #include "endstone_papi/unregister_reason.h"
10
+
11
+ namespace papi {
12
+
13
+ /**
14
+ * @brief Fired after an expansion has been added to the registry.
15
+ *
16
+ * Synchronous, non-cancellable, and dispatched after the registry has committed,
17
+ * so a listener querying PlaceholderAPI already observes the new expansion.
18
+ *
19
+ * Only copied metadata is exposed; the expansion object is never reachable from
20
+ * the event.
21
+ */
22
+ class ExpansionRegisteredEvent : public endstone::ServerEvent {
23
+ public:
24
+ ENDSTONE_EVENT(ExpansionRegisteredEvent);
25
+
26
+ explicit ExpansionRegisteredEvent(ExpansionInfo info) : info_(std::move(info)) {}
27
+
28
+ /**
29
+ * @brief Metadata describing the expansion that was registered.
30
+ */
31
+ [[nodiscard]] const ExpansionInfo &getExpansionInfo() const { return info_; }
32
+
33
+ private:
34
+ ExpansionInfo info_;
35
+ };
36
+
37
+ /**
38
+ * @brief Fired after an expansion has been removed from the registry.
39
+ *
40
+ * Synchronous, non-cancellable, and dispatched after the registry has removed the
41
+ * entry and the expansion's cleanup callback has returned, so a listener querying
42
+ * PlaceholderAPI no longer observes the expansion.
43
+ *
44
+ * This event is suppressed during PAPI's own shutdown, because the event system
45
+ * and its listeners are being torn down at that point.
46
+ */
47
+ class ExpansionUnregisteredEvent : public endstone::ServerEvent {
48
+ public:
49
+ ENDSTONE_EVENT(ExpansionUnregisteredEvent);
50
+
51
+ ExpansionUnregisteredEvent(ExpansionInfo info, const UnregisterReason reason)
52
+ : info_(std::move(info)), reason_(reason)
53
+ {
54
+ }
55
+
56
+ /**
57
+ * @brief Metadata describing the expansion that was removed.
58
+ */
59
+ [[nodiscard]] const ExpansionInfo &getExpansionInfo() const { return info_; }
60
+
61
+ /**
62
+ * @brief Why the expansion was removed.
63
+ */
64
+ [[nodiscard]] UnregisterReason getReason() const { return reason_; }
65
+
66
+ private:
67
+ ExpansionInfo info_;
68
+ UnregisterReason reason_;
69
+ };
70
+
71
+ } // namespace papi
@@ -0,0 +1,54 @@
1
+ #pragma once
2
+
3
+ #include <optional>
4
+ #include <string>
5
+
6
+ namespace papi {
7
+
8
+ /**
9
+ * @brief An immutable copy of a registered expansion's metadata.
10
+ *
11
+ * Instances are pure values. They contain no plugin pointer, expansion pointer,
12
+ * callback, or registry reference, so they stay valid and unchanged after the
13
+ * expansion they describe is unregistered and destroyed.
14
+ */
15
+ struct ExpansionInfo {
16
+ /**
17
+ * @brief The canonical identifier, always ASCII lowercase.
18
+ */
19
+ std::string identifier;
20
+
21
+ /**
22
+ * @brief The human-readable name reported by the expansion.
23
+ */
24
+ std::string name;
25
+
26
+ /**
27
+ * @brief The author reported by the expansion.
28
+ */
29
+ std::string author;
30
+
31
+ /**
32
+ * @brief The version reported by the expansion.
33
+ */
34
+ std::string version;
35
+
36
+ /**
37
+ * @brief The name of the Endstone plugin that registered the expansion.
38
+ */
39
+ std::string owner;
40
+
41
+ /**
42
+ * @brief The plugin the expansion requires, if it declared one.
43
+ */
44
+ std::optional<std::string> required_plugin;
45
+
46
+ /**
47
+ * @brief Whether the expansion handles relational placeholders.
48
+ */
49
+ bool relational = false;
50
+
51
+ [[nodiscard]] friend bool operator==(const ExpansionInfo &, const ExpansionInfo &) = default;
52
+ };
53
+
54
+ } // namespace papi
@@ -0,0 +1,8 @@
1
+ #pragma once
2
+
3
+ #include "endstone_papi/events.h"
4
+ #include "endstone_papi/expansion_info.h"
5
+ #include "endstone_papi/placeholder_api.h"
6
+ #include "endstone_papi/placeholder_expansion.h"
7
+ #include "endstone_papi/unregister_reason.h"
8
+ #include "endstone_papi/version.h"