Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions docs/lowlevel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Low-level reference

The high-level API (`SFSClient` / `SFSServer` / `SFSModel`) covers almost
every use case. When you need to talk to the wire directly — interoperate
with a custom SFS dialect, ship raw bytes, or write your own
acceptor — this page documents the building blocks underneath.

- [`sfs2x.core`](#sfs2xcore) — typed primitives and `SFSObject`/`SFSArray`.
- [`sfs2x.protocol`](#sfs2xprotocol) — `Message`, the wire codec, compression and encryption.
- [`sfs2x.transport`](#sfs2xtransport) — `Transport`/`Acceptor`, TCP and WebSocket implementations.

---

## `sfs2x.core`

The data model. Every value that travels on the wire is a `Field`
subclass: a typed wrapper around a raw Python value that knows how to
serialise/deserialise itself.

### Primitives & arrays

```python
from sfs2x.core import (
Bool, Byte, Short, Int, Long, Float, Double, UtfString, Text,
BoolArray, ByteArray, ShortArray, IntArray, LongArray,
FloatArray, DoubleArray, UtfStringArray,
)

i = Int(42) # i.value == 42, i.type_code is TypeCode.INT
arr = IntArray([1, 2]) # arr.value == [1, 2]
```

### `SFSObject` / `SFSArray`

```python
from sfs2x.core import SFSObject, SFSArray, Int, UtfString

obj = SFSObject()
obj.put_int("score", 1200) \
.put_utf_string("name", "Zewsic") \
.put_double_array("history", [3.14, -4.5])

# Declarative form — raw dicts/lists are auto-wrapped:
obj = SFSObject({
"name": UtfString("Zewsic"),
"score": Int(2022),
"items": [UtfString("Sword"), UtfString("Shield")],
"object": {"some": UtfString("thing")},
})

# Access (auto-unwraps Field → raw Python value):
obj["score"] # 1200
obj.get("missing", 0) # 0
obj.value["score"] # Int(1200) — the actual Field instance
```

### Round-tripping bytes

```python
from sfs2x.core import decode, SFSObject, Int

obj = SFSObject({"example": Int(42)})
raw = obj.to_bytes()
restored: SFSObject = decode(raw)
print(restored.get("example")) # 42
```

---

## `sfs2x.protocol`

### `Message`

```python
from sfs2x.protocol import Message, ControllerID, SysAction
from sfs2x.core import SFSObject, UtfString

msg = Message(
controller=ControllerID.SYSTEM,
action=SysAction.PUBLIC_MESSAGE,
payload=SFSObject({"text": UtfString("Hello!")}),
)

# Extension calls have a convenience factory:
ext = Message.extension(
"myCmd",
SFSObject({"x": UtfString("y")}),
request_id=42,
)
```

### Encode / decode

```python
from sfs2x.protocol import encode, decode

packet = encode(
msg,
compress_threshold=512, # zlib-compress payloads > 512 bytes
encryption_key=b"my_secret_16byte", # AES-128-CBC
)
back = decode(packet, encryption_key=b"my_secret_16byte")
```

### Flag bits

| Bit | Meaning |
| ------------ | -------------------------------- |
| `BINARY` | Always set on SFS2X binary packets |
| `COMPRESSED` | `zlib` compression applied |
| `ENCRYPTED` | AES-128-CBC encryption applied |
| `BIG_SIZE` | Payload length > 65535 bytes |
| `BLUEBOX` | Reserved (BlueBox transport) |

### `AESCipher`

`AES-128-CBC` with random IV per encryption, PKCS#7 padding. Requires
`pycryptodome`. Key must be exactly 16 bytes.

```python
from sfs2x.protocol import AESCipher
c = AESCipher(b"my_secret_16byte")
encrypted = c.encrypt(b"hello")
plain = c.decrypt(encrypted)
```

### `ControllerID` / `SysAction`

See [protocol.md](protocol.md) for the full enum tables and SFS2X
payload-key conventions (`c`/`a`/`p`/`r`/`zn`/`un`/…).

---

## `sfs2x.transport`

### `Transport` lifecycle

```python
from sfs2x.transport import client_from_url

async with client_from_url("tcp://localhost:9933") as t:
await t.send(msg)
reply = await t.recv()
async for m in t.listen():
...
```

### `Acceptor` lifecycle

```python
from sfs2x.transport import server_from_url

async with server_from_url("tcp://0.0.0.0:9933") as acc:
async for client in acc:
asyncio.create_task(handle(client))
```

### URL → transport mapping

| Scheme | Client | Server | Default port | Default path |
| ------- | ------------------ | -------------------- | ------------ | ---------------------- |
| `tcp` | `TCPTransport` | `TCPAcceptor` | `9933` | — |
| `ws` | `WSTransport` | `WSAcceptor` | `8080` | `/BlueBox/websocket` |
| `wss` | `WSTransport(TLS)` | `WSAcceptor(TLS)` | `443` | `/BlueBox/websocket` |

Pass `ssl=ssl.SSLContext(...)` to the factory for `wss://`.

### Concrete classes

| Symbol | Purpose |
| --------------------- | --------------------------------------------------------- |
| `Transport` | Abstract base for client-side connections. |
| `Acceptor` | Abstract base for server-side acceptors. |
| `TransportClosedError`| Raised when `send`/`recv` is attempted on a closed transport. |
| `TCPTransport` | TCP client (`asyncio.open_connection`). |
| `TCPAcceptor` | TCP server (`asyncio.start_server`). |
| `WSTransport` | WebSocket client (`ws://` / `wss://`). |
| `WSAcceptor` | WebSocket server (default path `/BlueBox/websocket`). |
| `client_from_url` | URL → `Transport`. |
| `server_from_url` | URL → `Acceptor`. |
98 changes: 98 additions & 0 deletions docs/protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# SFS2X protocol reference

Controller IDs, system action codes and the SFSObject-key conventions
used by the wire format. These are mainly useful when you need to call
`Message(...)` directly or define your own custom `SystemRequest` /
`SystemResponse` models.

## `ControllerID`

| Value | Name | Purpose |
|-------|----------------|------------------------------------------|
| 0 | `SYSTEM` | Handshake, login, rooms, messages, users |
| 1 | `EXTENSION` | Custom extension commands |
| 2 | `SECURITY` | UDP / encryption negotiation |
| 3 | `BUDDY` | Buddy list management |
| 4 | `ROOM_MANAGER` | Room creation / destruction |
| 5 | `ADMIN` | Admin tool & console commands |

## `SysAction`

| Code | Name |
|------|------------------------------|
| 0 | `HANDSHAKE` |
| 1 | `LOGIN` |
| 2 | `LOGOUT` |
| 3 | `GET_ROOM_LIST` |
| 4 | `JOIN_ROOM` |
| 5 | `AUTO_JOIN` |
| 6 | `CREATE_ROOM` |
| 7 | `GENERIC_MESSAGE` |
| 8 | `CHANGE_ROOM_NAME` |
| 9 | `CHANGE_ROOM_PASSWORD` |
| 10 | `OBJECT_MESSAGE` |
| 11 | `SET_ROOM_VARIABLES` |
| 12 | `SET_USER_VARIABLES` |
| 13 | `CALL_EXTENSION` |
| 14 | `LEAVE_ROOM` |
| 15 | `SUBSCRIBE_ROOM_GROUP` |
| 16 | `UNSUBSCRIBE_ROOM_GROUP` |
| 17 | `SPECTATOR_TO_PLAYER` |
| 18 | `PLAYER_TO_SPECTATOR` |
| 19 | `CHANGE_ROOM_CAPACITY` |
| 20 | `PUBLIC_MESSAGE` |
| 21 | `PRIVATE_MESSAGE` |
| 22 | `MODERATOR_MESSAGE` |
| 23 | `ADMIN_MESSAGE` |
| 24 | `KICK_USER` |
| 25 | `BAN_USER` |
| 26 | `MANUAL_DISCONNECTION` |
| 27 | `FIND_ROOMS` |
| 28 | `FIND_USERS` |
| 29 | `PING_PONG` |
| 30 | `SET_USER_POSITION` |
| 31 | `QUICK_JOIN_OR_CREATE_ROOM` |
| 200+ | Buddy actions |
| 300+ | Game / invite actions |
| 500+ | Cluster actions |
| 1000+| Server-pushed event actions |

## Wire envelope

Every `Message` is encoded as an `SFSObject` with these top-level keys:

| Key | Type | Meaning |
|-----|-----------|--------------------------------------------|
| `c` | `Byte` | Controller ID |
| `a` | `Short` | Action code (`SysAction` for `SYSTEM`) |
| `p` | `SFSObject` | Action-specific parameters |

## Extension envelope

Extension messages set `controller = EXTENSION` and `action = 12`. The
payload (`p` above) is itself an `SFSObject` with these keys:

| Key | Type | Meaning |
|-----|-------------|-------------------------------------------------------------|
| `c` | `UtfString` | Extension command name (`"gs_pussy"`, `"join"`, …) |
| `r` | `Int` | Request id — echo it back in your reply for correlation. `-1` = no correlation. |
| `p` | `SFSObject` | Command-specific parameters |

## Common payload keys (SFS2X conventions)

| Key | Usual meaning | Example actions |
|-------|---------------------------|------------------------|
| `zn` | Zone name | `LOGIN`, `LOGOUT` |
| `un` | Username | `LOGIN` |
| `pw` | Password | `LOGIN` |
| `id` | User id | `LOGIN` response |
| `pi` | Privilege id | `LOGIN` response |
| `ec` | Error code | error responses |
| `em` | Error message | error responses |
| `epr` | Error params | error responses |
| `api` | SFS API version | `HANDSHAKE` |
| `cl` | Client identifier | `HANDSHAKE` |
| `bin` | Binary mode flag | `HANDSHAKE` |
| `tk` | Session token | `HANDSHAKE` response |
| `ct` | Compression threshold | `HANDSHAKE` response |
| `ms` | Max message size | `HANDSHAKE` response |
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ build-backend = "setuptools.build_meta"

[project]
name = "sfs2x"
version = "0.1.0.1"
description = "Python-Based implementation of SmartFoxServer2X (SFS2X) Protocol."
version = "0.2.0"
description = "High-level SmartFoxServer 2X client & server for Python."
authors = [{name="Zewsic", email="me@zewsic.pro"}]
readme = "readme.md"
license = {text = "MIT"}
Expand Down
Loading
Loading