soulfire 0.1.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.
- soulfire-0.1.1/.gitignore +61 -0
- soulfire-0.1.1/PKG-INFO +256 -0
- soulfire-0.1.1/README.md +237 -0
- soulfire-0.1.1/pyproject.toml +51 -0
- soulfire-0.1.1/src/soulfire/__init__.py +38 -0
- soulfire-0.1.1/src/soulfire/_auth.py +50 -0
- soulfire-0.1.1/src/soulfire/_install.py +514 -0
- soulfire-0.1.1/src/soulfire/api_docs_pb2.py +40 -0
- soulfire-0.1.1/src/soulfire/api_docs_pb2.pyi +38 -0
- soulfire-0.1.1/src/soulfire/automation_connect.py +1700 -0
- soulfire-0.1.1/src/soulfire/automation_pb2.py +597 -0
- soulfire-0.1.1/src/soulfire/automation_pb2.pyi +758 -0
- soulfire-0.1.1/src/soulfire/behaviors.py +171 -0
- soulfire-0.1.1/src/soulfire/bot.py +1197 -0
- soulfire-0.1.1/src/soulfire/bot_connect.py +1570 -0
- soulfire-0.1.1/src/soulfire/bot_live_connect.py +1310 -0
- soulfire-0.1.1/src/soulfire/bot_live_pb2.py +410 -0
- soulfire-0.1.1/src/soulfire/bot_live_pb2.pyi +681 -0
- soulfire-0.1.1/src/soulfire/bot_pb2.py +651 -0
- soulfire-0.1.1/src/soulfire/bot_pb2.pyi +1092 -0
- soulfire-0.1.1/src/soulfire/client.py +1163 -0
- soulfire-0.1.1/src/soulfire/client_connect.py +530 -0
- soulfire-0.1.1/src/soulfire/client_pb2.py +121 -0
- soulfire-0.1.1/src/soulfire/client_pb2.pyi +107 -0
- soulfire-0.1.1/src/soulfire/command_connect.py +270 -0
- soulfire-0.1.1/src/soulfire/command_pb2.py +84 -0
- soulfire-0.1.1/src/soulfire/command_pb2.pyi +75 -0
- soulfire-0.1.1/src/soulfire/common_pb2.py +266 -0
- soulfire-0.1.1/src/soulfire/common_pb2.pyi +448 -0
- soulfire-0.1.1/src/soulfire/download_connect.py +205 -0
- soulfire-0.1.1/src/soulfire/download_pb2.py +63 -0
- soulfire-0.1.1/src/soulfire/download_pb2.pyi +41 -0
- soulfire-0.1.1/src/soulfire/instance_connect.py +1700 -0
- soulfire-0.1.1/src/soulfire/instance_live_connect.py +205 -0
- soulfire-0.1.1/src/soulfire/instance_live_pb2.py +54 -0
- soulfire-0.1.1/src/soulfire/instance_live_pb2.pyi +37 -0
- soulfire-0.1.1/src/soulfire/instance_pb2.py +360 -0
- soulfire-0.1.1/src/soulfire/instance_pb2.pyi +441 -0
- soulfire-0.1.1/src/soulfire/login_connect.py +270 -0
- soulfire-0.1.1/src/soulfire/login_pb2.py +70 -0
- soulfire-0.1.1/src/soulfire/login_pb2.pyi +53 -0
- soulfire-0.1.1/src/soulfire/logs_connect.py +270 -0
- soulfire-0.1.1/src/soulfire/logs_pb2.py +93 -0
- soulfire-0.1.1/src/soulfire/logs_pb2.pyi +108 -0
- soulfire-0.1.1/src/soulfire/mc_auth_connect.py +335 -0
- soulfire-0.1.1/src/soulfire/mc_auth_pb2.py +93 -0
- soulfire-0.1.1/src/soulfire/mc_auth_pb2.pyi +87 -0
- soulfire-0.1.1/src/soulfire/metrics_connect.py +270 -0
- soulfire-0.1.1/src/soulfire/metrics_pb2.py +161 -0
- soulfire-0.1.1/src/soulfire/metrics_pb2.pyi +165 -0
- soulfire-0.1.1/src/soulfire/plugin_stats_connect.py +205 -0
- soulfire-0.1.1/src/soulfire/plugin_stats_pb2.py +67 -0
- soulfire-0.1.1/src/soulfire/plugin_stats_pb2.pyi +53 -0
- soulfire-0.1.1/src/soulfire/proxy_check_connect.py +205 -0
- soulfire-0.1.1/src/soulfire/proxy_check_pb2.py +62 -0
- soulfire-0.1.1/src/soulfire/proxy_check_pb2.pyi +42 -0
- soulfire-0.1.1/src/soulfire/py.typed +1 -0
- soulfire-0.1.1/src/soulfire/script_connect.py +985 -0
- soulfire-0.1.1/src/soulfire/script_pb2.py +446 -0
- soulfire-0.1.1/src/soulfire/script_pb2.pyi +668 -0
- soulfire-0.1.1/src/soulfire/server_connect.py +335 -0
- soulfire-0.1.1/src/soulfire/server_pb2.py +80 -0
- soulfire-0.1.1/src/soulfire/server_pb2.pyi +58 -0
- soulfire-0.1.1/src/soulfire/user_connect.py +595 -0
- soulfire-0.1.1/src/soulfire/user_pb2.py +140 -0
- soulfire-0.1.1/src/soulfire/user_pb2.pyi +131 -0
- soulfire-0.1.1/tests/test_auth.py +32 -0
- soulfire-0.1.1/tests/test_bot.py +134 -0
- soulfire-0.1.1/tests/test_client.py +179 -0
- soulfire-0.1.1/tests/test_install.py +32 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Eclipse stuff
|
|
2
|
+
/.classpath
|
|
3
|
+
/.project
|
|
4
|
+
/.settings
|
|
5
|
+
|
|
6
|
+
# NetBeans
|
|
7
|
+
*/nbproject
|
|
8
|
+
nb-configuration.xml
|
|
9
|
+
|
|
10
|
+
# maven
|
|
11
|
+
*/target
|
|
12
|
+
|
|
13
|
+
# vim
|
|
14
|
+
.*.sw[a-p]
|
|
15
|
+
|
|
16
|
+
# virtual machine crash logs, see https://www.java.com/en/download/help/error_hotspot.xml
|
|
17
|
+
hs_err_pid*
|
|
18
|
+
|
|
19
|
+
# various other potential build files
|
|
20
|
+
*/build/
|
|
21
|
+
/bin
|
|
22
|
+
/dist
|
|
23
|
+
/manifest.mf
|
|
24
|
+
*.log
|
|
25
|
+
|
|
26
|
+
# Mac filesystem dust
|
|
27
|
+
.DS_Store
|
|
28
|
+
|
|
29
|
+
# IntelliJ
|
|
30
|
+
*.iml
|
|
31
|
+
*.ipr
|
|
32
|
+
*.iws
|
|
33
|
+
.idea/
|
|
34
|
+
|
|
35
|
+
# Gradle
|
|
36
|
+
.kotlin
|
|
37
|
+
.gradle
|
|
38
|
+
|
|
39
|
+
# Ignore Gradle GUI config
|
|
40
|
+
gradle-app.setting
|
|
41
|
+
|
|
42
|
+
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
|
43
|
+
!gradle-wrapper.jar
|
|
44
|
+
|
|
45
|
+
**/build/
|
|
46
|
+
**/run/
|
|
47
|
+
out/
|
|
48
|
+
|
|
49
|
+
temp/
|
|
50
|
+
tmp/
|
|
51
|
+
.fabric
|
|
52
|
+
|
|
53
|
+
# SDK toolchains
|
|
54
|
+
.soulfire/
|
|
55
|
+
node_modules/
|
|
56
|
+
sdk/typescript/dist/
|
|
57
|
+
sdk/python/.venv/
|
|
58
|
+
sdk/python/.pytest_cache/
|
|
59
|
+
sdk/python/.ruff_cache/
|
|
60
|
+
sdk/python/dist/
|
|
61
|
+
**/__pycache__/
|
soulfire-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: soulfire
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python SDK for automating SoulFire bots over gRPC-Web.
|
|
5
|
+
Project-URL: Homepage, https://soulfiremc.com
|
|
6
|
+
Project-URL: Repository, https://github.com/soulfiremc-com/SoulFire
|
|
7
|
+
Author: SoulFire
|
|
8
|
+
License-Expression: AGPL-3.0-only
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: connectrpc<0.12,>=0.11.1
|
|
11
|
+
Requires-Dist: googleapis-common-protos<2,>=1.70
|
|
12
|
+
Requires-Dist: protobuf<8,>=6.31.1
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: hatchling>=1.27; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest-asyncio<2,>=1.2; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest<10,>=9.1; extra == 'dev'
|
|
17
|
+
Requires-Dist: ruff<0.17,>=0.16; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# SoulFire Python SDK
|
|
21
|
+
|
|
22
|
+
Use `soulfire` to control bots through a SoulFire server from synchronous
|
|
23
|
+
or asyncio Python applications. The SDK always uses gRPC-Web.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install soulfire
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Connect and stream events
|
|
32
|
+
|
|
33
|
+
Create a client with an API token, then select an operator-provisioned instance
|
|
34
|
+
and bot:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import asyncio
|
|
38
|
+
import os
|
|
39
|
+
|
|
40
|
+
from soulfire import SoulFire
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def main() -> None:
|
|
44
|
+
async with SoulFire.connect(
|
|
45
|
+
"https://soulfire.example.com",
|
|
46
|
+
token=os.environ["SOULFIRE_TOKEN"],
|
|
47
|
+
) as soulfire:
|
|
48
|
+
bot = soulfire.instance("instance-uuid").bot("bot-uuid")
|
|
49
|
+
await bot.start()
|
|
50
|
+
|
|
51
|
+
async for event in bot.events():
|
|
52
|
+
print(event)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
asyncio.run(main())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The default event filter includes status, state, inventory, damage, chat, and
|
|
59
|
+
lifecycle events. The stream stays open while the bot is stopped and follows
|
|
60
|
+
the configured account across reconnects.
|
|
61
|
+
|
|
62
|
+
## Control bots
|
|
63
|
+
|
|
64
|
+
Bot intent is persistent. Calling `start()` sets the bot's desired state to
|
|
65
|
+
running, while `stop()` sets it to stopped:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
instance = soulfire.instance("instance-uuid")
|
|
69
|
+
bot = instance.bot("bot-uuid")
|
|
70
|
+
|
|
71
|
+
await bot.start()
|
|
72
|
+
await bot.restart()
|
|
73
|
+
await bot.stop()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Control a group by explicit ID or count:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
await instance.start(count=25)
|
|
80
|
+
await instance.stop(bot_ids=["bot-uuid-1", "bot-uuid-2"])
|
|
81
|
+
await instance.restart()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
With no selection, `start()` targets stopped bots, `stop()` targets desired
|
|
85
|
+
bots, and `restart()` targets desired bots. A `count` selection follows the
|
|
86
|
+
instance's `account.shuffle-accounts` setting. Explicit `bot_ids` always use
|
|
87
|
+
the IDs you provide.
|
|
88
|
+
|
|
89
|
+
## Watch bot status
|
|
90
|
+
|
|
91
|
+
`watch_bot_statuses()` first yields a complete snapshot, then incremental
|
|
92
|
+
updates and removals:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
async for event in instance.watch_bot_statuses():
|
|
96
|
+
event_type = event.WhichOneof("event")
|
|
97
|
+
if event_type == "snapshot":
|
|
98
|
+
print(event.snapshot.bots)
|
|
99
|
+
elif event_type == "update":
|
|
100
|
+
print(event.update.profile_id, event.update.runtime_state)
|
|
101
|
+
elif event_type == "removed_bot_id":
|
|
102
|
+
print("Removed", event.removed_bot_id)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use `await bot.status()` when you only need the current state of one bot. The
|
|
106
|
+
synchronous client exposes the same methods without `await`.
|
|
107
|
+
|
|
108
|
+
## Provision a local server
|
|
109
|
+
|
|
110
|
+
`SoulFire.install()` downloads and verifies the latest SoulFire dedicated
|
|
111
|
+
server and a Temurin 25 runtime, starts the server, and returns an authenticated
|
|
112
|
+
client:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import asyncio
|
|
116
|
+
|
|
117
|
+
from soulfire import SoulFire
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def main() -> None:
|
|
121
|
+
soulfire = await SoulFire.install(
|
|
122
|
+
directory=".soulfire",
|
|
123
|
+
on_log=print,
|
|
124
|
+
)
|
|
125
|
+
try:
|
|
126
|
+
print(soulfire.local_server.base_url)
|
|
127
|
+
bot = soulfire.instance("instance-uuid").bot("bot-uuid")
|
|
128
|
+
# Use the operator-provisioned bot.
|
|
129
|
+
finally:
|
|
130
|
+
await soulfire.close()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
asyncio.run(main())
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Pass `version` to pin a release tag. Existing verified downloads are reused.
|
|
137
|
+
|
|
138
|
+
## Send a chat message
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
await bot.send_chat("Hello from the SoulFire SDK")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Action calls return after the bot game thread executes them. Failed or
|
|
145
|
+
cancelled actions raise `SoulFireActionError`, which includes the action ID and
|
|
146
|
+
server result.
|
|
147
|
+
|
|
148
|
+
## Control inventory and movement
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from soulfire.bot_pb2 import LEFT_CLICK
|
|
152
|
+
|
|
153
|
+
inventory = await bot.inventory()
|
|
154
|
+
print(inventory.slots)
|
|
155
|
+
|
|
156
|
+
await bot.select_hotbar(0)
|
|
157
|
+
await bot.click_inventory(12, LEFT_CLICK)
|
|
158
|
+
await bot.transfer_inventory_slot(12)
|
|
159
|
+
await bot.move_inventory_stack(12, 36)
|
|
160
|
+
|
|
161
|
+
await bot.set_movement(forward=True, sprint=True)
|
|
162
|
+
await bot.look(90, 0)
|
|
163
|
+
await bot.reset_movement()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The synchronous bot exposes the same methods without `await`.
|
|
167
|
+
|
|
168
|
+
## Coordinate multiple controllers
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
lease = await bot.acquire_control(ttl_seconds=30)
|
|
172
|
+
try:
|
|
173
|
+
await bot.send_chat("This action carries the lease token")
|
|
174
|
+
await lease.renew(ttl_seconds=30)
|
|
175
|
+
finally:
|
|
176
|
+
await lease.release()
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Control leases are optional. Acquiring one prevents other clients from issuing
|
|
180
|
+
actions until the lease is released or expires.
|
|
181
|
+
|
|
182
|
+
## Use composable behaviors
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from soulfire import AttackNearest, CollectBlocks, run_behaviors
|
|
186
|
+
|
|
187
|
+
await run_behaviors(
|
|
188
|
+
bot,
|
|
189
|
+
[
|
|
190
|
+
CollectBlocks(["minecraft:oak_log"], max_count=16),
|
|
191
|
+
AttackNearest(["minecraft:zombie"]),
|
|
192
|
+
],
|
|
193
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The package includes `CollectBlocks`, `FollowEntity`, `AttackNearest`,
|
|
197
|
+
`AutoEat`, and `Build`.
|
|
198
|
+
|
|
199
|
+
## Provision instances and accounts
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from soulfire.common_pb2 import MICROSOFT_JAVA_DEVICE_CODE
|
|
203
|
+
|
|
204
|
+
instance = await soulfire.create_instance("automation")
|
|
205
|
+
|
|
206
|
+
async for step in instance.login_device_code(MICROSOFT_JAVA_DEVICE_CODE):
|
|
207
|
+
if step.WhichOneof("data") == "device_code":
|
|
208
|
+
print(step.device_code.verification_uri, step.device_code.user_code)
|
|
209
|
+
elif step.WhichOneof("data") == "account":
|
|
210
|
+
await instance.add_accounts([step.account])
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Instance listing, settings, account and proxy batches, credentials login,
|
|
214
|
+
device-code login, and account refresh are available on async and synchronous
|
|
215
|
+
clients.
|
|
216
|
+
|
|
217
|
+
## Manage an installed server
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
print(soulfire.local_server.version)
|
|
221
|
+
print(soulfire.local_server_logs)
|
|
222
|
+
|
|
223
|
+
await soulfire.restart_local_server()
|
|
224
|
+
await soulfire.stop_local_server()
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Restart keeps the same directory, port, release, and root API token.
|
|
228
|
+
|
|
229
|
+
## Use the synchronous client
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
import os
|
|
233
|
+
|
|
234
|
+
from soulfire import SoulFireSync
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
with SoulFireSync.connect(
|
|
238
|
+
"https://soulfire.example.com",
|
|
239
|
+
token=os.environ["SOULFIRE_TOKEN"],
|
|
240
|
+
) as soulfire:
|
|
241
|
+
bot = soulfire.instance("instance-uuid").bot("bot-uuid")
|
|
242
|
+
bot.send_chat("Hello from Python")
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Generated request messages and service clients are importable from the
|
|
246
|
+
`soulfire` package when an RPC does not have a high-level wrapper:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from soulfire.instance_connect import InstanceServiceClient
|
|
250
|
+
|
|
251
|
+
instances = soulfire.service(InstanceServiceClient)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Instances are not lifecycle units. They compartmentalize accounts, settings,
|
|
255
|
+
proxies, permissions, scripts, and automation. Each bot can be controlled
|
|
256
|
+
independently.
|
soulfire-0.1.1/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# SoulFire Python SDK
|
|
2
|
+
|
|
3
|
+
Use `soulfire` to control bots through a SoulFire server from synchronous
|
|
4
|
+
or asyncio Python applications. The SDK always uses gRPC-Web.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install soulfire
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Connect and stream events
|
|
13
|
+
|
|
14
|
+
Create a client with an API token, then select an operator-provisioned instance
|
|
15
|
+
and bot:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import asyncio
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
from soulfire import SoulFire
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def main() -> None:
|
|
25
|
+
async with SoulFire.connect(
|
|
26
|
+
"https://soulfire.example.com",
|
|
27
|
+
token=os.environ["SOULFIRE_TOKEN"],
|
|
28
|
+
) as soulfire:
|
|
29
|
+
bot = soulfire.instance("instance-uuid").bot("bot-uuid")
|
|
30
|
+
await bot.start()
|
|
31
|
+
|
|
32
|
+
async for event in bot.events():
|
|
33
|
+
print(event)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
asyncio.run(main())
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The default event filter includes status, state, inventory, damage, chat, and
|
|
40
|
+
lifecycle events. The stream stays open while the bot is stopped and follows
|
|
41
|
+
the configured account across reconnects.
|
|
42
|
+
|
|
43
|
+
## Control bots
|
|
44
|
+
|
|
45
|
+
Bot intent is persistent. Calling `start()` sets the bot's desired state to
|
|
46
|
+
running, while `stop()` sets it to stopped:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
instance = soulfire.instance("instance-uuid")
|
|
50
|
+
bot = instance.bot("bot-uuid")
|
|
51
|
+
|
|
52
|
+
await bot.start()
|
|
53
|
+
await bot.restart()
|
|
54
|
+
await bot.stop()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Control a group by explicit ID or count:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
await instance.start(count=25)
|
|
61
|
+
await instance.stop(bot_ids=["bot-uuid-1", "bot-uuid-2"])
|
|
62
|
+
await instance.restart()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
With no selection, `start()` targets stopped bots, `stop()` targets desired
|
|
66
|
+
bots, and `restart()` targets desired bots. A `count` selection follows the
|
|
67
|
+
instance's `account.shuffle-accounts` setting. Explicit `bot_ids` always use
|
|
68
|
+
the IDs you provide.
|
|
69
|
+
|
|
70
|
+
## Watch bot status
|
|
71
|
+
|
|
72
|
+
`watch_bot_statuses()` first yields a complete snapshot, then incremental
|
|
73
|
+
updates and removals:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
async for event in instance.watch_bot_statuses():
|
|
77
|
+
event_type = event.WhichOneof("event")
|
|
78
|
+
if event_type == "snapshot":
|
|
79
|
+
print(event.snapshot.bots)
|
|
80
|
+
elif event_type == "update":
|
|
81
|
+
print(event.update.profile_id, event.update.runtime_state)
|
|
82
|
+
elif event_type == "removed_bot_id":
|
|
83
|
+
print("Removed", event.removed_bot_id)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use `await bot.status()` when you only need the current state of one bot. The
|
|
87
|
+
synchronous client exposes the same methods without `await`.
|
|
88
|
+
|
|
89
|
+
## Provision a local server
|
|
90
|
+
|
|
91
|
+
`SoulFire.install()` downloads and verifies the latest SoulFire dedicated
|
|
92
|
+
server and a Temurin 25 runtime, starts the server, and returns an authenticated
|
|
93
|
+
client:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import asyncio
|
|
97
|
+
|
|
98
|
+
from soulfire import SoulFire
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def main() -> None:
|
|
102
|
+
soulfire = await SoulFire.install(
|
|
103
|
+
directory=".soulfire",
|
|
104
|
+
on_log=print,
|
|
105
|
+
)
|
|
106
|
+
try:
|
|
107
|
+
print(soulfire.local_server.base_url)
|
|
108
|
+
bot = soulfire.instance("instance-uuid").bot("bot-uuid")
|
|
109
|
+
# Use the operator-provisioned bot.
|
|
110
|
+
finally:
|
|
111
|
+
await soulfire.close()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
asyncio.run(main())
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Pass `version` to pin a release tag. Existing verified downloads are reused.
|
|
118
|
+
|
|
119
|
+
## Send a chat message
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
await bot.send_chat("Hello from the SoulFire SDK")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Action calls return after the bot game thread executes them. Failed or
|
|
126
|
+
cancelled actions raise `SoulFireActionError`, which includes the action ID and
|
|
127
|
+
server result.
|
|
128
|
+
|
|
129
|
+
## Control inventory and movement
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from soulfire.bot_pb2 import LEFT_CLICK
|
|
133
|
+
|
|
134
|
+
inventory = await bot.inventory()
|
|
135
|
+
print(inventory.slots)
|
|
136
|
+
|
|
137
|
+
await bot.select_hotbar(0)
|
|
138
|
+
await bot.click_inventory(12, LEFT_CLICK)
|
|
139
|
+
await bot.transfer_inventory_slot(12)
|
|
140
|
+
await bot.move_inventory_stack(12, 36)
|
|
141
|
+
|
|
142
|
+
await bot.set_movement(forward=True, sprint=True)
|
|
143
|
+
await bot.look(90, 0)
|
|
144
|
+
await bot.reset_movement()
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The synchronous bot exposes the same methods without `await`.
|
|
148
|
+
|
|
149
|
+
## Coordinate multiple controllers
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
lease = await bot.acquire_control(ttl_seconds=30)
|
|
153
|
+
try:
|
|
154
|
+
await bot.send_chat("This action carries the lease token")
|
|
155
|
+
await lease.renew(ttl_seconds=30)
|
|
156
|
+
finally:
|
|
157
|
+
await lease.release()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Control leases are optional. Acquiring one prevents other clients from issuing
|
|
161
|
+
actions until the lease is released or expires.
|
|
162
|
+
|
|
163
|
+
## Use composable behaviors
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from soulfire import AttackNearest, CollectBlocks, run_behaviors
|
|
167
|
+
|
|
168
|
+
await run_behaviors(
|
|
169
|
+
bot,
|
|
170
|
+
[
|
|
171
|
+
CollectBlocks(["minecraft:oak_log"], max_count=16),
|
|
172
|
+
AttackNearest(["minecraft:zombie"]),
|
|
173
|
+
],
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The package includes `CollectBlocks`, `FollowEntity`, `AttackNearest`,
|
|
178
|
+
`AutoEat`, and `Build`.
|
|
179
|
+
|
|
180
|
+
## Provision instances and accounts
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from soulfire.common_pb2 import MICROSOFT_JAVA_DEVICE_CODE
|
|
184
|
+
|
|
185
|
+
instance = await soulfire.create_instance("automation")
|
|
186
|
+
|
|
187
|
+
async for step in instance.login_device_code(MICROSOFT_JAVA_DEVICE_CODE):
|
|
188
|
+
if step.WhichOneof("data") == "device_code":
|
|
189
|
+
print(step.device_code.verification_uri, step.device_code.user_code)
|
|
190
|
+
elif step.WhichOneof("data") == "account":
|
|
191
|
+
await instance.add_accounts([step.account])
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Instance listing, settings, account and proxy batches, credentials login,
|
|
195
|
+
device-code login, and account refresh are available on async and synchronous
|
|
196
|
+
clients.
|
|
197
|
+
|
|
198
|
+
## Manage an installed server
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
print(soulfire.local_server.version)
|
|
202
|
+
print(soulfire.local_server_logs)
|
|
203
|
+
|
|
204
|
+
await soulfire.restart_local_server()
|
|
205
|
+
await soulfire.stop_local_server()
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Restart keeps the same directory, port, release, and root API token.
|
|
209
|
+
|
|
210
|
+
## Use the synchronous client
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
import os
|
|
214
|
+
|
|
215
|
+
from soulfire import SoulFireSync
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
with SoulFireSync.connect(
|
|
219
|
+
"https://soulfire.example.com",
|
|
220
|
+
token=os.environ["SOULFIRE_TOKEN"],
|
|
221
|
+
) as soulfire:
|
|
222
|
+
bot = soulfire.instance("instance-uuid").bot("bot-uuid")
|
|
223
|
+
bot.send_chat("Hello from Python")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Generated request messages and service clients are importable from the
|
|
227
|
+
`soulfire` package when an RPC does not have a high-level wrapper:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from soulfire.instance_connect import InstanceServiceClient
|
|
231
|
+
|
|
232
|
+
instances = soulfire.service(InstanceServiceClient)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Instances are not lifecycle units. They compartmentalize accounts, settings,
|
|
236
|
+
proxies, permissions, scripts, and automation. Each bot can be controlled
|
|
237
|
+
independently.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "soulfire"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Python SDK for automating SoulFire bots over gRPC-Web."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "AGPL-3.0-only"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "SoulFire" },
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"connectrpc>=0.11.1,<0.12",
|
|
17
|
+
"googleapis-common-protos>=1.70,<2",
|
|
18
|
+
"protobuf>=6.31.1,<8",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://soulfiremc.com"
|
|
23
|
+
Repository = "https://github.com/soulfiremc-com/SoulFire"
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = [
|
|
27
|
+
"hatchling>=1.27",
|
|
28
|
+
"pytest>=9.1,<10",
|
|
29
|
+
"pytest-asyncio>=1.2,<2",
|
|
30
|
+
"ruff>=0.16,<0.17",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/soulfire"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
addopts = "-q"
|
|
38
|
+
asyncio_mode = "auto"
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
extend-exclude = [
|
|
43
|
+
"src/soulfire/*_connect.py",
|
|
44
|
+
"src/soulfire/*_pb2.py",
|
|
45
|
+
"src/soulfire/*_pb2.pyi",
|
|
46
|
+
]
|
|
47
|
+
line-length = 100
|
|
48
|
+
target-version = "py310"
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint]
|
|
51
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from ._install import LocalSoulFireServer
|
|
2
|
+
from .behaviors import (
|
|
3
|
+
AttackNearest,
|
|
4
|
+
AutoEat,
|
|
5
|
+
BotBehavior,
|
|
6
|
+
Build,
|
|
7
|
+
BuildPlacement,
|
|
8
|
+
CollectBlocks,
|
|
9
|
+
FollowEntity,
|
|
10
|
+
run_behaviors,
|
|
11
|
+
)
|
|
12
|
+
from .bot import (
|
|
13
|
+
SoulFireActionError,
|
|
14
|
+
SoulFireBot,
|
|
15
|
+
SoulFireBotControlLease,
|
|
16
|
+
SoulFireBotControlLeaseSync,
|
|
17
|
+
SoulFireBotSync,
|
|
18
|
+
)
|
|
19
|
+
from .client import SoulFire, SoulFireSync
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AttackNearest",
|
|
23
|
+
"AutoEat",
|
|
24
|
+
"BotBehavior",
|
|
25
|
+
"Build",
|
|
26
|
+
"BuildPlacement",
|
|
27
|
+
"CollectBlocks",
|
|
28
|
+
"FollowEntity",
|
|
29
|
+
"LocalSoulFireServer",
|
|
30
|
+
"SoulFire",
|
|
31
|
+
"SoulFireActionError",
|
|
32
|
+
"SoulFireBot",
|
|
33
|
+
"SoulFireBotControlLease",
|
|
34
|
+
"SoulFireBotControlLeaseSync",
|
|
35
|
+
"SoulFireBotSync",
|
|
36
|
+
"SoulFireSync",
|
|
37
|
+
"run_behaviors",
|
|
38
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import TypeAlias
|
|
5
|
+
|
|
6
|
+
from connectrpc.request import RequestContext
|
|
7
|
+
|
|
8
|
+
TokenProvider: TypeAlias = str | Callable[[], str | None]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def resolve_token(token: TokenProvider | None) -> str | None:
|
|
12
|
+
if callable(token):
|
|
13
|
+
return token()
|
|
14
|
+
return token
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BearerAuthInterceptor:
|
|
18
|
+
def __init__(self, token_provider: Callable[[], TokenProvider | None]) -> None:
|
|
19
|
+
self._token_provider = token_provider
|
|
20
|
+
|
|
21
|
+
async def on_start(self, ctx: RequestContext) -> None:
|
|
22
|
+
token = resolve_token(self._token_provider())
|
|
23
|
+
if token:
|
|
24
|
+
ctx.request_headers["Authorization"] = f"Bearer {token}"
|
|
25
|
+
|
|
26
|
+
async def on_end(
|
|
27
|
+
self,
|
|
28
|
+
_token: None,
|
|
29
|
+
_ctx: RequestContext,
|
|
30
|
+
_error: Exception | None,
|
|
31
|
+
) -> None:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BearerAuthInterceptorSync:
|
|
36
|
+
def __init__(self, token_provider: Callable[[], TokenProvider | None]) -> None:
|
|
37
|
+
self._token_provider = token_provider
|
|
38
|
+
|
|
39
|
+
def on_start_sync(self, ctx: RequestContext) -> None:
|
|
40
|
+
token = resolve_token(self._token_provider())
|
|
41
|
+
if token:
|
|
42
|
+
ctx.request_headers["Authorization"] = f"Bearer {token}"
|
|
43
|
+
|
|
44
|
+
def on_end_sync(
|
|
45
|
+
self,
|
|
46
|
+
_token: None,
|
|
47
|
+
_ctx: RequestContext,
|
|
48
|
+
_error: Exception | None,
|
|
49
|
+
) -> None:
|
|
50
|
+
return None
|