nodelink 0.2.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.
nodelink-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Marcus
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,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: nodelink
3
+ Version: 0.2.0
4
+ Summary: A high-level Python multiplayer library. Just works.
5
+ Author: Marcus
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yourusername/nodelink
8
+ Project-URL: Repository, https://github.com/yourusername/nodelink
9
+ Project-URL: Issues, https://github.com/yourusername/nodelink/issues
10
+ Keywords: multiplayer,networking,websocket,gamedev,realtime
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Games/Entertainment
23
+ Classifier: Topic :: Internet
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: websockets>=12.0
29
+ Dynamic: license-file
30
+
31
+ # nodelink
32
+
33
+ **A high-level Python multiplayer library. Just works.**
34
+
35
+ No presets. No opinions. You define the events, the data, and the behavior — nodelink handles the connections, state sync, and networking underneath.
36
+
37
+ ```
38
+ pip install nodelink
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Quickstart
44
+
45
+ **Server:**
46
+ ```python
47
+ from nodelink import Server
48
+
49
+ server = Server(port=5000)
50
+
51
+ @server.on("connect")
52
+ def on_connect(player):
53
+ server.broadcast("joined", {"id": player.id})
54
+
55
+ @server.on("move")
56
+ def on_move(player, data):
57
+ player.update_state({"x": data["x"], "y": data["y"]})
58
+
59
+ @server.on("disconnect")
60
+ def on_disconnect(player):
61
+ server.broadcast("left", {"id": player.id})
62
+
63
+ @server.on_error()
64
+ def on_error(error, player):
65
+ print(f"Error from {player.id}: {error}")
66
+
67
+ server.start()
68
+ ```
69
+
70
+ **Client:**
71
+ ```python
72
+ from nodelink import Client
73
+
74
+ client = Client("localhost", 5000)
75
+
76
+ @client.on("connect")
77
+ def on_connect():
78
+ client.send("move", {"x": 10, "y": 20})
79
+
80
+ @client.on("joined")
81
+ def on_joined(data):
82
+ print(f"Player {data['id']} joined!")
83
+
84
+ @client.on("__state_sync__")
85
+ def on_sync(data):
86
+ for player_id, state in data["players"].items():
87
+ print(f"{player_id}: {state}")
88
+
89
+ @client.on_error()
90
+ def on_error(error):
91
+ print(f"Error: {error}")
92
+
93
+ client.connect()
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Core concepts
99
+
100
+ ### Events
101
+ Everything is event-based. Send an event from the client, handle it on the server (and vice versa) with a simple decorator.
102
+
103
+ ```python
104
+ # Client sends:
105
+ client.send("chat", {"msg": "hello!"})
106
+
107
+ # Server handles:
108
+ @server.on("chat")
109
+ def on_chat(player, data):
110
+ server.broadcast("chat", {"from": player.id, "msg": data["msg"]})
111
+
112
+ # All clients receive:
113
+ @client.on("chat")
114
+ def on_chat(data):
115
+ print(f"[{data['from']}]: {data['msg']}")
116
+ ```
117
+
118
+ ### Player state sync
119
+ Each player has a `.state` dict. Update it on the server and nodelink automatically broadcasts only the changed fields to all clients at 20 ticks/sec.
120
+
121
+ ```python
122
+ @server.on("move")
123
+ def on_move(player, data):
124
+ player.update_state({"x": data["x"], "y": data["y"]})
125
+ # That's it — state is synced automatically
126
+ ```
127
+
128
+ Force an immediate sync if you can't wait for the next tick:
129
+ ```python
130
+ server.sync_state() # delta only
131
+ server.sync_state(full=True) # full state
132
+ ```
133
+
134
+ ### Sending to one player
135
+ ```python
136
+ @server.on("connect")
137
+ def on_connect(player):
138
+ # Send something only to this player
139
+ asyncio.ensure_future(player.send("welcome", {"id": player.id}))
140
+ ```
141
+
142
+ ### Error handling
143
+ Errors in event handlers are caught and routed to your error handler instead of crashing everything.
144
+
145
+ ```python
146
+ @server.on_error()
147
+ def on_error(error, player):
148
+ print(f"Error from {player.id}: {error}")
149
+
150
+ @client.on_error()
151
+ def on_error(error):
152
+ print(f"Client error: {error}")
153
+ ```
154
+
155
+ ### Send queue
156
+ Messages sent before the client is fully connected are automatically queued and delivered the moment the connection opens. No timing issues.
157
+
158
+ ```python
159
+ client.send("hello", {"msg": "this works even before connect() is called"})
160
+ client.connect() # queued message is flushed on connect
161
+ ```
162
+
163
+ ---
164
+
165
+ ## API Reference
166
+
167
+ ### `Server(host, port, tick_rate, delta_only)`
168
+
169
+ | Arg | Default | Description |
170
+ |-----|---------|-------------|
171
+ | `host` | `"0.0.0.0"` | Hostname to bind to |
172
+ | `port` | `5000` | Port to listen on |
173
+ | `tick_rate` | `20` | State syncs per second |
174
+ | `delta_only` | `True` | Only broadcast changed fields |
175
+
176
+ | Method | Description |
177
+ |--------|-------------|
178
+ | `server.on(event)` | Register an event handler |
179
+ | `server.on_error()` | Register an error handler |
180
+ | `server.broadcast(event, data)` | Send event to all players |
181
+ | `server.sync_state(full=False)` | Force immediate state sync |
182
+ | `server.get_players()` | List of all connected Players |
183
+ | `server.get_player(id)` | Look up a player by ID |
184
+ | `server.player_count` | Number of connected players |
185
+ | `server.start()` | Start the server (blocking) |
186
+
187
+ ### `Client(host, port)`
188
+
189
+ | Method | Description |
190
+ |--------|-------------|
191
+ | `client.on(event)` | Register an event handler |
192
+ | `client.on_error()` | Register an error handler |
193
+ | `client.send(event, data)` | Send event to server |
194
+ | `client.connect()` | Connect and start listening (blocking) |
195
+
196
+ ### `Player`
197
+
198
+ | Attribute/Method | Description |
199
+ |-----------------|-------------|
200
+ | `player.id` | Unique player ID |
201
+ | `player.state` | Dict of synced values |
202
+ | `player.set_state(key, value)` | Set one state value |
203
+ | `player.get_state(key, default)` | Get one state value |
204
+ | `player.update_state(dict)` | Merge dict into state |
205
+ | `player.clear_state()` | Wipe state entirely |
206
+ | `player.send(event, data)` | Send directly to this player |
207
+ | `player.connected_at` | Unix timestamp of connection time |
208
+ | `player.last_seen` | Unix timestamp of last message |
209
+
210
+ ---
211
+
212
+ ## Built-in events
213
+
214
+ | Event | Where | Handler signature |
215
+ |-------|-------|------------------|
216
+ | `"connect"` | server | `fn(player)` |
217
+ | `"disconnect"` | server | `fn(player)` |
218
+ | `"connect"` | client | `fn()` |
219
+ | `"disconnect"` | client | `fn()` |
220
+ | `"__state_sync__"` | client | `fn(data)` — `data["players"]` is a dict of `player_id -> state_delta` |
221
+
222
+ ---
223
+
224
+ ## Requirements
225
+
226
+ - Python 3.8+
227
+ - `websockets >= 12.0`
228
+
229
+ ---
230
+
231
+ ## License
232
+
233
+ MIT
@@ -0,0 +1,203 @@
1
+ # nodelink
2
+
3
+ **A high-level Python multiplayer library. Just works.**
4
+
5
+ No presets. No opinions. You define the events, the data, and the behavior — nodelink handles the connections, state sync, and networking underneath.
6
+
7
+ ```
8
+ pip install nodelink
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Quickstart
14
+
15
+ **Server:**
16
+ ```python
17
+ from nodelink import Server
18
+
19
+ server = Server(port=5000)
20
+
21
+ @server.on("connect")
22
+ def on_connect(player):
23
+ server.broadcast("joined", {"id": player.id})
24
+
25
+ @server.on("move")
26
+ def on_move(player, data):
27
+ player.update_state({"x": data["x"], "y": data["y"]})
28
+
29
+ @server.on("disconnect")
30
+ def on_disconnect(player):
31
+ server.broadcast("left", {"id": player.id})
32
+
33
+ @server.on_error()
34
+ def on_error(error, player):
35
+ print(f"Error from {player.id}: {error}")
36
+
37
+ server.start()
38
+ ```
39
+
40
+ **Client:**
41
+ ```python
42
+ from nodelink import Client
43
+
44
+ client = Client("localhost", 5000)
45
+
46
+ @client.on("connect")
47
+ def on_connect():
48
+ client.send("move", {"x": 10, "y": 20})
49
+
50
+ @client.on("joined")
51
+ def on_joined(data):
52
+ print(f"Player {data['id']} joined!")
53
+
54
+ @client.on("__state_sync__")
55
+ def on_sync(data):
56
+ for player_id, state in data["players"].items():
57
+ print(f"{player_id}: {state}")
58
+
59
+ @client.on_error()
60
+ def on_error(error):
61
+ print(f"Error: {error}")
62
+
63
+ client.connect()
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Core concepts
69
+
70
+ ### Events
71
+ Everything is event-based. Send an event from the client, handle it on the server (and vice versa) with a simple decorator.
72
+
73
+ ```python
74
+ # Client sends:
75
+ client.send("chat", {"msg": "hello!"})
76
+
77
+ # Server handles:
78
+ @server.on("chat")
79
+ def on_chat(player, data):
80
+ server.broadcast("chat", {"from": player.id, "msg": data["msg"]})
81
+
82
+ # All clients receive:
83
+ @client.on("chat")
84
+ def on_chat(data):
85
+ print(f"[{data['from']}]: {data['msg']}")
86
+ ```
87
+
88
+ ### Player state sync
89
+ Each player has a `.state` dict. Update it on the server and nodelink automatically broadcasts only the changed fields to all clients at 20 ticks/sec.
90
+
91
+ ```python
92
+ @server.on("move")
93
+ def on_move(player, data):
94
+ player.update_state({"x": data["x"], "y": data["y"]})
95
+ # That's it — state is synced automatically
96
+ ```
97
+
98
+ Force an immediate sync if you can't wait for the next tick:
99
+ ```python
100
+ server.sync_state() # delta only
101
+ server.sync_state(full=True) # full state
102
+ ```
103
+
104
+ ### Sending to one player
105
+ ```python
106
+ @server.on("connect")
107
+ def on_connect(player):
108
+ # Send something only to this player
109
+ asyncio.ensure_future(player.send("welcome", {"id": player.id}))
110
+ ```
111
+
112
+ ### Error handling
113
+ Errors in event handlers are caught and routed to your error handler instead of crashing everything.
114
+
115
+ ```python
116
+ @server.on_error()
117
+ def on_error(error, player):
118
+ print(f"Error from {player.id}: {error}")
119
+
120
+ @client.on_error()
121
+ def on_error(error):
122
+ print(f"Client error: {error}")
123
+ ```
124
+
125
+ ### Send queue
126
+ Messages sent before the client is fully connected are automatically queued and delivered the moment the connection opens. No timing issues.
127
+
128
+ ```python
129
+ client.send("hello", {"msg": "this works even before connect() is called"})
130
+ client.connect() # queued message is flushed on connect
131
+ ```
132
+
133
+ ---
134
+
135
+ ## API Reference
136
+
137
+ ### `Server(host, port, tick_rate, delta_only)`
138
+
139
+ | Arg | Default | Description |
140
+ |-----|---------|-------------|
141
+ | `host` | `"0.0.0.0"` | Hostname to bind to |
142
+ | `port` | `5000` | Port to listen on |
143
+ | `tick_rate` | `20` | State syncs per second |
144
+ | `delta_only` | `True` | Only broadcast changed fields |
145
+
146
+ | Method | Description |
147
+ |--------|-------------|
148
+ | `server.on(event)` | Register an event handler |
149
+ | `server.on_error()` | Register an error handler |
150
+ | `server.broadcast(event, data)` | Send event to all players |
151
+ | `server.sync_state(full=False)` | Force immediate state sync |
152
+ | `server.get_players()` | List of all connected Players |
153
+ | `server.get_player(id)` | Look up a player by ID |
154
+ | `server.player_count` | Number of connected players |
155
+ | `server.start()` | Start the server (blocking) |
156
+
157
+ ### `Client(host, port)`
158
+
159
+ | Method | Description |
160
+ |--------|-------------|
161
+ | `client.on(event)` | Register an event handler |
162
+ | `client.on_error()` | Register an error handler |
163
+ | `client.send(event, data)` | Send event to server |
164
+ | `client.connect()` | Connect and start listening (blocking) |
165
+
166
+ ### `Player`
167
+
168
+ | Attribute/Method | Description |
169
+ |-----------------|-------------|
170
+ | `player.id` | Unique player ID |
171
+ | `player.state` | Dict of synced values |
172
+ | `player.set_state(key, value)` | Set one state value |
173
+ | `player.get_state(key, default)` | Get one state value |
174
+ | `player.update_state(dict)` | Merge dict into state |
175
+ | `player.clear_state()` | Wipe state entirely |
176
+ | `player.send(event, data)` | Send directly to this player |
177
+ | `player.connected_at` | Unix timestamp of connection time |
178
+ | `player.last_seen` | Unix timestamp of last message |
179
+
180
+ ---
181
+
182
+ ## Built-in events
183
+
184
+ | Event | Where | Handler signature |
185
+ |-------|-------|------------------|
186
+ | `"connect"` | server | `fn(player)` |
187
+ | `"disconnect"` | server | `fn(player)` |
188
+ | `"connect"` | client | `fn()` |
189
+ | `"disconnect"` | client | `fn()` |
190
+ | `"__state_sync__"` | client | `fn(data)` — `data["players"]` is a dict of `player_id -> state_delta` |
191
+
192
+ ---
193
+
194
+ ## Requirements
195
+
196
+ - Python 3.8+
197
+ - `websockets >= 12.0`
198
+
199
+ ---
200
+
201
+ ## License
202
+
203
+ MIT
@@ -0,0 +1,38 @@
1
+ """
2
+ nodelink
3
+ ~~~~~~~~
4
+ A high-level Python multiplayer library. Just works.
5
+
6
+ Basic usage::
7
+
8
+ from nodelink import Server, Client
9
+
10
+ # Server
11
+ server = Server(port=5000)
12
+
13
+ @server.on("connect")
14
+ def on_connect(player):
15
+ server.broadcast("joined", {"id": player.id})
16
+
17
+ server.start()
18
+
19
+ # Client
20
+ client = Client("localhost", 5000)
21
+
22
+ @client.on("joined")
23
+ def on_joined(data):
24
+ print(f"Player {data['id']} joined!")
25
+
26
+ client.connect()
27
+
28
+ :copyright: 2024 Marcus
29
+ :license: MIT
30
+ """
31
+
32
+ from .server import Server
33
+ from .client import Client
34
+ from .player import Player
35
+
36
+ __version__ = "0.2.0"
37
+ __author__ = "Marcus"
38
+ __all__ = ["Server", "Client", "Player"]