Metadata-Version: 2.4
Name: netio-game
Version: 0.1.1
Summary: Lightweight Python library for multiplayer game networking over TCP
Author: Talim Aushakhman
License-Expression: MIT
Keywords: networking,multiplayer,games,serialization,tcp,rpc
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Games/Entertainment
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Networking
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: build>=1.0.0; extra == "dev"
Requires-Dist: twine>=5.0.0; extra == "dev"
Dynamic: license-file

# netio-game

A lightweight Python library for multiplayer game networking. It provides a custom binary wire format, decorator-based RPC routing, and server-authoritative object synchronization over TCP.

## Features

- **Binary serialization** — compact encoding for primitives, tuples, lists, and typed objects
- **RPC routing** — `@request`, `@response`, and `@event` handlers with optional datatype parsing
- **Object sync** — server creates, updates, and deletes `Serializable` objects on connected clients
- **Per-player visibility** — override `validate()` on synced objects to control who sees what

## Requirements

- Python 3.10+

## Installation

**From PyPI:**

```bash
pip install netio-game
```

**From source** (development):

```bash
git clone <your-repo-url>
cd netio-game
pip install -e .
```

```python
from netio_game import Client, Host, ServerRouter, ClientRouter
```

## Quick start

Install the package, then run the examples from the repo root:

```bash
pip install -e .
```

**Terminal 1 — server:**

```bash
python examples/server.py
```

**Terminal 2 — client:**

```bash
python examples/client.py
```

Expected client output:

```
[client] connected, waiting for handshake...
[client] joined as PlayerData <('127.0.0.1', ...)>
[client] response: pong to 127.0.0.1
[client] done
```

## Core concepts

### Host and Client

| Class | Role |
|-------|------|
| `Host` | Listens for connections, runs the game manager, syncs objects |
| `Client` | Connects to a host, receives synced objects, sends messages |

Both use background threads for I/O. Call `.start()` after construction.

### Message types

| `MessageType` | Direction | Purpose |
|---------------|-----------|---------|
| `CONNECT` | Client → Server | Initial handshake |
| `REQUEST` | Client → Server | Ask the server to run a route handler |
| `RESPONSE` | Server → Client | Reply to a request |
| `EVENT` | Either way | Fire-and-forget notification |
| `CREATE` | Server → Client | Spawn a synced object |
| `SYNCHRONIZE` | Server → Client | Push field updates |
| `DELETE` | Server → Client | Remove a synced object |
| `ERROR` | Either way | Report an error |

### Routers

Handlers are registered with decorators on `ServerRouter` (server) or `ClientRouter` (client):

```python
from netio_game import ServerRouter, ClientRouter
from netio_game.serialization.routing import MessageType

server_router = ServerRouter()

@server_router.request("ping")
def handle_ping(player, _data):
    return (f"pong to {player.address[0]}",)

@server_router.event("chat", datatype=tuple[str])
def handle_chat(player, data):
    print(player, data[0])
```

```python
client_router = ClientRouter()

@client_router.response("ping", datatype=tuple[str])
def on_pong(data):
    print(data[0])

# after client.start() and handshake:
client.send_message(MessageType.REQUEST, "ping", ())
client.send_message(MessageType.EVENT, "chat", ("hello",))
```

**Important:** If a handler receives payload data, pass `datatype=` to the decorator (e.g. `tuple[str]`). Without it, the parsed argument is `None`.

Server handlers receive `(player_data, data)`. Client handlers receive `(data)` only.

### Serializable objects

Synced game state inherits from `Serializable`. Fields must use `Annotated` with `SerializeField()`:

```python
from typing import Annotated
from netio_game import Serializable, SerializeField

class Bullet(Serializable):
    x: Annotated[float, SerializeField()]
    y: Annotated[float, SerializeField()]

    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y
```

On the server, register objects with the sync system:

```python
bullet = Bullet(10.0, 20.0)
host.create_object(bullet)   # sends CREATE to clients
bullet.x = 15.0
host.synchronize()           # pushes deltas
host.delete_object(bullet)   # sends DELETE
```

Override lifecycle hooks on the client:

```python
def client_on_create(self): ...
def client_on_update(self): ...
def client_on_destroy(self): ...
```

Override `validate(player_data) -> bool` to hide objects from specific players.

### Player data

Subclass `PlayerData` for per-connection state. The built-in `PlayerData` does **not** serialize `address` — define your own type if the client needs it (see `examples/shared.py`):

```python
from typing import Annotated
from netio_game import PlayerData, SerializeField

class GamePlayer(PlayerData):
    address: Annotated[tuple[str, int], SerializeField()]
```

Pass `player_data_type=` / `pdata_type=` to `Host` and `Client` respectively.

### Linking client and server classes (`sync_key`)

When client and server define **different** Python classes for the same networked object, assign them the same `sync_key` so they share a wire class ID:

```python
from netio_game import PlayerData, SerializeField, sync_key
from typing import Annotated

@sync_key("game_player")
class ServerGamePlayer(PlayerData):
    address: Annotated[tuple[str, int], SerializeField()]

@sync_key("game_player")
class ClientGamePlayer(PlayerData):
    address: Annotated[tuple[str, int], SerializeField()]
```

Or pass it as a class keyword:

```python
class GamePlayer(PlayerData, sync_key="game_player"):
    ...
```

The server serializes `ServerGamePlayer`; the client deserializes into `ClientGamePlayer` (and vice versa for server-bound types).

## Publishing to PyPI

```bash
pip install build twine
python -m build
twine check dist/*
twine upload dist/*
```

## Project layout

```
netio-game/
├── client.py              # Client connection and message loop
├── server.py              # Host, GameManager, connection handling
├── router.py              # Decorator-based route handlers
├── datatypes.py           # PlayerData, ConnectionData
├── serialization/
│   ├── serializer.py      # Serializable base class
│   ├── routing.py         # MessageType, Reader, Writer
│   └── io/io.py           # Binary encode/decode
├── util/                  # GenericType, LazyRef helpers
└── examples/              # Runnable server/client demo
```

## Logging

The library writes debug output to `network.log` and the server also appends to `log.txt` during development. Both are listed in `.gitignore`.

## License

MIT — see [LICENSE](LICENSE).
