aiosatisfactory 0.1.0__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.
- aiosatisfactory-0.1.0/LICENSE +21 -0
- aiosatisfactory-0.1.0/PKG-INFO +57 -0
- aiosatisfactory-0.1.0/README.md +42 -0
- aiosatisfactory-0.1.0/aiosatisfactory/__init__.py +21 -0
- aiosatisfactory-0.1.0/aiosatisfactory/https/__init__.py +36 -0
- aiosatisfactory-0.1.0/aiosatisfactory/https/api.py +375 -0
- aiosatisfactory-0.1.0/aiosatisfactory/https/models.py +118 -0
- aiosatisfactory-0.1.0/aiosatisfactory/lightweight/__init__.py +38 -0
- aiosatisfactory-0.1.0/aiosatisfactory/lightweight/const.py +75 -0
- aiosatisfactory-0.1.0/aiosatisfactory/lightweight/request.py +33 -0
- aiosatisfactory-0.1.0/aiosatisfactory/lightweight/response.py +80 -0
- aiosatisfactory-0.1.0/aiosatisfactory/lightweight/udp.py +21 -0
- aiosatisfactory-0.1.0/aiosatisfactory.egg-info/PKG-INFO +57 -0
- aiosatisfactory-0.1.0/aiosatisfactory.egg-info/SOURCES.txt +17 -0
- aiosatisfactory-0.1.0/aiosatisfactory.egg-info/dependency_links.txt +1 -0
- aiosatisfactory-0.1.0/aiosatisfactory.egg-info/requires.txt +1 -0
- aiosatisfactory-0.1.0/aiosatisfactory.egg-info/top_level.txt +1 -0
- aiosatisfactory-0.1.0/pyproject.toml +25 -0
- aiosatisfactory-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Rikys
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aiosatisfactory
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async client for Satisfactory dedicated server APIs
|
|
5
|
+
Author: Rikys
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Rikys/aiosatisfactory
|
|
8
|
+
Project-URL: Documentation, https://github.com/Rikys/aiosatisfactory#readme
|
|
9
|
+
Project-URL: Issues, https://github.com/Rikys/aiosatisfactory/issues
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# aiosatisfactory
|
|
17
|
+
|
|
18
|
+
This is an async Python library for Satisfactory dedicated server's APIs.
|
|
19
|
+
|
|
20
|
+
This work is based off the official documentation that is provided with the game files or is also available on the official [wiki](https://satisfactory.wiki.gg)
|
|
21
|
+
|
|
22
|
+
## Lightweight API [(docs)](https://satisfactory.wiki.gg/wiki/Dedicated_servers/Lightweight_Query_API)
|
|
23
|
+
This API should be used to poll the server state before making most of the https requests \
|
|
24
|
+
No errors are raised but you must check if the query was succesful
|
|
25
|
+
|
|
26
|
+
### Usage:
|
|
27
|
+
```python
|
|
28
|
+
from aiosatisfactory import SatisfactoryServer
|
|
29
|
+
import time
|
|
30
|
+
|
|
31
|
+
client = SatisfactoryServer("server.ip")
|
|
32
|
+
query = await client.lightweight.query(time.time_ns()) #We use time to generate a Cookie
|
|
33
|
+
if query is not None:
|
|
34
|
+
print(query.response)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Https API [(docs)](https://satisfactory.wiki.gg/wiki/Dedicated_servers/HTTPS_API)
|
|
38
|
+
This API requires the *session* parameter to be set in the *SatisfactoryServer* constructor \
|
|
39
|
+
It does raise an *ErrorResponse* exeption if the function you try to execute fails
|
|
40
|
+
|
|
41
|
+
### Usage:
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
from aiosatisfactory import SatisfactoryServer
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
|
|
48
|
+
with aiohttp.ClientSession() as session:
|
|
49
|
+
client = SatisfactoryServer("server.ip", session=session)
|
|
50
|
+
try:
|
|
51
|
+
response = await client.https.api.health_check()
|
|
52
|
+
print(response.health)
|
|
53
|
+
except ErrorResponse as e:
|
|
54
|
+
print(f"Error: {e.error_code, e.error_message, e.error_details}")
|
|
55
|
+
|
|
56
|
+
asyncio.run(main())
|
|
57
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# aiosatisfactory
|
|
2
|
+
|
|
3
|
+
This is an async Python library for Satisfactory dedicated server's APIs.
|
|
4
|
+
|
|
5
|
+
This work is based off the official documentation that is provided with the game files or is also available on the official [wiki](https://satisfactory.wiki.gg)
|
|
6
|
+
|
|
7
|
+
## Lightweight API [(docs)](https://satisfactory.wiki.gg/wiki/Dedicated_servers/Lightweight_Query_API)
|
|
8
|
+
This API should be used to poll the server state before making most of the https requests \
|
|
9
|
+
No errors are raised but you must check if the query was succesful
|
|
10
|
+
|
|
11
|
+
### Usage:
|
|
12
|
+
```python
|
|
13
|
+
from aiosatisfactory import SatisfactoryServer
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
client = SatisfactoryServer("server.ip")
|
|
17
|
+
query = await client.lightweight.query(time.time_ns()) #We use time to generate a Cookie
|
|
18
|
+
if query is not None:
|
|
19
|
+
print(query.response)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Https API [(docs)](https://satisfactory.wiki.gg/wiki/Dedicated_servers/HTTPS_API)
|
|
23
|
+
This API requires the *session* parameter to be set in the *SatisfactoryServer* constructor \
|
|
24
|
+
It does raise an *ErrorResponse* exeption if the function you try to execute fails
|
|
25
|
+
|
|
26
|
+
### Usage:
|
|
27
|
+
```python
|
|
28
|
+
import asyncio
|
|
29
|
+
from aiosatisfactory import SatisfactoryServer
|
|
30
|
+
|
|
31
|
+
async def main():
|
|
32
|
+
|
|
33
|
+
with aiohttp.ClientSession() as session:
|
|
34
|
+
client = SatisfactoryServer("server.ip", session=session)
|
|
35
|
+
try:
|
|
36
|
+
response = await client.https.api.health_check()
|
|
37
|
+
print(response.health)
|
|
38
|
+
except ErrorResponse as e:
|
|
39
|
+
print(f"Error: {e.error_code, e.error_message, e.error_details}")
|
|
40
|
+
|
|
41
|
+
asyncio.run(main())
|
|
42
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
from .lightweight import LightweightAPI
|
|
3
|
+
from .https import HttpsAPI
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SatisfactoryServer:
|
|
7
|
+
def __init__(self, host: str, port: int = 7777, self_signed_certificate: bool = True, session: aiohttp.ClientSession | None = None, api_token: str = ""):
|
|
8
|
+
self._lightweight = LightweightAPI(host, port)
|
|
9
|
+
if session is None:
|
|
10
|
+
self._https = None
|
|
11
|
+
else:
|
|
12
|
+
self._https = HttpsAPI(self_signed_certificate, host, port, session, api_token)
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def lightweight(self) -> LightweightAPI:
|
|
16
|
+
return self._lightweight
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def https(self) -> HttpsAPI | None:
|
|
20
|
+
return self._https
|
|
21
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import ssl
|
|
2
|
+
import aiohttp
|
|
3
|
+
from .api import ApiEndpoints
|
|
4
|
+
|
|
5
|
+
class HttpsAPI:
|
|
6
|
+
|
|
7
|
+
def __init__(self, self_signed_certificate: bool, host: str, port: int, session: aiohttp.ClientSession, api_token: str):
|
|
8
|
+
self._host = host
|
|
9
|
+
self._port = port
|
|
10
|
+
self._api_token = api_token
|
|
11
|
+
self._headers = {
|
|
12
|
+
'content-type': 'application/json',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
self._ssl_context = ssl.create_default_context()
|
|
16
|
+
if self_signed_certificate:
|
|
17
|
+
self._ssl_context.check_hostname = False
|
|
18
|
+
self._ssl_context.verify_mode = ssl.CERT_NONE
|
|
19
|
+
|
|
20
|
+
if api_token:
|
|
21
|
+
self._headers["Authorization"] = f"Bearer {self._api_token}"
|
|
22
|
+
|
|
23
|
+
self._url = f"https://{self._host}:{self._port}/api/v1"
|
|
24
|
+
|
|
25
|
+
self._api = ApiEndpoints(session, self._url, self._headers, self._ssl_context)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def api_token(self, new_token: str = "") -> str:
|
|
29
|
+
if new_token:
|
|
30
|
+
self._api_token = new_token
|
|
31
|
+
self._headers["Authorization"] = f"Bearer {self._api_token}"
|
|
32
|
+
return self._api_token
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def api(self) -> ApiEndpoints:
|
|
36
|
+
return self._api
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import json, aiohttp
|
|
2
|
+
import ssl
|
|
3
|
+
from .models import (
|
|
4
|
+
BaseResponse,
|
|
5
|
+
HealthCheckResponse,
|
|
6
|
+
PasswordlessLoginResponse,
|
|
7
|
+
PasswordLoginResponse,
|
|
8
|
+
QueryServerStateResponse,
|
|
9
|
+
GetServerOptionsResponse,
|
|
10
|
+
GetAdvancedGameSettingsResponse,
|
|
11
|
+
ClaimServerResponse,
|
|
12
|
+
RunCommandResponse,
|
|
13
|
+
ServerNewGameData,
|
|
14
|
+
EnumerateSessionsResponse,
|
|
15
|
+
DownloadSaveGameResponse,
|
|
16
|
+
ErrorResponse
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
class ApiEndpoints():
|
|
20
|
+
def __init__(self, session: aiohttp.ClientSession, url: str, headers: dict[str, str], ssl_context: ssl.SSLContext):
|
|
21
|
+
self._session = session
|
|
22
|
+
self._url = url
|
|
23
|
+
self._headers = headers
|
|
24
|
+
self._ssl_context = ssl_context
|
|
25
|
+
|
|
26
|
+
async def _post(self, function: str, data: dict[str, str] = {}) -> aiohttp.ClientResponse:
|
|
27
|
+
|
|
28
|
+
return await self._session.post(
|
|
29
|
+
self._url,
|
|
30
|
+
json={"function": function, "data": data},
|
|
31
|
+
headers=self._headers,
|
|
32
|
+
ssl=self._ssl_context
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def _raise_error(self, response: aiohttp.ClientResponse, data: dict[str, str]) -> None:
|
|
36
|
+
if response.status >= 400:
|
|
37
|
+
raise ErrorResponse(data["errorCode"], data.get("errorMessage"), data.get("errorData"))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def health_check(self, client_custom_data: str = "") -> HealthCheckResponse:
|
|
41
|
+
response = await self._post(
|
|
42
|
+
"HealthCheck",
|
|
43
|
+
{
|
|
44
|
+
"ClientCustomData": client_custom_data
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
data = await response.json()
|
|
48
|
+
self._raise_error(response, data)
|
|
49
|
+
data = data["data"]
|
|
50
|
+
return HealthCheckResponse(
|
|
51
|
+
response.status,
|
|
52
|
+
data["health"],
|
|
53
|
+
data["serverCustomData"]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def verify_authentication_token(self) -> BaseResponse:
|
|
59
|
+
response = await self._post("VerifyAuthenticationToken")
|
|
60
|
+
if response.status >= 400:
|
|
61
|
+
data = await response.json()
|
|
62
|
+
self._raise_error(response, data)
|
|
63
|
+
return BaseResponse(response.status)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def passwordless_login(self, minimum_privilege_level: str) -> PasswordlessLoginResponse:
|
|
67
|
+
response = await self._post(
|
|
68
|
+
"PasswordlessLogin",
|
|
69
|
+
{
|
|
70
|
+
"MinimumPrivilegeLevel": minimum_privilege_level
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
data = await response.json()
|
|
74
|
+
self._raise_error(response, data)
|
|
75
|
+
data = data["data"]
|
|
76
|
+
return PasswordlessLoginResponse(
|
|
77
|
+
response.status,
|
|
78
|
+
data["authenticationToken"]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def passwordlogin(self, minimum_privilege_level: str, password: str) -> PasswordLoginResponse:
|
|
84
|
+
response = await self._post(
|
|
85
|
+
"PasswordLogin",
|
|
86
|
+
{
|
|
87
|
+
"MinimumPrivilegeLevel": minimum_privilege_level,
|
|
88
|
+
"Password": password
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
data = await response.json()
|
|
92
|
+
self._raise_error(response, data)
|
|
93
|
+
data = data["data"]
|
|
94
|
+
return PasswordLoginResponse(
|
|
95
|
+
response.status,
|
|
96
|
+
data["authenticationToken"]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def query_server_state(self) -> QueryServerStateResponse:
|
|
102
|
+
response = await self._post("QueryServerState")
|
|
103
|
+
data = await response.json()
|
|
104
|
+
self._raise_error(response, data)
|
|
105
|
+
data = data["ServerGameState"]
|
|
106
|
+
return QueryServerStateResponse(
|
|
107
|
+
response.status,
|
|
108
|
+
data["ActiveSessionName"],
|
|
109
|
+
data["NumConnectedPlayers"],
|
|
110
|
+
data["PlayerLimit"],
|
|
111
|
+
data["TechTier"],
|
|
112
|
+
data["ActiveSchematic"],
|
|
113
|
+
data["GamePhase"],
|
|
114
|
+
data["IsGameRunning"],
|
|
115
|
+
data["TotalGameDuration"],
|
|
116
|
+
data["IsGamePaused"],
|
|
117
|
+
data["AverageTickRate"],
|
|
118
|
+
data["AutoLoadSessionName"]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def get_server_options(self) -> GetServerOptionsResponse:
|
|
124
|
+
response = await self._post("GetServerOptions")
|
|
125
|
+
data = await response.json()
|
|
126
|
+
self._raise_error(response, data)
|
|
127
|
+
return GetServerOptionsResponse(
|
|
128
|
+
response.status,
|
|
129
|
+
data["ServerOptions"],
|
|
130
|
+
data["PendingServerOptions"]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def get_advanced_game_settings(self) -> GetAdvancedGameSettingsResponse:
|
|
136
|
+
response = await self._post("GetAdvancedGameSettings")
|
|
137
|
+
data = await response.json()
|
|
138
|
+
self._raise_error(response, data)
|
|
139
|
+
return GetAdvancedGameSettingsResponse(
|
|
140
|
+
response.status,
|
|
141
|
+
data["CreativeModeEnabled"],
|
|
142
|
+
data["AdvancedGameSettings"]
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def apply_advanced_game_settings(self, applied_advanced_game_settings: dict[str, str]) -> BaseResponse:
|
|
148
|
+
response = await self._post(
|
|
149
|
+
"ApplyAdvancedGameSettings",
|
|
150
|
+
{
|
|
151
|
+
"AppliedAdvancedGameSettings": json.dumps(applied_advanced_game_settings)
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
data = await response.json()
|
|
155
|
+
self._raise_error(response, data)
|
|
156
|
+
return BaseResponse(response.status)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def claim_server(self, server_name: str, admin_password: str) -> ClaimServerResponse:
|
|
161
|
+
response = await self._post(
|
|
162
|
+
"ClaimServer",
|
|
163
|
+
{
|
|
164
|
+
"ServerName": server_name,
|
|
165
|
+
"AdminPassword": admin_password
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
data = await response.json()
|
|
169
|
+
self._raise_error(response, data)
|
|
170
|
+
return ClaimServerResponse(
|
|
171
|
+
response.status,
|
|
172
|
+
data["authenticationToken"]
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def rename_server(self, server_name: str) -> BaseResponse:
|
|
178
|
+
response = await self._post(
|
|
179
|
+
"RenameServer",
|
|
180
|
+
{
|
|
181
|
+
"ServerName": server_name
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
data = await response.json()
|
|
185
|
+
self._raise_error(response, data)
|
|
186
|
+
return BaseResponse(response.status)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def set_client_password(self, password: str) -> BaseResponse:
|
|
191
|
+
response = await self._post(
|
|
192
|
+
"SetClientPassword",
|
|
193
|
+
{
|
|
194
|
+
"Password": password
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
data = await response.json()
|
|
198
|
+
self._raise_error(response, data)
|
|
199
|
+
return BaseResponse(response.status)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def set_admin_password(self, password: str, authentication_token: str) -> BaseResponse:
|
|
204
|
+
response = await self._post(
|
|
205
|
+
"SetAdminPassword",
|
|
206
|
+
{
|
|
207
|
+
"Password": password,
|
|
208
|
+
"AuthenticationToken": authentication_token #??? - We have to generate a new token? - Maybe the documentation is wrong and this field is returned from the server?
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
data = await response.json()
|
|
212
|
+
self._raise_error(response, data)
|
|
213
|
+
return BaseResponse(response.status)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async def set_auto_load_session_name(self, session_name: str) -> BaseResponse:
|
|
218
|
+
response = await self._post(
|
|
219
|
+
"SetAutoLoadSessionName",
|
|
220
|
+
{
|
|
221
|
+
"SessionName": session_name
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
data = await response.json()
|
|
225
|
+
self._raise_error(response, data)
|
|
226
|
+
return BaseResponse(response.status)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def run_command(self, command: str) -> RunCommandResponse:
|
|
231
|
+
response = await self._post(
|
|
232
|
+
"RunCommand",
|
|
233
|
+
{
|
|
234
|
+
"Command": command
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
data = await response.json()
|
|
238
|
+
self._raise_error(response, data)
|
|
239
|
+
return RunCommandResponse(
|
|
240
|
+
response.status,
|
|
241
|
+
data["CommandResult"],
|
|
242
|
+
data["ReturnValue"]
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def shutdown(self) -> BaseResponse:
|
|
248
|
+
response = await self._post("Shutdown")
|
|
249
|
+
data = await response.json()
|
|
250
|
+
self._raise_error(response, data)
|
|
251
|
+
return BaseResponse(response.status)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def apply_server_options(self, updated_server_options: dict[str, str]) -> BaseResponse:
|
|
256
|
+
response = await self._post(
|
|
257
|
+
"ApplyServerOptions",
|
|
258
|
+
{
|
|
259
|
+
"UpdatedServerOptions": json.dumps(updated_server_options)
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
data = await response.json()
|
|
263
|
+
self._raise_error(response, data)
|
|
264
|
+
return BaseResponse(response.status)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
async def create_new_game(self, new_game_data: ServerNewGameData) -> BaseResponse:
|
|
269
|
+
response = await self._post(
|
|
270
|
+
"CreateNewGame",
|
|
271
|
+
{
|
|
272
|
+
"NewGameData": json.dumps(new_game_data)
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
data = await response.json()
|
|
276
|
+
self._raise_error(response, data)
|
|
277
|
+
return BaseResponse(response.status)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
async def save_game(self, save_name: str) -> BaseResponse:
|
|
282
|
+
response = await self._post(
|
|
283
|
+
"SaveGame",
|
|
284
|
+
{
|
|
285
|
+
"SaveName": save_name
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
data = await response.json()
|
|
289
|
+
self._raise_error(response, data)
|
|
290
|
+
return BaseResponse(response.status)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
async def delete_save_file(self, save_name: str) -> BaseResponse:
|
|
295
|
+
response = await self._post(
|
|
296
|
+
"DeleteSaveFile",
|
|
297
|
+
{
|
|
298
|
+
"SaveName": save_name
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
data = await response.json()
|
|
302
|
+
self._raise_error(response, data)
|
|
303
|
+
return BaseResponse(response.status)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
async def delete_save_session(self, session_name: str) -> BaseResponse:
|
|
308
|
+
response = await self._post(
|
|
309
|
+
"DeleteSaveSession",
|
|
310
|
+
{
|
|
311
|
+
"SessionName": session_name
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
data = await response.json()
|
|
315
|
+
self._raise_error(response, data)
|
|
316
|
+
return BaseResponse(response.status)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
async def enumerate_sessions(self) -> EnumerateSessionsResponse:
|
|
321
|
+
response = await self._post("EnumerateSessions")
|
|
322
|
+
data = await response.json()
|
|
323
|
+
self._raise_error(response, data)
|
|
324
|
+
return EnumerateSessionsResponse(
|
|
325
|
+
response.status,
|
|
326
|
+
data["Sessions"],
|
|
327
|
+
data["CurrentSessionIndex"]
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
async def load_game(self, save_name: str, enable_advanced_game_settings: bool) -> BaseResponse:
|
|
333
|
+
response = await self._post(
|
|
334
|
+
"LoadGame",
|
|
335
|
+
{
|
|
336
|
+
"SaveName": save_name,
|
|
337
|
+
"EnableAdvancedGameSettings": str(enable_advanced_game_settings)
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
data = await response.json()
|
|
341
|
+
self._raise_error(response, data)
|
|
342
|
+
return BaseResponse(response.status)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
#TODO
|
|
347
|
+
async def upload_save_game(self, save_name: str, load_save_game: bool, enable_advanced_game_settings: bool) -> BaseResponse:
|
|
348
|
+
response = await self._post(
|
|
349
|
+
"UploadSaveGame",
|
|
350
|
+
{
|
|
351
|
+
"SaveName": save_name,
|
|
352
|
+
"LoadSaveGame": str(load_save_game),
|
|
353
|
+
"EnableAdvancedGameSettings": str(enable_advanced_game_settings)
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
data = await response.json()
|
|
357
|
+
self._raise_error(response, data)
|
|
358
|
+
return BaseResponse(response.status)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
#TODO
|
|
363
|
+
async def download_save_game(self, save_name: str) -> DownloadSaveGameResponse:
|
|
364
|
+
response = await self._post(
|
|
365
|
+
"DownloadSaveGame",
|
|
366
|
+
{
|
|
367
|
+
"SaveName": save_name
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
data = await response.json()
|
|
371
|
+
self._raise_error(response, data)
|
|
372
|
+
return DownloadSaveGameResponse(
|
|
373
|
+
response.status,
|
|
374
|
+
data["SaveData"]
|
|
375
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(slots=True)
|
|
6
|
+
class BaseResponse:
|
|
7
|
+
status_code: int
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class HealthCheckResponse(BaseResponse):
|
|
13
|
+
health: str
|
|
14
|
+
server_custom_data: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class PasswordlessLoginResponse(BaseResponse):
|
|
20
|
+
authentication_token: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True)
|
|
25
|
+
class PasswordLoginResponse(BaseResponse):
|
|
26
|
+
authentication_token: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class QueryServerStateResponse(BaseResponse):
|
|
32
|
+
active_session_name: str
|
|
33
|
+
num_connected_players: int
|
|
34
|
+
player_limit: int
|
|
35
|
+
tech_tier: int
|
|
36
|
+
active_schematic: str
|
|
37
|
+
game_phase: str
|
|
38
|
+
is_game_running: bool
|
|
39
|
+
total_game_duration: int
|
|
40
|
+
is_game_paused: bool
|
|
41
|
+
average_tick_rate: float
|
|
42
|
+
auto_load_session_name: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(slots=True)
|
|
47
|
+
class GetServerOptionsResponse(BaseResponse):
|
|
48
|
+
server_options: dict[str, str]
|
|
49
|
+
pending_server_options: dict[str, str]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(slots=True)
|
|
54
|
+
class GetAdvancedGameSettingsResponse(BaseResponse):
|
|
55
|
+
creative_mode_enabled: bool
|
|
56
|
+
advanced_game_settings: dict[str, str]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(slots=True)
|
|
61
|
+
class ClaimServerResponse(BaseResponse):
|
|
62
|
+
authentication_token: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(slots=True)
|
|
67
|
+
class RunCommandResponse(BaseResponse):
|
|
68
|
+
command_result: str
|
|
69
|
+
return_value: bool
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(slots=True)
|
|
74
|
+
class ServerNewGameData:
|
|
75
|
+
session_name: str
|
|
76
|
+
map_name: str
|
|
77
|
+
starting_location: str
|
|
78
|
+
skip_onboarding: bool
|
|
79
|
+
advanced_game_settings: dict[str, str]
|
|
80
|
+
custom_options_only_for_modding: dict[str, str]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(slots=True)
|
|
85
|
+
class SaveHeader:
|
|
86
|
+
save_version: int
|
|
87
|
+
build_version: int
|
|
88
|
+
save_name: str
|
|
89
|
+
map_name: str
|
|
90
|
+
map_options: str
|
|
91
|
+
session_name: str
|
|
92
|
+
play_duration_seconds: int
|
|
93
|
+
save_date_time: str
|
|
94
|
+
is_modded_save: bool
|
|
95
|
+
is_edited_save: bool
|
|
96
|
+
is_creative_mode_enabled: bool
|
|
97
|
+
|
|
98
|
+
@dataclass(slots=True)
|
|
99
|
+
class SessionSaveStruct:
|
|
100
|
+
session_name: str
|
|
101
|
+
save_headers: list[SaveHeader]
|
|
102
|
+
|
|
103
|
+
@dataclass(slots=True)
|
|
104
|
+
class EnumerateSessionsResponse(BaseResponse):
|
|
105
|
+
sessions: list[SessionSaveStruct]
|
|
106
|
+
current_session_index: int
|
|
107
|
+
|
|
108
|
+
@dataclass(slots=True)
|
|
109
|
+
class DownloadSaveGameResponse(BaseResponse):
|
|
110
|
+
save_data: bytes
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(slots=True)
|
|
115
|
+
class ErrorResponse(Exception):
|
|
116
|
+
error_code: str
|
|
117
|
+
error_message: str | None = None
|
|
118
|
+
error_details: str | None = None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
from .udp import udp_query
|
|
3
|
+
from .response import ServerStateResponse
|
|
4
|
+
from .request import ServerStateRequest
|
|
5
|
+
|
|
6
|
+
from .const import (
|
|
7
|
+
POLL_FORMAT,
|
|
8
|
+
PROTOCOL_MAGIC,
|
|
9
|
+
MESSAGE_TYPE_POLL,
|
|
10
|
+
PROTOCOL_VERSION,
|
|
11
|
+
TERMINATOR_BYTE
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
class LightweightAPIResult():
|
|
15
|
+
def __init__(self, request: ServerStateRequest, response: ServerStateResponse):
|
|
16
|
+
self._request = request
|
|
17
|
+
self._response = response
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def request(self) -> ServerStateRequest:
|
|
21
|
+
return self._request
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def response(self) -> ServerStateResponse:
|
|
25
|
+
return self._response
|
|
26
|
+
|
|
27
|
+
class LightweightAPI():
|
|
28
|
+
def __init__(self, host: str, port: int):
|
|
29
|
+
self._host = host
|
|
30
|
+
self._port = port
|
|
31
|
+
|
|
32
|
+
async def query(self, Cookie: int) -> LightweightAPIResult | None:
|
|
33
|
+
|
|
34
|
+
request = struct.pack(POLL_FORMAT, PROTOCOL_MAGIC, MESSAGE_TYPE_POLL, PROTOCOL_VERSION, Cookie, TERMINATOR_BYTE)
|
|
35
|
+
response = await udp_query(self._host, self._port, request)
|
|
36
|
+
if response is None:
|
|
37
|
+
return None
|
|
38
|
+
return LightweightAPIResult(ServerStateRequest(request), ServerStateResponse(response))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Constants for Satisfactory LightWeight Query protocol."""
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
All the data in this protocol is in little-endian byte order as stated in the official documentation
|
|
5
|
+
The conversion between the documentation DataType and struct formats is as follows:
|
|
6
|
+
- uint8 -> B
|
|
7
|
+
- uint16 -> H
|
|
8
|
+
- uint32 -> L
|
|
9
|
+
- uint64 -> Q
|
|
10
|
+
- uint8[] -> s
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
"""General purpose fields and formats."""
|
|
14
|
+
|
|
15
|
+
PROTOCOL_MAGIC: int = 0xF6D5
|
|
16
|
+
PROTOCOL_MAGIC_FORMAT: str = "<H"
|
|
17
|
+
PROTOCOL_MAGIC_OFFSET: int = 0
|
|
18
|
+
|
|
19
|
+
MESSAGE_TYPE_FORMAT: str = "<B"
|
|
20
|
+
MESSAGE_TYPE_OFFSET: int = 2
|
|
21
|
+
|
|
22
|
+
PROTOCOL_VERSION: int = 1
|
|
23
|
+
PROTOCOL_VERSION_FORMAT: str = "<B"
|
|
24
|
+
PROTOCOL_VERSION_OFFSET: int = 3
|
|
25
|
+
|
|
26
|
+
# The payload starts after the 3-byte header, we use this constant to have a direct reference to the documentation offsets
|
|
27
|
+
PAYLOAD_START_BYTE: int = 4
|
|
28
|
+
|
|
29
|
+
# The payload always starts with a cookie to identify the request/response pair
|
|
30
|
+
COOKIE_FORMAT: str = "<Q"
|
|
31
|
+
COOKIE_OFFSET: int = PAYLOAD_START_BYTE + 0
|
|
32
|
+
|
|
33
|
+
# The last byte is always the terminator byte
|
|
34
|
+
TERMINATOR_BYTE: int = 0x01
|
|
35
|
+
TERMINATOR_BYTE_FORMAT: str = "<B"
|
|
36
|
+
TERMINATOR_BYTE_OFFSET: int = -1
|
|
37
|
+
|
|
38
|
+
"""Poll specific field formats and offsets."""
|
|
39
|
+
|
|
40
|
+
MESSAGE_TYPE_POLL: int = 0
|
|
41
|
+
|
|
42
|
+
POLL_FORMAT: str = "<HBBQB"
|
|
43
|
+
|
|
44
|
+
SUB_STATES_STRUCTURE_SIZE: int = 3
|
|
45
|
+
|
|
46
|
+
"""Response specific field formats and offsets."""
|
|
47
|
+
|
|
48
|
+
MESSAGE_TYPE_RESPONSE: int = 1
|
|
49
|
+
|
|
50
|
+
SERVER_STATE_FORMAT: str = "<B"
|
|
51
|
+
SERVER_STATE_OFFSET: int = PAYLOAD_START_BYTE + 8
|
|
52
|
+
|
|
53
|
+
SERVER_NET_CL_FORMAT: str = "<L"
|
|
54
|
+
SERVER_NET_CL_OFFSET: int = PAYLOAD_START_BYTE + 9
|
|
55
|
+
|
|
56
|
+
SERVER_FLAGS_FORMAT: str = "<Q"
|
|
57
|
+
SERVER_FLAGS_OFFSET: int = PAYLOAD_START_BYTE + 13
|
|
58
|
+
|
|
59
|
+
NUM_SUB_STATES_FORMAT: str = "<B"
|
|
60
|
+
NUM_SUB_STATES_OFFSET: int = PAYLOAD_START_BYTE + 21
|
|
61
|
+
|
|
62
|
+
# The sub-states section directly start with the first sub state ID
|
|
63
|
+
SUB_STATE_ID_FORMAT: str = "<B"
|
|
64
|
+
SUB_STATE_ID_OFFSET: int = PAYLOAD_START_BYTE + 22
|
|
65
|
+
|
|
66
|
+
# The sub-state version follows after it's ID, the documentation has a typo here saying there is an 8 bytes offset
|
|
67
|
+
SUB_STATE_VERSION_FORMAT: str = "<H"
|
|
68
|
+
BASE_SUB_STATE_VERSION_OFFSET: int = PAYLOAD_START_BYTE + 22 + 1
|
|
69
|
+
|
|
70
|
+
SERVER_NAME_LENGTH_FORMAT: str = "<H"
|
|
71
|
+
BASE_SERVER_NAME_LENGTH_OFFSET: int = PAYLOAD_START_BYTE + 22
|
|
72
|
+
|
|
73
|
+
# {} is to be replaced with the length of the server name + 1
|
|
74
|
+
SERVER_NAME_FORMAT: str = "<{}s"
|
|
75
|
+
BASE_SERVER_NAME_OFFSET: int = PAYLOAD_START_BYTE + 22 + 1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
|
|
3
|
+
from .const import (
|
|
4
|
+
PROTOCOL_MAGIC_FORMAT, PROTOCOL_MAGIC_OFFSET,
|
|
5
|
+
MESSAGE_TYPE_FORMAT, MESSAGE_TYPE_OFFSET,
|
|
6
|
+
PROTOCOL_VERSION_FORMAT, PROTOCOL_VERSION_OFFSET,
|
|
7
|
+
COOKIE_FORMAT, COOKIE_OFFSET,
|
|
8
|
+
TERMINATOR_BYTE_FORMAT, TERMINATOR_BYTE_OFFSET,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
class ServerStateRequest:
|
|
12
|
+
def __init__(self, raw_request: bytes):
|
|
13
|
+
self.raw_request = raw_request
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def ProtocolMagic(self) -> str:
|
|
17
|
+
return struct.unpack_from(PROTOCOL_MAGIC_FORMAT, self.raw_request, PROTOCOL_MAGIC_OFFSET)[0]
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def MessageType(self) -> int:
|
|
21
|
+
return struct.unpack_from(MESSAGE_TYPE_FORMAT, self.raw_request, MESSAGE_TYPE_OFFSET)[0]
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def ProtocolVersion(self) -> int:
|
|
25
|
+
return struct.unpack_from(PROTOCOL_VERSION_FORMAT, self.raw_request, PROTOCOL_VERSION_OFFSET)[0]
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def Cookie(self) -> int:
|
|
29
|
+
return struct.unpack_from(COOKIE_FORMAT, self.raw_request, COOKIE_OFFSET)[0]
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def TerminatorByte(self) -> int:
|
|
33
|
+
return struct.unpack_from(TERMINATOR_BYTE_FORMAT, self.raw_request, TERMINATOR_BYTE_OFFSET)[0]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
|
|
3
|
+
from .const import (
|
|
4
|
+
PROTOCOL_MAGIC_FORMAT, PROTOCOL_MAGIC_OFFSET,
|
|
5
|
+
MESSAGE_TYPE_FORMAT, MESSAGE_TYPE_OFFSET,
|
|
6
|
+
PROTOCOL_VERSION_FORMAT, PROTOCOL_VERSION_OFFSET,
|
|
7
|
+
COOKIE_FORMAT, COOKIE_OFFSET,
|
|
8
|
+
SERVER_STATE_FORMAT, SERVER_STATE_OFFSET,
|
|
9
|
+
SERVER_NET_CL_FORMAT, SERVER_NET_CL_OFFSET,
|
|
10
|
+
SERVER_FLAGS_FORMAT, SERVER_FLAGS_OFFSET,
|
|
11
|
+
NUM_SUB_STATES_FORMAT, NUM_SUB_STATES_OFFSET,
|
|
12
|
+
SUB_STATE_ID_FORMAT, SUB_STATE_ID_OFFSET,
|
|
13
|
+
SUB_STATE_VERSION_FORMAT, BASE_SUB_STATE_VERSION_OFFSET,
|
|
14
|
+
SERVER_NAME_LENGTH_FORMAT, BASE_SERVER_NAME_LENGTH_OFFSET,
|
|
15
|
+
SERVER_NAME_FORMAT, BASE_SERVER_NAME_OFFSET,
|
|
16
|
+
TERMINATOR_BYTE_FORMAT, TERMINATOR_BYTE_OFFSET,
|
|
17
|
+
SUB_STATES_STRUCTURE_SIZE
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
class ServerStateResponse:
|
|
21
|
+
|
|
22
|
+
def __init__(self, raw_response: bytes):
|
|
23
|
+
self.raw_response = raw_response
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def ProtocolMagic(self) -> str:
|
|
27
|
+
return struct.unpack_from(PROTOCOL_MAGIC_FORMAT, self.raw_response, PROTOCOL_MAGIC_OFFSET)[0]
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def MessageType(self) -> int:
|
|
31
|
+
return struct.unpack_from(MESSAGE_TYPE_FORMAT, self.raw_response, MESSAGE_TYPE_OFFSET)[0]
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def ProtocolVersion(self) -> int:
|
|
35
|
+
return struct.unpack_from(PROTOCOL_VERSION_FORMAT, self.raw_response, PROTOCOL_VERSION_OFFSET)[0]
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def Cookie(self) -> int:
|
|
39
|
+
return struct.unpack_from(COOKIE_FORMAT, self.raw_response, COOKIE_OFFSET)[0]
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def ServerState(self) -> int:
|
|
43
|
+
return struct.unpack_from(SERVER_STATE_FORMAT, self.raw_response, SERVER_STATE_OFFSET)[0]
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def ServerNetCL(self) -> int:
|
|
47
|
+
return struct.unpack_from(SERVER_NET_CL_FORMAT, self.raw_response, SERVER_NET_CL_OFFSET)[0]
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def ServerFlags(self) -> int:
|
|
51
|
+
return struct.unpack_from(SERVER_FLAGS_FORMAT, self.raw_response, SERVER_FLAGS_OFFSET)[0]
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def NumSubStates(self) -> int:
|
|
55
|
+
return struct.unpack_from(NUM_SUB_STATES_FORMAT, self.raw_response, NUM_SUB_STATES_OFFSET)[0]
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def SubStates(self) -> list[tuple[int, int]]:
|
|
59
|
+
sub_states: list[tuple[int, int]] = []
|
|
60
|
+
for i in range(self.NumSubStates):
|
|
61
|
+
sub_state = (
|
|
62
|
+
struct.unpack_from(SUB_STATE_ID_FORMAT, self.raw_response, SUB_STATE_ID_OFFSET + i * SUB_STATES_STRUCTURE_SIZE)[0],
|
|
63
|
+
struct.unpack_from(SUB_STATE_VERSION_FORMAT, self.raw_response, BASE_SUB_STATE_VERSION_OFFSET + i * SUB_STATES_STRUCTURE_SIZE)[0]
|
|
64
|
+
)
|
|
65
|
+
sub_states.append(sub_state)
|
|
66
|
+
return sub_states
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def ServerNameLength(self) -> int:
|
|
70
|
+
return struct.unpack_from(SERVER_NAME_LENGTH_FORMAT, self.raw_response, BASE_SERVER_NAME_LENGTH_OFFSET + self.NumSubStates * SUB_STATES_STRUCTURE_SIZE)[0]
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def ServerName(self) -> str:
|
|
74
|
+
calculated_server_name_format = SERVER_NAME_FORMAT.format(self.ServerNameLength + 1)
|
|
75
|
+
calculated_server_name_offset = BASE_SERVER_NAME_OFFSET + self.NumSubStates * SUB_STATES_STRUCTURE_SIZE
|
|
76
|
+
return struct.unpack_from(calculated_server_name_format, self.raw_response, calculated_server_name_offset)[0].decode('utf-8')
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def TerminatorByte(self) -> int:
|
|
80
|
+
return struct.unpack_from(TERMINATOR_BYTE_FORMAT, self.raw_response, TERMINATOR_BYTE_OFFSET)[0]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
async def udp_query(host: str, port: int, data: bytes, timeout: float = 2.0) -> bytes | None:
|
|
4
|
+
|
|
5
|
+
async_loop = asyncio.get_running_loop()
|
|
6
|
+
future = async_loop.create_future()
|
|
7
|
+
|
|
8
|
+
class UdpProtocol(asyncio.DatagramProtocol):
|
|
9
|
+
def datagram_received(self, data: bytes, addr: tuple[str, int]):
|
|
10
|
+
future.set_result(data)
|
|
11
|
+
|
|
12
|
+
transport, _ = await async_loop.create_datagram_endpoint(UdpProtocol, remote_addr=(host, port))
|
|
13
|
+
|
|
14
|
+
transport.sendto(data)
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
return await asyncio.wait_for(future, timeout)
|
|
18
|
+
except TimeoutError:
|
|
19
|
+
return None
|
|
20
|
+
finally:
|
|
21
|
+
transport.close()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aiosatisfactory
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async client for Satisfactory dedicated server APIs
|
|
5
|
+
Author: Rikys
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Rikys/aiosatisfactory
|
|
8
|
+
Project-URL: Documentation, https://github.com/Rikys/aiosatisfactory#readme
|
|
9
|
+
Project-URL: Issues, https://github.com/Rikys/aiosatisfactory/issues
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# aiosatisfactory
|
|
17
|
+
|
|
18
|
+
This is an async Python library for Satisfactory dedicated server's APIs.
|
|
19
|
+
|
|
20
|
+
This work is based off the official documentation that is provided with the game files or is also available on the official [wiki](https://satisfactory.wiki.gg)
|
|
21
|
+
|
|
22
|
+
## Lightweight API [(docs)](https://satisfactory.wiki.gg/wiki/Dedicated_servers/Lightweight_Query_API)
|
|
23
|
+
This API should be used to poll the server state before making most of the https requests \
|
|
24
|
+
No errors are raised but you must check if the query was succesful
|
|
25
|
+
|
|
26
|
+
### Usage:
|
|
27
|
+
```python
|
|
28
|
+
from aiosatisfactory import SatisfactoryServer
|
|
29
|
+
import time
|
|
30
|
+
|
|
31
|
+
client = SatisfactoryServer("server.ip")
|
|
32
|
+
query = await client.lightweight.query(time.time_ns()) #We use time to generate a Cookie
|
|
33
|
+
if query is not None:
|
|
34
|
+
print(query.response)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Https API [(docs)](https://satisfactory.wiki.gg/wiki/Dedicated_servers/HTTPS_API)
|
|
38
|
+
This API requires the *session* parameter to be set in the *SatisfactoryServer* constructor \
|
|
39
|
+
It does raise an *ErrorResponse* exeption if the function you try to execute fails
|
|
40
|
+
|
|
41
|
+
### Usage:
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
from aiosatisfactory import SatisfactoryServer
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
|
|
48
|
+
with aiohttp.ClientSession() as session:
|
|
49
|
+
client = SatisfactoryServer("server.ip", session=session)
|
|
50
|
+
try:
|
|
51
|
+
response = await client.https.api.health_check()
|
|
52
|
+
print(response.health)
|
|
53
|
+
except ErrorResponse as e:
|
|
54
|
+
print(f"Error: {e.error_code, e.error_message, e.error_details}")
|
|
55
|
+
|
|
56
|
+
asyncio.run(main())
|
|
57
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
aiosatisfactory/__init__.py
|
|
5
|
+
aiosatisfactory.egg-info/PKG-INFO
|
|
6
|
+
aiosatisfactory.egg-info/SOURCES.txt
|
|
7
|
+
aiosatisfactory.egg-info/dependency_links.txt
|
|
8
|
+
aiosatisfactory.egg-info/requires.txt
|
|
9
|
+
aiosatisfactory.egg-info/top_level.txt
|
|
10
|
+
aiosatisfactory/https/__init__.py
|
|
11
|
+
aiosatisfactory/https/api.py
|
|
12
|
+
aiosatisfactory/https/models.py
|
|
13
|
+
aiosatisfactory/lightweight/__init__.py
|
|
14
|
+
aiosatisfactory/lightweight/const.py
|
|
15
|
+
aiosatisfactory/lightweight/request.py
|
|
16
|
+
aiosatisfactory/lightweight/response.py
|
|
17
|
+
aiosatisfactory/lightweight/udp.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aiohttp>=3.9.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aiosatisfactory
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aiosatisfactory"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Async client for Satisfactory dedicated server APIs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Rikys" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"aiohttp>=3.9.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/Rikys/aiosatisfactory"
|
|
21
|
+
Documentation = "https://github.com/Rikys/aiosatisfactory#readme"
|
|
22
|
+
Issues = "https://github.com/Rikys/aiosatisfactory/issues"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
include = ["aiosatisfactory*"]
|