py2hackCraft2 1.1.44__tar.gz → 1.2.1__tar.gz
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.
- {py2hackcraft2-1.1.44 → py2hackcraft2-1.2.1}/PKG-INFO +15 -1
- {py2hackcraft2-1.1.44 → py2hackcraft2-1.2.1}/README.md +14 -0
- {py2hackcraft2-1.1.44 → py2hackcraft2-1.2.1}/py2hackCraft/modules.py +410 -8
- {py2hackcraft2-1.1.44 → py2hackcraft2-1.2.1}/py2hackCraft2.egg-info/PKG-INFO +15 -1
- {py2hackcraft2-1.1.44 → py2hackcraft2-1.2.1}/py2hackCraft2.egg-info/SOURCES.txt +6 -1
- {py2hackcraft2-1.1.44 → py2hackcraft2-1.2.1}/py2hackCraft2.egg-info/top_level.txt +1 -0
- py2hackcraft2-1.2.1/pyproject.toml +8 -0
- {py2hackcraft2-1.1.44 → py2hackcraft2-1.2.1}/setup.py +2 -2
- py2hackcraft2-1.2.1/tests/__init__.py +0 -0
- py2hackcraft2-1.2.1/tests/test_backward_compat.py +59 -0
- py2hackcraft2-1.2.1/tests/test_connection_timeout.py +29 -0
- py2hackcraft2-1.2.1/tests/test_connection_url.py +34 -0
- py2hackcraft2-1.2.1/tests/test_secure_login.py +72 -0
- py2hackcraft2-1.1.44/pyproject.toml +0 -3
- {py2hackcraft2-1.1.44 → py2hackcraft2-1.2.1}/py2hackCraft/__init__.py +0 -0
- {py2hackcraft2-1.1.44 → py2hackcraft2-1.2.1}/py2hackCraft/material.py +0 -0
- {py2hackcraft2-1.1.44 → py2hackcraft2-1.2.1}/py2hackCraft2.egg-info/dependency_links.txt +0 -0
- {py2hackcraft2-1.1.44 → py2hackcraft2-1.2.1}/py2hackCraft2.egg-info/requires.txt +0 -0
- {py2hackcraft2-1.1.44 → py2hackcraft2-1.2.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py2hackCraft2
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Python client library for hackCraft2
|
|
5
5
|
Home-page: https://github.com/0x48lab/hackCraft2-python
|
|
6
6
|
Author: masafumi_t
|
|
@@ -65,6 +65,20 @@ entity.set_event_area(Volume.local(10, 10, 10, -10, -10, -10))
|
|
|
65
65
|
# その他の操作...
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
### セキュア(wss)接続(Colab / リモート)
|
|
69
|
+
|
|
70
|
+
Google Colab などから、HTTPS 公開トンネル(Cloudflare 等)越しに動いているサーバーへ接続する場合は `secure=True` を指定します。`port` を省略すると標準TLSポート(443)を使用します。
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# 公開サーバーへ wss 接続
|
|
74
|
+
player.login("your-tunnel.example.com", secure=True)
|
|
75
|
+
|
|
76
|
+
# 自己署名証明書や自前トンネルで証明書検証を無効化する場合(自己責任)
|
|
77
|
+
player.login("self-hosted.example.com", secure=True, verify_ssl=False)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
ローカルサーバーへは従来どおり `player.login("localhost", 25570)` で接続できます(後方互換)。
|
|
81
|
+
|
|
68
82
|
## ライセンス
|
|
69
83
|
|
|
70
84
|
MIT License
|
|
@@ -38,6 +38,20 @@ entity.set_event_area(Volume.local(10, 10, 10, -10, -10, -10))
|
|
|
38
38
|
# その他の操作...
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
### セキュア(wss)接続(Colab / リモート)
|
|
42
|
+
|
|
43
|
+
Google Colab などから、HTTPS 公開トンネル(Cloudflare 等)越しに動いているサーバーへ接続する場合は `secure=True` を指定します。`port` を省略すると標準TLSポート(443)を使用します。
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# 公開サーバーへ wss 接続
|
|
47
|
+
player.login("your-tunnel.example.com", secure=True)
|
|
48
|
+
|
|
49
|
+
# 自己署名証明書や自前トンネルで証明書検証を無効化する場合(自己責任)
|
|
50
|
+
player.login("self-hosted.example.com", secure=True, verify_ssl=False)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
ローカルサーバーへは従来どおり `player.login("localhost", 25570)` で接続できます(後方互換)。
|
|
54
|
+
|
|
41
55
|
## ライセンス
|
|
42
56
|
|
|
43
57
|
MIT License
|
|
@@ -3,9 +3,45 @@ import threading
|
|
|
3
3
|
import time
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
|
+
import ssl
|
|
7
|
+
import functools
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from typing import Callable, Any, List, Optional, NamedTuple
|
|
8
10
|
|
|
11
|
+
# 標準のTLS(セキュアWebSocket)ポート
|
|
12
|
+
DEFAULT_SECURE_PORT = 443
|
|
13
|
+
|
|
14
|
+
# 接続確立を待つ上限秒数(到達不能・TLS不一致時に無限待機しないため)
|
|
15
|
+
CONNECT_TIMEOUT_SECONDS = 15
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConnectionTimeoutError(Exception):
|
|
19
|
+
"""接続確立が制限時間内に完了しなかったことを示す例外"""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_ws_url(host: str, port: Optional[int], secure: bool) -> str:
|
|
24
|
+
"""接続先 WebSocket URL を組み立てる(副作用なし)。
|
|
25
|
+
|
|
26
|
+
- secure=True: ``wss://{host}:{port or 443}/ws``
|
|
27
|
+
- secure=False: ``ws://{host}:{port}/ws``(従来どおり)
|
|
28
|
+
"""
|
|
29
|
+
scheme = "wss" if secure else "ws"
|
|
30
|
+
if secure and port is None:
|
|
31
|
+
port = DEFAULT_SECURE_PORT
|
|
32
|
+
return "%s://%s:%d/ws" % (scheme, host, port)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_sslopt(secure: bool, verify_ssl: bool) -> Optional[dict]:
|
|
36
|
+
"""run_forever に渡す sslopt を組み立てる(副作用なし)。
|
|
37
|
+
|
|
38
|
+
セキュア接続で証明書検証を無効化する場合のみ ``{"cert_reqs": ssl.CERT_NONE}``。
|
|
39
|
+
それ以外(非セキュア / 既定検証)は None(websocket-client の既定挙動に委ねる)。
|
|
40
|
+
"""
|
|
41
|
+
if secure and not verify_ssl:
|
|
42
|
+
return {"cert_reqs": ssl.CERT_NONE}
|
|
43
|
+
return None
|
|
44
|
+
|
|
9
45
|
def str_to_bool(s):
|
|
10
46
|
"""
|
|
11
47
|
文字列をブール値に変換する
|
|
@@ -44,10 +80,11 @@ class _WebSocketClient:
|
|
|
44
80
|
self.response_event = threading.Event()
|
|
45
81
|
self.callbacks = {}
|
|
46
82
|
|
|
47
|
-
def connect(self, host: str, port: int
|
|
83
|
+
def connect(self, host: str, port: Optional[int] = None,
|
|
84
|
+
secure: bool = False, verify_ssl: bool = True):
|
|
48
85
|
self.host = host
|
|
49
86
|
self.port = port
|
|
50
|
-
self.url =
|
|
87
|
+
self.url = build_ws_url(host, port, secure)
|
|
51
88
|
logging.debug("connecting '%s'" % (self.url))
|
|
52
89
|
self.connected = False
|
|
53
90
|
self.ws = websocket.WebSocketApp(self.url,
|
|
@@ -55,7 +92,10 @@ class _WebSocketClient:
|
|
|
55
92
|
on_error=self._on_error,
|
|
56
93
|
on_close=self._on_close)
|
|
57
94
|
self.ws.on_open = self._on_open
|
|
58
|
-
|
|
95
|
+
# セキュア接続で証明書検証を無効化する場合のみ sslopt を渡す(None なら既定挙動)
|
|
96
|
+
run_forever = functools.partial(self.ws.run_forever,
|
|
97
|
+
sslopt=build_sslopt(secure, verify_ssl))
|
|
98
|
+
self.thread = threading.Thread(target=run_forever)
|
|
59
99
|
self.thread.daemon = True
|
|
60
100
|
self._run_forever()
|
|
61
101
|
|
|
@@ -118,8 +158,16 @@ class _WebSocketClient:
|
|
|
118
158
|
def _run_forever(self):
|
|
119
159
|
self.thread.start()
|
|
120
160
|
|
|
121
|
-
def _wait_for_connection(self):
|
|
161
|
+
def _wait_for_connection(self, timeout: float = CONNECT_TIMEOUT_SECONDS):
|
|
162
|
+
# 一定時間内に接続が確立しない場合は例外を送出し、無限待機を避ける
|
|
163
|
+
deadline = time.monotonic() + timeout
|
|
122
164
|
while not self.connected:
|
|
165
|
+
if time.monotonic() >= deadline:
|
|
166
|
+
raise ConnectionTimeoutError(
|
|
167
|
+
"接続を確立できませんでした(%s に %d 秒以内に接続できませんでした)。"
|
|
168
|
+
"ホスト/ポート、secure 指定、TLS証明書の検証設定を確認してください。"
|
|
169
|
+
% (getattr(self, "url", "?"), int(timeout))
|
|
170
|
+
)
|
|
123
171
|
time.sleep(0.1)
|
|
124
172
|
|
|
125
173
|
|
|
@@ -193,12 +241,16 @@ class Location:
|
|
|
193
241
|
- "~": 相対座標 (例: ~10, ~0, ~-5)
|
|
194
242
|
|
|
195
243
|
- "^": ローカル座標 (例: ^0, ^5, ^0)
|
|
244
|
+
yaw (float): 水平方向の向き(デフォルトは0)
|
|
245
|
+
pitch (float): 垂直方向の向き(デフォルトは0)
|
|
196
246
|
"""
|
|
197
247
|
x: int
|
|
198
248
|
y: int
|
|
199
249
|
z: int
|
|
200
250
|
world: str = "world"
|
|
201
251
|
cord: str = "" # デフォルトは絶対座標
|
|
252
|
+
yaw: float = 0
|
|
253
|
+
pitch: float = 0
|
|
202
254
|
|
|
203
255
|
class LocationFactory:
|
|
204
256
|
"""
|
|
@@ -444,9 +496,18 @@ class Player:
|
|
|
444
496
|
def __init__(self, player: str):
|
|
445
497
|
self.name = player
|
|
446
498
|
|
|
447
|
-
def login(self, host: str, port: int
|
|
499
|
+
def login(self, host: str, port: Optional[int] = None, *,
|
|
500
|
+
secure: bool = False, verify_ssl: bool = True) -> 'Player':
|
|
501
|
+
"""サーバーにログインする。
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
host: サーバーのホスト名 / IP / 公開トンネルのドメイン
|
|
505
|
+
port: ポート番号。secure=True で省略した場合は標準TLSポート(443)を使用
|
|
506
|
+
secure: True で wss(TLS暗号化)接続。Colab 等から公開サーバーへ接続する場合に使用
|
|
507
|
+
verify_ssl: secure=True 時のTLS証明書検証。自己署名/自前トンネル向けに False で無効化可能
|
|
508
|
+
"""
|
|
448
509
|
self.client = _WebSocketClient()
|
|
449
|
-
self.client.connect(host, port)
|
|
510
|
+
self.client.connect(host, port, secure=secure, verify_ssl=verify_ssl)
|
|
450
511
|
self.client.send(json.dumps({
|
|
451
512
|
"type": "login",
|
|
452
513
|
"data": {
|
|
@@ -898,11 +959,11 @@ class Entity:
|
|
|
898
959
|
|
|
899
960
|
def pop(self) -> bool:
|
|
900
961
|
"""
|
|
901
|
-
|
|
962
|
+
自分の位置を保存した位置に戻す(ブロックがあっても移動可能、向きも復元)
|
|
902
963
|
"""
|
|
903
964
|
if(len(self.positions) > 0):
|
|
904
965
|
pos = self.positions.pop()
|
|
905
|
-
self.
|
|
966
|
+
self.client.send_call(self.uuid, "forceTeleport", [pos.x, pos.y, pos.z, pos.cord, pos.yaw, pos.pitch])
|
|
906
967
|
return True
|
|
907
968
|
else:
|
|
908
969
|
return False
|
|
@@ -1358,6 +1419,156 @@ class Entity:
|
|
|
1358
1419
|
self.client.send_call(self.uuid, "digX", [0, -1, 0])
|
|
1359
1420
|
return str_to_bool(self.client.result)
|
|
1360
1421
|
|
|
1422
|
+
# ===== Sign Operations =====
|
|
1423
|
+
|
|
1424
|
+
def write_sign(self, text) -> bool:
|
|
1425
|
+
"""
|
|
1426
|
+
自分の前方の看板にテキストを書き込む
|
|
1427
|
+
|
|
1428
|
+
Args:
|
|
1429
|
+
text: 書き込むテキスト。文字列またはリスト(各要素が各行になる、最大4行)
|
|
1430
|
+
|
|
1431
|
+
Returns:
|
|
1432
|
+
bool: 操作が成功した場合はTrue、失敗した場合はFalse
|
|
1433
|
+
|
|
1434
|
+
Examples:
|
|
1435
|
+
文字列で書き込む::
|
|
1436
|
+
|
|
1437
|
+
entity.write_sign("Hello World")
|
|
1438
|
+
|
|
1439
|
+
複数行を書き込む::
|
|
1440
|
+
|
|
1441
|
+
entity.write_sign(["1行目", "2行目", "3行目", "4行目"])
|
|
1442
|
+
"""
|
|
1443
|
+
if isinstance(text, list):
|
|
1444
|
+
text = '\n'.join(str(line) for line in text[:4])
|
|
1445
|
+
self.client.send_call(self.uuid, "setSign", [str(text), "front"])
|
|
1446
|
+
return str_to_bool(self.client.result)
|
|
1447
|
+
|
|
1448
|
+
def write_sign_up(self, text) -> bool:
|
|
1449
|
+
"""
|
|
1450
|
+
自分の真上の看板にテキストを書き込む
|
|
1451
|
+
|
|
1452
|
+
Args:
|
|
1453
|
+
text: 書き込むテキスト。文字列またはリスト(各要素が各行になる、最大4行)
|
|
1454
|
+
|
|
1455
|
+
Returns:
|
|
1456
|
+
bool: 操作が成功した場合はTrue、失敗した場合はFalse
|
|
1457
|
+
"""
|
|
1458
|
+
if isinstance(text, list):
|
|
1459
|
+
text = '\n'.join(str(line) for line in text[:4])
|
|
1460
|
+
self.client.send_call(self.uuid, "setSign", [str(text), "up"])
|
|
1461
|
+
return str_to_bool(self.client.result)
|
|
1462
|
+
|
|
1463
|
+
def write_sign_down(self, text) -> bool:
|
|
1464
|
+
"""
|
|
1465
|
+
自分の真下の看板にテキストを書き込む
|
|
1466
|
+
|
|
1467
|
+
Args:
|
|
1468
|
+
text: 書き込むテキスト。文字列またはリスト(各要素が各行になる、最大4行)
|
|
1469
|
+
|
|
1470
|
+
Returns:
|
|
1471
|
+
bool: 操作が成功した場合はTrue、失敗した場合はFalse
|
|
1472
|
+
"""
|
|
1473
|
+
if isinstance(text, list):
|
|
1474
|
+
text = '\n'.join(str(line) for line in text[:4])
|
|
1475
|
+
self.client.send_call(self.uuid, "setSign", [str(text), "down"])
|
|
1476
|
+
return str_to_bool(self.client.result)
|
|
1477
|
+
|
|
1478
|
+
def write_sign_at(self, loc: Location, text) -> bool:
|
|
1479
|
+
"""
|
|
1480
|
+
指定した座標の看板にテキストを書き込む
|
|
1481
|
+
|
|
1482
|
+
Args:
|
|
1483
|
+
loc (Location): 座標情報(LocationFactory.absolute/relative/localで生成)
|
|
1484
|
+
text: 書き込むテキスト。文字列またはリスト(各要素が各行になる、最大4行)
|
|
1485
|
+
|
|
1486
|
+
Returns:
|
|
1487
|
+
bool: 操作が成功した場合はTrue、失敗した場合はFalse
|
|
1488
|
+
|
|
1489
|
+
Examples:
|
|
1490
|
+
相対座標で指定::
|
|
1491
|
+
|
|
1492
|
+
loc = LocationFactory.relative(0, 1, 0) # 1ブロック上
|
|
1493
|
+
entity.write_sign_at(loc, "相対座標のメッセージ")
|
|
1494
|
+
|
|
1495
|
+
ローカル座標で指定::
|
|
1496
|
+
|
|
1497
|
+
loc = LocationFactory.local(0, 0, 1) # 前方1ブロック
|
|
1498
|
+
entity.write_sign_at(loc, "ローカル座標のメッセージ")
|
|
1499
|
+
|
|
1500
|
+
複数行を書き込む::
|
|
1501
|
+
|
|
1502
|
+
loc = LocationFactory.local(0, 0, 1)
|
|
1503
|
+
entity.write_sign_at(loc, ["1行目", "2行目", "3行目"])
|
|
1504
|
+
"""
|
|
1505
|
+
if isinstance(text, list):
|
|
1506
|
+
text = '\n'.join(str(line) for line in text[:4])
|
|
1507
|
+
self.client.send_call(self.uuid, "setSignX", [str(text), loc.x, loc.y, loc.z, loc.cord])
|
|
1508
|
+
return str_to_bool(self.client.result)
|
|
1509
|
+
|
|
1510
|
+
def read_sign(self) -> str:
|
|
1511
|
+
"""
|
|
1512
|
+
自分の前方の看板のテキストを読み取る
|
|
1513
|
+
|
|
1514
|
+
Returns:
|
|
1515
|
+
str: 看板のテキスト(複数行の場合は改行で区切られた文字列)、看板がない場合は空文字列
|
|
1516
|
+
|
|
1517
|
+
Examples:
|
|
1518
|
+
テキストを読み取る::
|
|
1519
|
+
|
|
1520
|
+
text = entity.read_sign()
|
|
1521
|
+
print(text) # "1行目\\n2行目\\n..."
|
|
1522
|
+
"""
|
|
1523
|
+
self.client.send_call(self.uuid, "getSign", ["front"])
|
|
1524
|
+
return self.client.result if self.client.result else ""
|
|
1525
|
+
|
|
1526
|
+
def read_sign_up(self) -> str:
|
|
1527
|
+
"""
|
|
1528
|
+
自分の真上の看板のテキストを読み取る
|
|
1529
|
+
|
|
1530
|
+
Returns:
|
|
1531
|
+
str: 看板のテキスト(複数行の場合は改行で区切られた文字列)、看板がない場合は空文字列
|
|
1532
|
+
"""
|
|
1533
|
+
self.client.send_call(self.uuid, "getSign", ["up"])
|
|
1534
|
+
return self.client.result if self.client.result else ""
|
|
1535
|
+
|
|
1536
|
+
def read_sign_down(self) -> str:
|
|
1537
|
+
"""
|
|
1538
|
+
自分の真下の看板のテキストを読み取る
|
|
1539
|
+
|
|
1540
|
+
Returns:
|
|
1541
|
+
str: 看板のテキスト(複数行の場合は改行で区切られた文字列)、看板がない場合は空文字列
|
|
1542
|
+
"""
|
|
1543
|
+
self.client.send_call(self.uuid, "getSign", ["down"])
|
|
1544
|
+
return self.client.result if self.client.result else ""
|
|
1545
|
+
|
|
1546
|
+
def read_sign_at(self, loc: Location) -> str:
|
|
1547
|
+
"""
|
|
1548
|
+
指定した座標の看板のテキストを読み取る
|
|
1549
|
+
|
|
1550
|
+
Args:
|
|
1551
|
+
loc (Location): 座標情報(LocationFactory.absolute/relative/localで生成)
|
|
1552
|
+
|
|
1553
|
+
Returns:
|
|
1554
|
+
str: 看板のテキスト(複数行の場合は改行で区切られた文字列)、看板がない場合は空文字列
|
|
1555
|
+
|
|
1556
|
+
Examples:
|
|
1557
|
+
相対座標で指定::
|
|
1558
|
+
|
|
1559
|
+
loc = LocationFactory.relative(0, 1, 0) # 1ブロック上
|
|
1560
|
+
text = entity.read_sign_at(loc)
|
|
1561
|
+
|
|
1562
|
+
ローカル座標で指定::
|
|
1563
|
+
|
|
1564
|
+
loc = LocationFactory.local(0, 0, 1) # 前方1ブロック
|
|
1565
|
+
text = entity.read_sign_at(loc)
|
|
1566
|
+
"""
|
|
1567
|
+
self.client.send_call(self.uuid, "getSignX", [loc.x, loc.y, loc.z, loc.cord])
|
|
1568
|
+
return self.client.result if self.client.result else ""
|
|
1569
|
+
|
|
1570
|
+
# ===== End Sign Operations =====
|
|
1571
|
+
|
|
1361
1572
|
def attack(self) -> bool:
|
|
1362
1573
|
"""
|
|
1363
1574
|
自分の前方を攻撃する
|
|
@@ -2057,6 +2268,197 @@ class Entity:
|
|
|
2057
2268
|
|
|
2058
2269
|
# ===== Livestock Methods =====
|
|
2059
2270
|
|
|
2271
|
+
def get_biome(self) -> str:
|
|
2272
|
+
"""
|
|
2273
|
+
自分の現在位置のバイオームを取得する
|
|
2274
|
+
|
|
2275
|
+
Returns:
|
|
2276
|
+
str: バイオーム名(例: "PLAINS", "DESERT", "FOREST")
|
|
2277
|
+
"""
|
|
2278
|
+
self.client.send_call(self.uuid, "getBiome")
|
|
2279
|
+
return self.client.result
|
|
2280
|
+
|
|
2281
|
+
def get_biome_at(self, loc: Location) -> str:
|
|
2282
|
+
"""
|
|
2283
|
+
指定された座標のバイオームを取得する
|
|
2284
|
+
|
|
2285
|
+
Args:
|
|
2286
|
+
loc (Location): 座標情報(LocationFactory.absolute/relative/localで生成)
|
|
2287
|
+
|
|
2288
|
+
Returns:
|
|
2289
|
+
str: バイオーム名(例: "PLAINS", "DESERT", "FOREST")
|
|
2290
|
+
"""
|
|
2291
|
+
self.client.send_call(self.uuid, "getBiomeAt", [loc.x, loc.y, loc.z, loc.cord])
|
|
2292
|
+
return self.client.result
|
|
2293
|
+
|
|
2294
|
+
def get_time(self) -> int:
|
|
2295
|
+
"""
|
|
2296
|
+
ワールドの時刻を取得する
|
|
2297
|
+
|
|
2298
|
+
Returns:
|
|
2299
|
+
int: ワールドの時刻(0-24000のtick値。0=朝6時, 6000=正午, 12000=夕方, 18000=真夜中)
|
|
2300
|
+
"""
|
|
2301
|
+
self.client.send_call(self.uuid, "getTime")
|
|
2302
|
+
return int(self.client.result)
|
|
2303
|
+
|
|
2304
|
+
def get_weather(self) -> str:
|
|
2305
|
+
"""
|
|
2306
|
+
ワールドの天気を取得する
|
|
2307
|
+
|
|
2308
|
+
Returns:
|
|
2309
|
+
str: 天気("clear", "rain", "thunder"のいずれか)
|
|
2310
|
+
"""
|
|
2311
|
+
self.client.send_call(self.uuid, "getWeather")
|
|
2312
|
+
return self.client.result
|
|
2313
|
+
|
|
2314
|
+
def get_server_time(self) -> int:
|
|
2315
|
+
"""
|
|
2316
|
+
サーバーの現在時刻をUnixタイムスタンプ(ミリ秒)で取得する
|
|
2317
|
+
イベントログの取得開始時刻の記録に使用する
|
|
2318
|
+
|
|
2319
|
+
Returns:
|
|
2320
|
+
int: サーバーの現在時刻(Unixタイムスタンプ、ミリ秒)
|
|
2321
|
+
|
|
2322
|
+
Example:
|
|
2323
|
+
.. code-block:: python
|
|
2324
|
+
|
|
2325
|
+
start = entity.get_server_time()
|
|
2326
|
+
# ... 何か作業 ...
|
|
2327
|
+
log = entity.get_event_log(since=start)
|
|
2328
|
+
"""
|
|
2329
|
+
self.client.send_call(self.uuid, "getServerTime")
|
|
2330
|
+
return int(self.client.result)
|
|
2331
|
+
|
|
2332
|
+
def get_event_log(self, event_types: list = None, limit: int = 200) -> list:
|
|
2333
|
+
"""
|
|
2334
|
+
ワールドのイベントログを取得する(差分取得)
|
|
2335
|
+
呼び出すたびに、前回取得した位置以降の新しいイベントだけを返す
|
|
2336
|
+
サーバー側でエンティティごとにカーソルを管理しているため、sinceの指定は不要
|
|
2337
|
+
|
|
2338
|
+
Args:
|
|
2339
|
+
event_types (list, optional): 取得するイベントの種類(例: ["fish", "block_break"])
|
|
2340
|
+
limit (int): 最大取得件数(デフォルト: 200、最大: 200)
|
|
2341
|
+
|
|
2342
|
+
Returns:
|
|
2343
|
+
list: 新しいイベントのリスト。各イベントは辞書形式
|
|
2344
|
+
[{"type": "fish", "player": "taro", "item": "cod", "timestamp": 1714020000000}, ...]
|
|
2345
|
+
|
|
2346
|
+
Example:
|
|
2347
|
+
.. code-block:: python
|
|
2348
|
+
|
|
2349
|
+
new_events = entity.get_event_log(["fish"])
|
|
2350
|
+
for event in new_events:
|
|
2351
|
+
print(f"{event['player']}が{event['item']}を釣りました")
|
|
2352
|
+
"""
|
|
2353
|
+
types_str = ",".join(event_types) if event_types else None
|
|
2354
|
+
self.client.send_call(self.uuid, "getEventLog", [types_str, limit])
|
|
2355
|
+
result = json.loads(self.client.result)
|
|
2356
|
+
return result.get("events", [])
|
|
2357
|
+
|
|
2358
|
+
def get_event_log_status(self) -> dict:
|
|
2359
|
+
"""
|
|
2360
|
+
イベントログの状態を取得する
|
|
2361
|
+
|
|
2362
|
+
Returns:
|
|
2363
|
+
dict: ログの状態情報
|
|
2364
|
+
{"eventCount": 1234, "maxEvents": 50000, "serverTime": 1714020000000}
|
|
2365
|
+
"""
|
|
2366
|
+
self.client.send_call(self.uuid, "getEventLogStatus")
|
|
2367
|
+
return json.loads(self.client.result)
|
|
2368
|
+
|
|
2369
|
+
def scan_blocks(self, volume: 'Volume') -> list:
|
|
2370
|
+
"""
|
|
2371
|
+
指定範囲のブロックを一括取得する(最大32×32×32)
|
|
2372
|
+
|
|
2373
|
+
Args:
|
|
2374
|
+
volume (Volume): スキャンする範囲
|
|
2375
|
+
|
|
2376
|
+
Returns:
|
|
2377
|
+
list: ブロック情報のリスト
|
|
2378
|
+
[{"name": "stone", "x": 100, "y": 64, "z": -200, ...}, ...]
|
|
2379
|
+
|
|
2380
|
+
Example:
|
|
2381
|
+
.. code-block:: python
|
|
2382
|
+
|
|
2383
|
+
area = Volume.relative(-5, -5, -5, 5, 5, 5)
|
|
2384
|
+
blocks = entity.scan_blocks(area)
|
|
2385
|
+
print(f"{len(blocks)}ブロックをスキャンしました")
|
|
2386
|
+
"""
|
|
2387
|
+
x1, y1, z1, cord = volume.pos1
|
|
2388
|
+
x2, y2, z2, _ = volume.pos2
|
|
2389
|
+
self.client.send_call(self.uuid, "scanBlocks", [x1, y1, z1, x2, y2, z2, cord])
|
|
2390
|
+
return json.loads(self.client.result)
|
|
2391
|
+
|
|
2392
|
+
def reset_event_cursor(self):
|
|
2393
|
+
"""
|
|
2394
|
+
イベントログのカーソルをリセットする
|
|
2395
|
+
次回のget_event_log()は現在時刻以降のイベントから返すようになる
|
|
2396
|
+
|
|
2397
|
+
Example:
|
|
2398
|
+
.. code-block:: python
|
|
2399
|
+
|
|
2400
|
+
entity.reset_event_cursor()
|
|
2401
|
+
# ここから新しいイベントだけを取得する
|
|
2402
|
+
events = entity.get_event_log(["fish"])
|
|
2403
|
+
"""
|
|
2404
|
+
self.client.send_call(self.uuid, "resetEventCursor")
|
|
2405
|
+
|
|
2406
|
+
def watch_events(self, event_types: list = None, interval: int = 1, callback: Callable = None) -> list:
|
|
2407
|
+
"""
|
|
2408
|
+
リアルタイムでイベントを監視する
|
|
2409
|
+
1秒ごとに新しいイベントを取得し、callbackが設定されていれば呼び出す
|
|
2410
|
+
Ctrl+C(KeyboardInterrupt)で停止し、全イベントを返す
|
|
2411
|
+
サーバー側でカーソルを管理するため、差分だけが自動的に返される
|
|
2412
|
+
|
|
2413
|
+
Args:
|
|
2414
|
+
event_types (list, optional): 監視するイベントの種類(例: ["fish", "block_break"])
|
|
2415
|
+
interval (int): ポーリング間隔(秒)。デフォルト: 1
|
|
2416
|
+
callback (Callable, optional): 新しいイベントが来たときに呼ばれる関数
|
|
2417
|
+
callback(new_events, all_events) の形式で呼ばれる
|
|
2418
|
+
new_events: 今回新しく取得したイベントのリスト
|
|
2419
|
+
all_events: これまでの全イベントのリスト
|
|
2420
|
+
|
|
2421
|
+
Returns:
|
|
2422
|
+
list: 監視中に収集した全イベントのリスト
|
|
2423
|
+
|
|
2424
|
+
Example:
|
|
2425
|
+
.. code-block:: python
|
|
2426
|
+
|
|
2427
|
+
events = entity.watch_events(["fish"])
|
|
2428
|
+
|
|
2429
|
+
.. code-block:: python
|
|
2430
|
+
|
|
2431
|
+
from IPython.display import clear_output
|
|
2432
|
+
import matplotlib.pyplot as plt
|
|
2433
|
+
import pandas as pd
|
|
2434
|
+
|
|
2435
|
+
def update_graph(new_events, all_events):
|
|
2436
|
+
clear_output(wait=True)
|
|
2437
|
+
df = pd.DataFrame(all_events)
|
|
2438
|
+
df["item"].value_counts().plot(kind="bar")
|
|
2439
|
+
plt.title(f"釣り結果({len(df)}回)")
|
|
2440
|
+
plt.show()
|
|
2441
|
+
|
|
2442
|
+
events = entity.watch_events(["fish"], callback=update_graph)
|
|
2443
|
+
"""
|
|
2444
|
+
self.reset_event_cursor()
|
|
2445
|
+
all_events = []
|
|
2446
|
+
|
|
2447
|
+
try:
|
|
2448
|
+
while True:
|
|
2449
|
+
new_events = self.get_event_log(event_types=event_types)
|
|
2450
|
+
if new_events:
|
|
2451
|
+
all_events.extend(new_events)
|
|
2452
|
+
if callback:
|
|
2453
|
+
callback(new_events, all_events)
|
|
2454
|
+
time.sleep(interval)
|
|
2455
|
+
except KeyboardInterrupt:
|
|
2456
|
+
pass
|
|
2457
|
+
|
|
2458
|
+
return all_events
|
|
2459
|
+
|
|
2460
|
+
# ===== Livestock Methods =====
|
|
2461
|
+
|
|
2060
2462
|
def livestock_count_nearby(self, animal_type: str = "ALL", radius: float = 50.0) -> int:
|
|
2061
2463
|
"""
|
|
2062
2464
|
近くの動物を数える
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py2hackCraft2
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Python client library for hackCraft2
|
|
5
5
|
Home-page: https://github.com/0x48lab/hackCraft2-python
|
|
6
6
|
Author: masafumi_t
|
|
@@ -65,6 +65,20 @@ entity.set_event_area(Volume.local(10, 10, 10, -10, -10, -10))
|
|
|
65
65
|
# その他の操作...
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
### セキュア(wss)接続(Colab / リモート)
|
|
69
|
+
|
|
70
|
+
Google Colab などから、HTTPS 公開トンネル(Cloudflare 等)越しに動いているサーバーへ接続する場合は `secure=True` を指定します。`port` を省略すると標準TLSポート(443)を使用します。
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# 公開サーバーへ wss 接続
|
|
74
|
+
player.login("your-tunnel.example.com", secure=True)
|
|
75
|
+
|
|
76
|
+
# 自己署名証明書や自前トンネルで証明書検証を無効化する場合(自己責任)
|
|
77
|
+
player.login("self-hosted.example.com", secure=True, verify_ssl=False)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
ローカルサーバーへは従来どおり `player.login("localhost", 25570)` で接続できます(後方互換)。
|
|
81
|
+
|
|
68
82
|
## ライセンス
|
|
69
83
|
|
|
70
84
|
MIT License
|
|
@@ -8,4 +8,9 @@ py2hackCraft2.egg-info/PKG-INFO
|
|
|
8
8
|
py2hackCraft2.egg-info/SOURCES.txt
|
|
9
9
|
py2hackCraft2.egg-info/dependency_links.txt
|
|
10
10
|
py2hackCraft2.egg-info/requires.txt
|
|
11
|
-
py2hackCraft2.egg-info/top_level.txt
|
|
11
|
+
py2hackCraft2.egg-info/top_level.txt
|
|
12
|
+
tests/__init__.py
|
|
13
|
+
tests/test_backward_compat.py
|
|
14
|
+
tests/test_connection_timeout.py
|
|
15
|
+
tests/test_connection_url.py
|
|
16
|
+
tests/test_secure_login.py
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="py2hackCraft2",
|
|
5
|
-
version="v1.1
|
|
5
|
+
version="v1.2.1",
|
|
6
6
|
packages=find_packages(),
|
|
7
7
|
install_requires=[
|
|
8
8
|
"websocket-client>=1.6.0",
|
|
@@ -24,4 +24,4 @@ setup(
|
|
|
24
24
|
"Operating System :: OS Independent",
|
|
25
25
|
],
|
|
26
26
|
python_requires=">=3.7",
|
|
27
|
-
)
|
|
27
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""US2: 後方互換の回帰テスト。
|
|
2
|
+
|
|
3
|
+
既存の login(host, port)(非secure)が従来どおり ws:// URL を構築し、
|
|
4
|
+
sslopt が None(証明書設定の影響なし)であることを検証する。
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
import py2hackCraft.modules as modules
|
|
11
|
+
from py2hackCraft.modules import Player
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _FakeWS:
|
|
15
|
+
last_url = None
|
|
16
|
+
last_sslopt = "UNSET"
|
|
17
|
+
|
|
18
|
+
def __init__(self, url, on_message=None, on_error=None, on_close=None):
|
|
19
|
+
_FakeWS.last_url = url
|
|
20
|
+
self.url = url
|
|
21
|
+
self.on_open = None
|
|
22
|
+
self._on_message = on_message
|
|
23
|
+
|
|
24
|
+
def run_forever(self, sslopt=None, **kwargs):
|
|
25
|
+
_FakeWS.last_sslopt = sslopt
|
|
26
|
+
if self.on_open:
|
|
27
|
+
self.on_open(self)
|
|
28
|
+
|
|
29
|
+
def send(self, message):
|
|
30
|
+
self._on_message(self, json.dumps({
|
|
31
|
+
"type": "logged",
|
|
32
|
+
"data": {"playerUUID": "uuid-xyz", "world": "world"},
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
def close(self):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def patched(monkeypatch):
|
|
41
|
+
_FakeWS.last_url = None
|
|
42
|
+
_FakeWS.last_sslopt = "UNSET"
|
|
43
|
+
monkeypatch.setattr(modules.websocket, "WebSocketApp", _FakeWS)
|
|
44
|
+
|
|
45
|
+
def sync_run_forever(self):
|
|
46
|
+
self.thread.start()
|
|
47
|
+
self.thread.join()
|
|
48
|
+
monkeypatch.setattr(modules._WebSocketClient, "_run_forever", sync_run_forever)
|
|
49
|
+
return _FakeWS
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_legacy_login_builds_plain_ws_url(patched):
|
|
53
|
+
# 既存の呼び出し方法(位置引数 host, port)が従来どおり動作すること
|
|
54
|
+
player = Player("tester")
|
|
55
|
+
player.login("localhost", 25570)
|
|
56
|
+
assert patched.last_url == "ws://localhost:25570/ws"
|
|
57
|
+
# 非secure では sslopt は None(verify_ssl フラグの影響を受けない)
|
|
58
|
+
assert patched.last_sslopt is None
|
|
59
|
+
assert player.uuid == "uuid-xyz"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""接続確立タイムアウトの単体テスト(FR-005: 無限待機しない)。
|
|
2
|
+
|
|
3
|
+
実接続せず、connected が True にならない状況で _wait_for_connection が
|
|
4
|
+
短時間で ConnectionTimeoutError を送出することを検証する。
|
|
5
|
+
"""
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from py2hackCraft.modules import _WebSocketClient, ConnectionTimeoutError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_wait_for_connection_times_out_when_never_connected():
|
|
14
|
+
client = _WebSocketClient()
|
|
15
|
+
client.url = "wss://unreachable.example.com:443/ws"
|
|
16
|
+
# connected は False のまま(接続成立しないケース)
|
|
17
|
+
start = time.monotonic()
|
|
18
|
+
with pytest.raises(ConnectionTimeoutError):
|
|
19
|
+
client._wait_for_connection(timeout=0.3)
|
|
20
|
+
elapsed = time.monotonic() - start
|
|
21
|
+
# 上限時間付近で速やかに失敗すること(無限待機しない)
|
|
22
|
+
assert elapsed < 5
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_wait_for_connection_returns_immediately_when_connected():
|
|
26
|
+
client = _WebSocketClient()
|
|
27
|
+
client.connected = True
|
|
28
|
+
# 既に接続済みなら即座に戻る(例外なし)
|
|
29
|
+
client._wait_for_connection(timeout=0.3)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""build_ws_url / build_sslopt の純粋ロジック単体テスト(contracts/login-api.md 準拠)。
|
|
2
|
+
|
|
3
|
+
実 WebSocket 接続を行わず、URL と TLS オプションの組み立てのみを検証する。
|
|
4
|
+
"""
|
|
5
|
+
import ssl
|
|
6
|
+
|
|
7
|
+
from py2hackCraft.modules import build_ws_url, build_sslopt
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestBuildWsUrl:
|
|
11
|
+
def test_local_non_secure(self):
|
|
12
|
+
assert build_ws_url("localhost", 25570, False) == "ws://localhost:25570/ws"
|
|
13
|
+
|
|
14
|
+
def test_secure_default_tls_port(self):
|
|
15
|
+
# secure かつ port 省略(None) → 標準TLSポート 443
|
|
16
|
+
assert build_ws_url("example.com", None, True) == "wss://example.com:443/ws"
|
|
17
|
+
|
|
18
|
+
def test_secure_custom_port(self):
|
|
19
|
+
assert build_ws_url("example.com", 8443, True) == "wss://example.com:8443/ws"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestBuildSslopt:
|
|
23
|
+
def test_non_secure_returns_none(self):
|
|
24
|
+
assert build_sslopt(False, True) is None
|
|
25
|
+
|
|
26
|
+
def test_secure_verify_returns_none(self):
|
|
27
|
+
# 既定検証は websocket-client の既定挙動に委ねる → None
|
|
28
|
+
assert build_sslopt(True, True) is None
|
|
29
|
+
|
|
30
|
+
def test_secure_no_verify_disables_cert_check(self):
|
|
31
|
+
assert build_sslopt(True, False) == {"cert_reqs": ssl.CERT_NONE}
|
|
32
|
+
|
|
33
|
+
def test_non_secure_ignores_verify_flag(self):
|
|
34
|
+
assert build_sslopt(False, False) is None
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""US1: secure(wss) ログインの統合テスト。
|
|
2
|
+
|
|
3
|
+
実サーバーには接続せず、websocket.WebSocketApp と run_forever を monkeypatch して、
|
|
4
|
+
login(secure=True) が wss URL を構築し sslopt を run_forever に渡すことを検証する。
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import ssl
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
import py2hackCraft.modules as modules
|
|
12
|
+
from py2hackCraft.modules import Player
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _FakeWS:
|
|
16
|
+
"""WebSocketApp の差し替え。run_forever の sslopt を記録し、接続成立させる。"""
|
|
17
|
+
|
|
18
|
+
last_url = None
|
|
19
|
+
last_sslopt = "UNSET"
|
|
20
|
+
|
|
21
|
+
def __init__(self, url, on_message=None, on_error=None, on_close=None):
|
|
22
|
+
_FakeWS.last_url = url
|
|
23
|
+
self.url = url
|
|
24
|
+
self.on_open = None
|
|
25
|
+
self._on_message = on_message
|
|
26
|
+
|
|
27
|
+
def run_forever(self, sslopt=None, **kwargs):
|
|
28
|
+
# connect() が functools.partial(run_forever, sslopt=...) で渡す sslopt を記録
|
|
29
|
+
_FakeWS.last_sslopt = sslopt
|
|
30
|
+
# 接続成立を通知(on_open → _WebSocketClient.connected = True)
|
|
31
|
+
if self.on_open:
|
|
32
|
+
self.on_open(self)
|
|
33
|
+
|
|
34
|
+
def send(self, message):
|
|
35
|
+
# login 送信に対して logged 応答を即返してフローを進める
|
|
36
|
+
self._on_message(self, json.dumps({
|
|
37
|
+
"type": "logged",
|
|
38
|
+
"data": {"playerUUID": "uuid-123", "world": "world"},
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
def close(self):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def patched(monkeypatch):
|
|
47
|
+
_FakeWS.last_url = None
|
|
48
|
+
_FakeWS.last_sslopt = "UNSET"
|
|
49
|
+
monkeypatch.setattr(modules.websocket, "WebSocketApp", _FakeWS)
|
|
50
|
+
|
|
51
|
+
# スレッド起動を同期実行に置き換え(partial(run_forever, sslopt=...) をその場で実行)
|
|
52
|
+
def sync_run_forever(self):
|
|
53
|
+
self.thread.start()
|
|
54
|
+
self.thread.join()
|
|
55
|
+
monkeypatch.setattr(modules._WebSocketClient, "_run_forever", sync_run_forever)
|
|
56
|
+
return _FakeWS
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_secure_login_builds_wss_url_and_passes_sslopt(patched):
|
|
60
|
+
player = Player("tester")
|
|
61
|
+
player.login("example.com", secure=True)
|
|
62
|
+
assert patched.last_url == "wss://example.com:443/ws"
|
|
63
|
+
# verify_ssl 既定(True) → sslopt は None(既定検証)
|
|
64
|
+
assert patched.last_sslopt is None
|
|
65
|
+
assert player.uuid == "uuid-123"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_secure_login_verify_ssl_false_disables_cert_check(patched):
|
|
69
|
+
player = Player("tester")
|
|
70
|
+
player.login("example.com", secure=True, verify_ssl=False)
|
|
71
|
+
assert patched.last_url == "wss://example.com:443/ws"
|
|
72
|
+
assert patched.last_sslopt == {"cert_reqs": ssl.CERT_NONE}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|