feat(packages): ludic.net - a party over UDP: peers, reliable ordered delivery, the hello's frame and the host's pids, join codes, room codes through a matchmaker with STUN, punching and a relay, the MTU cap and the wire codec; messages in on an inbox, the session's end and a silent guest as facts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
411206e45a
commit
12172d7b75
20 changed files with 1762 additions and 0 deletions
93
packages/ludic.net/README.md
Normal file
93
packages/ludic.net/README.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# ludic.net
|
||||
|
||||
A small party over UDP: a host and up to eleven guests, packets with reliable ordered delivery,
|
||||
the frame of a handshake, join codes, and room codes through a matchmaker with STUN, hole punching
|
||||
and a relay for the routers punching cannot get through. Uses
|
||||
[`ludic.base`](../ludic.base/README.md) and nothing else.
|
||||
|
||||
```ludic
|
||||
import "ludic.net"
|
||||
```
|
||||
|
||||
What a message says, what each kind means, what a hello carries and why a host refuses one are
|
||||
the game's. The package gets bytes there once, in order, and says what happened to the session.
|
||||
|
||||
## The wire
|
||||
|
||||
A packet is a 20-byte header - `"ML"`, the protocol byte, `1`, the session token, the sender's pid,
|
||||
the highest reliable id taken in order, a message count - then messages: a kind, a flags byte, a
|
||||
reliable id when the message is reliable, a 16-bit length and the payload. A reliable message is
|
||||
kept until acknowledged and sent again every quarter second; a receiver takes them strictly in
|
||||
order and drops what arrives early (it comes again). An unreliable one half a second old is
|
||||
dropped. A message is at most `NET_MTU - 40` bytes and is cut, silently, past that; a string is at
|
||||
most 200 bytes (`nw_s`), a longer one is `nw_text` with its own cap. Numbers are little-endian and
|
||||
a float goes as its bits.
|
||||
|
||||
The host is pid 1 and hands out 2 upwards (`net_accept`). Kind 1 is `NET_HELLO`, the one kind a
|
||||
stranger may send a host; kind 0 is the heartbeat, never delivered. Until the game accepts a
|
||||
stranger, nothing else from it reaches the inbox.
|
||||
|
||||
## Config and ports
|
||||
|
||||
```ludic
|
||||
property NetConfig {
|
||||
proto (1), port (27415: an address typed without one)
|
||||
signal_url ("": room codes off), stun_host, stun_port (27400), stun_at ("ip:port" instead)
|
||||
no_stun, force_relay # tests on one machine
|
||||
relay_port0 (60000), seed # a relayed guest's port base; the STUN dice
|
||||
}
|
||||
port NetWorld { log: fn(string) -> void, trace: fn() -> bool } # unbound: silence
|
||||
port NetSocket { open, close, send, recv, from_ip, from_port, port, local_ip } # unbound: Udp
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `net_config(c)` | before the first session |
|
||||
| `net_host(port, token) -> bool` | open a socket and take guests (and a room, when the matchmaker is on); false: `net_why()` |
|
||||
| `net_hello_set()` | the message just written is the hello: sent again on every dial |
|
||||
| `net_join(ip, port) -> bool`, `net_room_join(code) -> bool` | dial an address, or ask the matchmaker for a room's host |
|
||||
| `net_accept(slot) -> pid`, `net_joined_as(token, pid)` | the host lets a stranger in; a guest was let in |
|
||||
| `net_dial(ip, port)`, `net_follow(ip, port)`, `net_stand_alone()`, `net_become_host(token)`, `net_room_host()` | a refused guest dialling again, a guest following its host elsewhere, a hand-off's heir |
|
||||
| `net_hello_wait(secs)` | a slow host gets this much longer (a map switch) |
|
||||
| `net_begin(dt)`, `net_update(dt)`, `net_end()` | a frame, with the game's own reading and writing between |
|
||||
| `net_keepalive()`, `net_flush_all()` | a load holding the frame; a machine about to let go |
|
||||
| `net_reset()`, `net_lose(why)`, `net_fresh()` | leave; out with a reason (`NET_DOWN`); a lost session read and done with |
|
||||
| `np_queue(slot, kind, rel)`, `np_queue_all(kind, rel, except)`, `np_flush(slot)`, `np_close(slot)`, `np_of_pid(pid)` | the written message to a peer or to all |
|
||||
| `net_close_when_sent(slot, secs)` | let a peer go once it has taken what was queued for it (`NET_CLOSED`) |
|
||||
| `nw_begin`, `nw_i`, `nw_b`, `nw_f`, `nw_s`, `nw_text`, `nw_len` | write a message |
|
||||
| `net_inbox() -> Queue<NetMessage>`, `net_read(m)`, `nr_i`, `nr_b`, `nr_f`, `nr_s`, `nr_text`, `nr_bad()` | what came in, in order, and reading it (a short message reads zeros and sets `nr_bad`) |
|
||||
| `net_copy_read()`, `net_written()` | pass the message read on; the message written, as if it came in |
|
||||
| `nb_put8`, `nb_put32`, `nb_get8`, `nb_get32`, `nb_copy` | bytes |
|
||||
| `net_code_of(ip, port)`, `net_join_code()`, `net_parse_code(text)`, `net_code_ip()`, `net_code_port()`, `net_is_room(text)` | ten letters for an address and port, and back; an `a.b.c.d[:port]` is taken too |
|
||||
| `net_state()`, `net_live()`, `net_active()`, `net_hosting()`, `net_joined()`, `net_joining()`, `net_my_pid()`, `net_token()`, `net_time()`, `net_why()`, `net_port()`, `net_join_ip()`, `net_join_port()`, `net_peer_on/pid/ip/port/heard(slot)`, `net_peer_count()`, `net_room_on()`, `net_room()`, `net_room_asking()`, `net_via_relay()`, `net_outside_ip/port()` | what the game asks |
|
||||
| `net_bytes_out()`, `net_kind_bytes(kind)`, `net_kind_out_clear()` | what a party costs the upload |
|
||||
| `net_facts() -> Queue<NetFact>` | `{ what, slot, pid, why, text }`: `NET_GONE` (a guest went silent), `NET_CLOSED`, `NET_DOWN` (why: `NET_WHY_*`, text: the room nobody had), `NET_ROOM` (text: the code), `NET_ROOM_FAILED` |
|
||||
|
||||
A frame, as the game runs it:
|
||||
|
||||
```ludic
|
||||
net_begin(dt) # time, and what came in onto the inbox
|
||||
let got = q_drain(net_inbox()) # the game's own dispatch
|
||||
for i in 0 .. len(got) { if net_peer_on(got[i].slot) { net_read(got[i]) ... } }
|
||||
net_update(dt) # the matchmaker, the hello, silence
|
||||
... queue this frame's messages ...
|
||||
net_end() # heartbeats, sending, slots let go
|
||||
q_drain(net_facts())
|
||||
```
|
||||
|
||||
The words for a reason are the game's: `NET_WHY_*` is a code, and `NET_WHY_GAME` means the game
|
||||
said why itself before `net_lose`.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
ludic test packages/ludic.net
|
||||
```
|
||||
|
||||
A fake socket: the codec, the codes, a host taking a stranger only with a hello, delivery in order,
|
||||
resending until taken, a stale unreliable message dropped, the hello's timeout, a silent guest, a
|
||||
slot let go once it took its last message, and no matchmaker. The matchmaker, STUN and the relay
|
||||
are covered by the game's `tests/net.sh` (`NET_ROOM=1`, `NET_RELAY=1`) against
|
||||
`tools/partyserver` on localhost.
|
||||
109
packages/ludic.net/codec.ludic
Normal file
109
packages/ludic.net/codec.ludic
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# codec.ludic - bytes little-endian, one message written at a time (nw_*) and one read (nr_*).
|
||||
# Numbers go as their raw bits: a float as float_bits, never rounded and never fixed.
|
||||
export function nb_put8(p: []byte, o: int, v: int) -> void { p[o] = v & 255 }
|
||||
|
||||
export function nb_put32(p: []byte, o: int, v: int) -> void {
|
||||
p[o] = v & 255
|
||||
p[o + 1] = (v >> 8) & 255
|
||||
p[o + 2] = (v >> 16) & 255
|
||||
p[o + 3] = (v >> 24) & 255
|
||||
}
|
||||
|
||||
export function nb_get8(p: []byte, o: int) -> int { return p[o] & 255 }
|
||||
|
||||
export function nb_get32(p: []byte, o: int) -> int {
|
||||
return (p[o] & 255) | ((p[o + 1] & 255) << 8) | ((p[o + 2] & 255) << 16) | ((p[o + 3] & 255) << 24)
|
||||
}
|
||||
|
||||
# n bytes from src at s to dst at d, forwards: within one buffer it may only move bytes DOWN
|
||||
export function nb_copy(dst: []byte, d: int, src: []byte, s: int, n: int) -> void {
|
||||
for i in 0 .. n { dst[d + i] = src[s + i] }
|
||||
}
|
||||
|
||||
# the writer: past the MTU it counts on and keeps nothing, and np_queue cuts the message there
|
||||
export function nw_begin() -> void {
|
||||
net__init()
|
||||
net__wl = 0
|
||||
}
|
||||
|
||||
export function nw_i(v: int) -> void {
|
||||
if net__wl + 4 <= NET_MTU { nb_put32(net__wb, net__wl, v) }
|
||||
net__wl += 4
|
||||
}
|
||||
|
||||
export function nw_b(v: bool) -> void { if v { nw_i(1) } else { nw_i(0) } }
|
||||
|
||||
export function nw_f(v: float) -> void { nw_i(float_bits(v)) }
|
||||
|
||||
# a string of at most 200 bytes; nw_text takes a longer one up to its own cap
|
||||
export function nw_s(s: string) -> void { nw_text(s, 200) }
|
||||
|
||||
export function nw_text(s: string, cap: int) -> void {
|
||||
var n = len(s)
|
||||
if n > cap { n = cap }
|
||||
nw_i(n)
|
||||
for i in 0 .. n {
|
||||
if net__wl < NET_MTU { net__wb[net__wl] = s[i] }
|
||||
net__wl += 1
|
||||
}
|
||||
}
|
||||
|
||||
export function nw_len() -> int { return net__wl }
|
||||
|
||||
# the reader: a short or bad message reads as zeros and sets nr_bad()
|
||||
export function nr_i() -> int {
|
||||
if net__rp + 4 > net__rl {
|
||||
net__rbad = true
|
||||
return 0
|
||||
}
|
||||
let v = nb_get32(net__rb, net__rp)
|
||||
net__rp += 4
|
||||
return v
|
||||
}
|
||||
|
||||
export function nr_b() -> bool { return nr_i() != 0 }
|
||||
|
||||
export function nr_f() -> float { return float_from_bits(nr_i()) }
|
||||
|
||||
export function nr_s() -> string { return nr_text(200) }
|
||||
|
||||
export function nr_text(cap: int) -> string {
|
||||
let n = nr_i()
|
||||
if n < 0 or n > cap or net__rp + n > net__rl {
|
||||
net__rbad = true
|
||||
return ""
|
||||
}
|
||||
if net__tb == null { net__tb = buffer(NET_MTU) }
|
||||
nb_copy(net__tb, 0, net__rb, net__rp, n)
|
||||
net__rp += n
|
||||
return text_of(net__tb, n)
|
||||
}
|
||||
|
||||
export function nr_bad() -> bool { return net__rbad }
|
||||
|
||||
# a message that came in, made the one nr_* reads
|
||||
export function net_read(m: NetMessage) -> void {
|
||||
net__init()
|
||||
nb_copy(net__rb, 0, m.data, 0, m.len)
|
||||
net__rl = m.len
|
||||
net__rp = 0
|
||||
net__rbad = false
|
||||
}
|
||||
|
||||
# the message just written, as if it had come in (a loopback: a test, a host telling itself)
|
||||
export function net_written() -> NetMessage {
|
||||
net__init()
|
||||
let m = new NetMessage
|
||||
m.from = net__my_pid
|
||||
m.len = min(net__wl, NET_MTU)
|
||||
m.data = buffer(m.len)
|
||||
nb_copy(m.data, 0, net__wb, 0, m.len)
|
||||
return m
|
||||
}
|
||||
|
||||
# the message being read, copied out as the next one to write (a host passing it on)
|
||||
export function net_copy_read() -> void {
|
||||
nw_begin()
|
||||
nb_copy(net__wb, 0, net__rb, 0, net__rl)
|
||||
net__wl = net__rl
|
||||
}
|
||||
100
packages/ludic.net/codes.ludic
Normal file
100
packages/ludic.net/codes.ludic
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# codes.ludic - an address and a port as ten letters in two fives, from an alphabet with no 0/O or
|
||||
# 1/I: what a host reads out. An "a.b.c.d:port" or a bare "a.b.c.d" is taken too; a room code is
|
||||
# six of the same letters.
|
||||
const NET_ALPHA: string = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
|
||||
export function net_code_of(ip: int, port: int) -> string {
|
||||
let a = NET_ALPHA
|
||||
let buf = buffer(11)
|
||||
# 48 bits: the port's 16, then the address's 32, five at a time from the top
|
||||
var o = 0
|
||||
for k in 0 .. 10 {
|
||||
let shift = 45 - k * 5
|
||||
var v = 0
|
||||
for b in 0 .. 5 {
|
||||
let bit = shift + 4 - b
|
||||
var x = 0
|
||||
if bit >= 32 { x = (port >> (bit - 32)) & 1 } else if bit >= 0 { x = (ip >> bit) & 1 }
|
||||
v = (v << 1) | x
|
||||
}
|
||||
buf[o] = a[v]
|
||||
o += 1
|
||||
if k == 4 {
|
||||
buf[o] = '-'
|
||||
o += 1
|
||||
}
|
||||
}
|
||||
return text_of(buf, o)
|
||||
}
|
||||
|
||||
# this machine's code: its address on the network and the socket's port
|
||||
export function net_join_code() -> string {
|
||||
var ip = NetSocket.local_ip()
|
||||
if ip == 0 { ip = Udp.ip("127.0.0.1") }
|
||||
return net_code_of(ip, net_port())
|
||||
}
|
||||
|
||||
function net__letter(c: int) -> int {
|
||||
var ch = c
|
||||
if ch >= 'a' and ch <= 'z' { ch = ch - 32 }
|
||||
if ch == 'O' { ch = '0' }
|
||||
if ch == 'I' { ch = '1' }
|
||||
let a = NET_ALPHA
|
||||
for i in 0 .. 32 { if a[i] == ch { return i } }
|
||||
return -1
|
||||
}
|
||||
|
||||
# a code or an address: true when net_code_ip() / net_code_port() say where
|
||||
export function net_parse_code(text: string) -> bool {
|
||||
let p = text
|
||||
var colon = -1
|
||||
var dots = 0
|
||||
for i in 0 .. len(p) {
|
||||
if p[i] == ':' { colon = i }
|
||||
if p[i] == '.' { dots += 1 }
|
||||
}
|
||||
if dots == 3 { return net__parse_addr(text, colon) }
|
||||
var hi = 0
|
||||
var lo = 0
|
||||
var n = 0
|
||||
for i in 0 .. len(p) {
|
||||
let c = p[i]
|
||||
if c == '-' or c == ' ' { continue }
|
||||
let v = net__letter(c)
|
||||
if v < 0 { return false }
|
||||
# 50 bits arrive; the top two are padding. Keep the port (bits 47..32) and the address.
|
||||
for b in 0 .. 5 {
|
||||
let bit = (v >> (4 - b)) & 1
|
||||
let pos = 49 - n * 5 - b
|
||||
if pos >= 32 and pos < 48 { hi = hi | (bit << (pos - 32)) }
|
||||
if pos >= 0 and pos < 32 { lo = lo | (bit << pos) }
|
||||
}
|
||||
n += 1
|
||||
}
|
||||
if n != 10 { return false }
|
||||
net__code_ip = lo
|
||||
net__code_port = hi
|
||||
return net__code_ip != 0 and net__code_port > 0
|
||||
}
|
||||
|
||||
function net__parse_addr(text: string, colon: int) -> bool {
|
||||
var host = text
|
||||
net__code_port = net__conf().port
|
||||
if colon > 0 {
|
||||
host = text[0..colon]
|
||||
net__code_port = Text.to_int(text[colon + 1..len(text)])
|
||||
}
|
||||
net__code_ip = Udp.ip(host)
|
||||
return net__code_ip != 0 and net__code_port > 0 and net__code_port < 65536
|
||||
}
|
||||
|
||||
export function net_code_ip() -> int { return net__code_ip }
|
||||
export function net_code_port() -> int { return net__code_port }
|
||||
|
||||
# six of the code's letters: a room at the matchmaker
|
||||
export function net_is_room(text: string) -> bool {
|
||||
let p = text
|
||||
if len(p) != 6 { return false }
|
||||
for i in 0 .. 6 { if net__letter(p[i]) < 0 { return false } }
|
||||
return true
|
||||
}
|
||||
21
packages/ludic.net/index.ludic
Normal file
21
packages/ludic.net/index.ludic
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# ludic.net - the transport a party plays over: what a message says and what each kind means are
|
||||
# the game's; getting it there, once, in order, through two home routers, is this
|
||||
module ludic_net uses ludic_base
|
||||
numbers float
|
||||
import "ludic.base"
|
||||
import "state.ludic"
|
||||
import "ports.ludic"
|
||||
import "codec.ludic"
|
||||
import "peers.ludic"
|
||||
import "send.ludic"
|
||||
import "receive.ludic"
|
||||
import "session.ludic"
|
||||
import "tick.ludic"
|
||||
import "queries.ludic"
|
||||
import "codes.ludic"
|
||||
import "stun.ludic"
|
||||
import "room.ludic"
|
||||
import "room_connect.ludic"
|
||||
import "room_tick.ludic"
|
||||
import "punch.ludic"
|
||||
import "relay.ludic"
|
||||
5
packages/ludic.net/package.ludic
Normal file
5
packages/ludic.net/package.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# ludic.net - a small party over UDP: peers, packets, reliable ordered delivery, the handshake's
|
||||
# frame, join codes, room codes through a matchmaker with STUN, punching and a relay. See README.md.
|
||||
package "ludic.net"
|
||||
version "0.1.0"
|
||||
kind source
|
||||
110
packages/ludic.net/peers.ludic
Normal file
110
packages/ludic.net/peers.ludic
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# peers.ludic - who this machine talks to, and the written message queued for them
|
||||
function net__find(ip: int, port: int) -> int {
|
||||
for i in 0 .. NET_PEERS {
|
||||
let p = net__peers[i]
|
||||
if p.on and p.ip == ip and p.port == port { return i }
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
# the slot talking to network player `pid`, or -1
|
||||
export function np_of_pid(pid: int) -> int {
|
||||
if net__peers == null { return -1 }
|
||||
for i in 0 .. NET_PEERS {
|
||||
let p = net__peers[i]
|
||||
if p.on and p.pid == pid { return i }
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function net__open(slot: int, ip: int, port: int, pid: int) -> void {
|
||||
let p = net__peers[slot]
|
||||
p.on = true
|
||||
p.ip = ip
|
||||
p.port = port
|
||||
p.pid = pid
|
||||
p.heard = net__time
|
||||
p.next_id = 1
|
||||
p.recv_id = 0
|
||||
p.acked = 0
|
||||
p.ack_due = false
|
||||
p.queue = new []NetMsg
|
||||
p.closing = false
|
||||
}
|
||||
|
||||
export function np_close(slot: int) -> void {
|
||||
if net__peers == null or slot < 0 or slot >= NET_PEERS { return }
|
||||
let p = net__peers[slot]
|
||||
p.on = false
|
||||
p.closing = false
|
||||
p.queue = new []NetMsg
|
||||
}
|
||||
|
||||
# the written message, for one peer (a reliable one is kept until it is taken)
|
||||
export function np_queue(slot: int, kind: int, rel: bool) -> void {
|
||||
if net__peers == null or slot < 0 or slot >= NET_PEERS { return }
|
||||
let p = net__peers[slot]
|
||||
if not p.on { return }
|
||||
let m = new NetMsg
|
||||
m.kind = kind
|
||||
m.rel = rel
|
||||
m.sent = -1.0
|
||||
m.made = net__time
|
||||
var n = net__wl
|
||||
if n > NET_MTU - 40 { n = NET_MTU - 40 }
|
||||
m.len = n
|
||||
m.data = buffer(n)
|
||||
nb_copy(m.data, 0, net__wb, 0, n)
|
||||
if rel {
|
||||
m.id = p.next_id
|
||||
p.next_id += 1
|
||||
}
|
||||
push(p.queue, m)
|
||||
}
|
||||
|
||||
# to everyone this machine talks to (a host: every guest but `except`; a guest: the host)
|
||||
export function np_queue_all(kind: int, rel: bool, except: int) -> void {
|
||||
if net__peers == null { return }
|
||||
for i in 0 .. NET_PEERS { if net__peers[i].on and i != except { np_queue(i, kind, rel) } }
|
||||
}
|
||||
|
||||
export function net_peer_on(slot: int) -> bool {
|
||||
if net__peers == null or slot < 0 or slot >= NET_PEERS { return false }
|
||||
return net__peers[slot].on
|
||||
}
|
||||
|
||||
export function net_peer_pid(slot: int) -> int {
|
||||
if not net_peer_on(slot) { return 0 }
|
||||
return net__peers[slot].pid
|
||||
}
|
||||
|
||||
export function net_peer_ip(slot: int) -> int {
|
||||
if not net_peer_on(slot) { return 0 }
|
||||
return net__peers[slot].ip
|
||||
}
|
||||
|
||||
export function net_peer_port(slot: int) -> int {
|
||||
if not net_peer_on(slot) { return 0 }
|
||||
return net__peers[slot].port
|
||||
}
|
||||
|
||||
export function net_peer_heard(slot: int) -> float {
|
||||
if not net_peer_on(slot) { return 0.0 }
|
||||
return net__peers[slot].heard
|
||||
}
|
||||
|
||||
# let the slot go once it has taken everything queued for it so far, or after `secs`
|
||||
export function net_close_when_sent(slot: int, secs: float) -> void {
|
||||
if not net_peer_on(slot) { return }
|
||||
let p = net__peers[slot]
|
||||
p.closing = true
|
||||
p.close_id = p.next_id - 1
|
||||
p.close_t = net__time + secs
|
||||
}
|
||||
|
||||
# the slots in use (a host's count of guests, a welcome not yet sent included)
|
||||
export function net_peer_count() -> int {
|
||||
var n = 0
|
||||
for i in 0 .. NET_PEERS { if net_peer_on(i) { n += 1 } }
|
||||
return n
|
||||
}
|
||||
96
packages/ludic.net/ports.ludic
Normal file
96
packages/ludic.net/ports.ludic
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# ports.ludic - the game's config, the socket and log it lends, and the facts it drains
|
||||
export property NetConfig {
|
||||
proto: int = 1 # a packet's protocol byte; two builds that differ here ignore each other
|
||||
port: int = 27415 # what an address typed without one means
|
||||
signal_url: string = "" # the matchmaker (HTTP); "" means room codes are off
|
||||
stun_host: string = "" # STUN and the relay (UDP), by name
|
||||
stun_port: int = 27400
|
||||
stun_at: string = "" # "ip:port" instead of the name (a test server)
|
||||
no_stun: bool = false # a test on one machine has no outside to ask about
|
||||
force_relay: bool = false # a room join goes through the relay at once
|
||||
relay_port0: int = 60000 # a relayed guest, as the host sees it: the relay's address, this + its session
|
||||
seed: int = 1 # the STUN transaction's dice
|
||||
}
|
||||
|
||||
# what the game hears about: a guest gone silent (slot, pid), a slot let go once it had taken
|
||||
# its last message (slot, pid), this machine out of the session (why), a room code (text) or a
|
||||
# matchmaker that failed a host (why)
|
||||
export const NET_GONE: int = 0
|
||||
export const NET_CLOSED: int = 1
|
||||
export const NET_DOWN: int = 2
|
||||
export const NET_ROOM: int = 3
|
||||
export const NET_ROOM_FAILED: int = 4
|
||||
|
||||
export const NET_WHY_GAME: int = 0 # the game's own words, set before net_lose
|
||||
export const NET_WHY_HOST_SILENT: int = 1
|
||||
export const NET_WHY_NO_ANSWER: int = 2 # a hello nobody answered
|
||||
export const NET_WHY_NO_SOCKET: int = 3
|
||||
export const NET_WHY_PORT_TAKEN: int = 4
|
||||
export const NET_WHY_NO_MATCHMAKER: int = 5
|
||||
export const NET_WHY_MATCHMAKER_SILENT: int = 6
|
||||
export const NET_WHY_NO_ROOM_GIVEN: int = 7
|
||||
export const NET_WHY_NO_ROOM: int = 8
|
||||
export const NET_WHY_BAD_ADDRESS: int = 9
|
||||
|
||||
export property NetFact {
|
||||
what: int = 0,
|
||||
slot: int = -1,
|
||||
pid: int = 0,
|
||||
why: int = 0,
|
||||
text: string = ""
|
||||
}
|
||||
|
||||
# how the game hears the transport talk (unbound: silence) and whether to log every packet
|
||||
export port NetWorld {
|
||||
log: fn(string) -> void = fn net__no_log
|
||||
trace: fn() -> bool = fn net__no
|
||||
}
|
||||
|
||||
# the socket (unbound: Udp); a test lends a fake one
|
||||
export port NetSocket {
|
||||
open: fn(int) -> int = fn net__udp_open
|
||||
close: fn(int) -> void = fn net__udp_close
|
||||
send: fn(int, int, int, []byte, int) -> int = fn net__udp_send
|
||||
recv: fn(int, []byte, int) -> int = fn net__udp_recv
|
||||
from_ip: fn(int) -> int = fn net__udp_from_ip
|
||||
from_port: fn(int) -> int = fn net__udp_from_port
|
||||
port: fn(int) -> int = fn net__udp_port
|
||||
local_ip: fn() -> int = fn net__udp_local_ip
|
||||
}
|
||||
|
||||
var net__cfg: NetConfig = null
|
||||
var net__facts: Queue<NetFact> = null
|
||||
|
||||
export function net_config(c: NetConfig) -> void { net__cfg = c }
|
||||
|
||||
function net__conf() -> NetConfig {
|
||||
if net__cfg == null { net__cfg = new NetConfig }
|
||||
return net__cfg
|
||||
}
|
||||
|
||||
export function net_facts() -> Queue<NetFact> {
|
||||
if net__facts == null { net__facts = queue_new("net.facts") }
|
||||
return net__facts
|
||||
}
|
||||
|
||||
function net__fact(what: int, slot: int, pid: int, why: int, text: string) -> void {
|
||||
let f = new NetFact
|
||||
f.what = what
|
||||
f.slot = slot
|
||||
f.pid = pid
|
||||
f.why = why
|
||||
f.text = text
|
||||
q_push(net_facts(), f)
|
||||
}
|
||||
|
||||
function net__no_log(s: string) -> void { }
|
||||
function net__no() -> bool { return false }
|
||||
function net__log(s: string) -> void { NetWorld.log(s) }
|
||||
function net__udp_open(port: int) -> int { return Udp.open(port) }
|
||||
function net__udp_close(h: int) -> void { Udp.close(h) }
|
||||
function net__udp_send(h: int, ip: int, port: int, b: []byte, n: int) -> int { return Udp.send(h, ip, port, b, n) }
|
||||
function net__udp_recv(h: int, b: []byte, cap: int) -> int { return Udp.recv(h, b, cap) }
|
||||
function net__udp_from_ip(h: int) -> int { return Udp.from_ip(h) }
|
||||
function net__udp_from_port(h: int) -> int { return Udp.from_port(h) }
|
||||
function net__udp_port(h: int) -> int { return Udp.port(h) }
|
||||
function net__udp_local_ip() -> int { return Udp.local_ip() }
|
||||
57
packages/ludic.net/punch.ludic
Normal file
57
packages/ludic.net/punch.ludic
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# punch.ludic - the host sends a few packets to each new guest's outside address, so its router
|
||||
# expects the guest, while the guest's hello goes the other way: both routers have seen traffic
|
||||
# go out, so the replies come in
|
||||
var net__seen: []string = null # guests already punched for
|
||||
var net__punch_ip: words = null
|
||||
var net__punch_port: words = null
|
||||
var net__punch_left: words = null
|
||||
var net__punch_t: float = 0.0
|
||||
var net__punch_b: []byte = null # the punch itself, "MP" and two zeros, made once
|
||||
|
||||
function net__new_guest(pub: string, local: string) -> void {
|
||||
let key = `{pub}|{local}`
|
||||
if net__seen == null { net__seen = new []string }
|
||||
for i in 0 .. len(net__seen) { if net__seen[i] == key { return } }
|
||||
push(net__seen, key)
|
||||
net__log(`room {net__room}: a guest at {pub} / {local}`)
|
||||
if len(pub) > 0 and net_parse_code(pub) { net__punch_add(net__code_ip, net__code_port) }
|
||||
if net_parse_code(local) { net__punch_add(net__code_ip, net__code_port) }
|
||||
}
|
||||
|
||||
function net__punch_add(ip: int, port: int) -> void {
|
||||
if net__punch_ip == null {
|
||||
net__punch_ip = words(16)
|
||||
net__punch_port = words(16)
|
||||
net__punch_left = words(16)
|
||||
for k in 0 .. 16 { net__punch_left[k] = 0 }
|
||||
}
|
||||
for k in 0 .. 16 {
|
||||
if net__punch_left[k] > 0 { continue }
|
||||
net__punch_ip[k] = ip
|
||||
net__punch_port[k] = port
|
||||
net__punch_left[k] = 10
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# twice a second while a guest is new: a packet the guest's side ignores, which is the point
|
||||
function net__punch_tick(dt: float) -> void {
|
||||
if net__punch_ip == null { return }
|
||||
net__punch_t = net__punch_t - dt
|
||||
if net__punch_t > 0.0 { return }
|
||||
net__punch_t = 0.5
|
||||
if net__punch_b == null {
|
||||
net__punch_b = buffer(4)
|
||||
nb_put8(net__punch_b, 0, 'M')
|
||||
nb_put8(net__punch_b, 1, 'P')
|
||||
}
|
||||
for k in 0 .. 16 {
|
||||
if net__punch_left[k] <= 0 { continue }
|
||||
NetSocket.send(net__sock, net__punch_ip[k], net__punch_port[k], net__punch_b, 4)
|
||||
net__punch_left[k] -= 1
|
||||
}
|
||||
}
|
||||
|
||||
function net__punch_clear() -> void {
|
||||
if net__punch_left != null { for k in 0 .. 16 { net__punch_left[k] = 0 } }
|
||||
}
|
||||
17
packages/ludic.net/queries.ludic
Normal file
17
packages/ludic.net/queries.ludic
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# queries.ludic - what the game asks of the session
|
||||
export function net_state() -> int { return net__state }
|
||||
export function net_live() -> bool { return net__state == NET_HOSTING or net__state == NET_JOINED }
|
||||
export function net_active() -> bool { return net__state != NET_IDLE and net__state != NET_LOST }
|
||||
export function net_hosting() -> bool { return net__state == NET_HOSTING }
|
||||
export function net_joined() -> bool { return net__state == NET_JOINED }
|
||||
export function net_joining() -> bool { return net__state == NET_JOINING }
|
||||
export function net_my_pid() -> int { return net__my_pid }
|
||||
export function net_token() -> int { return net__token }
|
||||
export function net_time() -> float { return net__time }
|
||||
export function net_why() -> int { return net__why }
|
||||
export function net_join_ip() -> int { return net__join_ip }
|
||||
export function net_join_port() -> int { return net__join_port }
|
||||
export function net_port() -> int {
|
||||
if net__sock <= 0 { return 0 }
|
||||
return NetSocket.port(net__sock)
|
||||
}
|
||||
91
packages/ludic.net/receive.ludic
Normal file
91
packages/ludic.net/receive.ludic
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# receive.ludic - what arrived, unwrapped from the relay, checked, acknowledged and put in order
|
||||
# on the inbox. A reliable message is taken strictly in order; one that arrives early is dropped
|
||||
# and comes again.
|
||||
function net__pump() -> void {
|
||||
if net__sock <= 0 { return }
|
||||
for guard in 0 .. 200 {
|
||||
let n = NetSocket.recv(net__sock, net__in, NET_MTU + 64)
|
||||
if n <= 0 { return }
|
||||
let n2 = net__unwrap(n, NetSocket.from_ip(net__sock), NetSocket.from_port(net__sock))
|
||||
if n2 > 0 { net__packet(n2, net__from_ip, net__from_port) }
|
||||
}
|
||||
}
|
||||
|
||||
function net__packet(n: int, ip: int, port: int) -> void {
|
||||
if NetWorld.trace() { net__log(`in {n} bytes from {Udp.ip_text(ip)}:{port}`) }
|
||||
if n < 20 { return }
|
||||
if net__stun_packet(n) { return } # what the router looks like from outside
|
||||
if nb_get8(net__in, 0) != 'M' or nb_get8(net__in, 1) != 'L' { return } # a punch ("MP") or noise
|
||||
if nb_get8(net__in, 2) != net__conf().proto { return }
|
||||
let slot = net__slot_for(n, ip, port)
|
||||
if slot < 0 { return }
|
||||
let p = net__peers[slot]
|
||||
p.heard = net__time
|
||||
# only what this connection has sent can be acknowledged: a packet from the one before it
|
||||
# (a re-join) carries the old numbers and would drop the new hello as already taken
|
||||
let ack = nb_get32(net__in, 12)
|
||||
if ack > p.acked and ack < p.next_id { p.acked = ack }
|
||||
net__messages(slot, n)
|
||||
}
|
||||
|
||||
# the known peer at that address, a stranger's new slot (a host, and only with a hello), or -1
|
||||
function net__slot_for(n: int, ip: int, port: int) -> int {
|
||||
let token = nb_get32(net__in, 4)
|
||||
var slot = net__find(ip, port)
|
||||
if slot >= 0 {
|
||||
if token != net__token and net__state != NET_JOINING and net__peers[slot].pid != 0 { return -1 }
|
||||
return slot
|
||||
}
|
||||
if net__state != NET_HOSTING { return -1 }
|
||||
if n < 28 or nb_get8(net__in, 20) != NET_HELLO { return -1 }
|
||||
for i in 1 .. NET_PEERS {
|
||||
if not net__peers[i].on {
|
||||
net__open(i, ip, port, 0)
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function net__messages(slot: int, n: int) -> void {
|
||||
let p = net__peers[slot]
|
||||
let from = nb_get32(net__in, 8)
|
||||
let count = nb_get32(net__in, 16)
|
||||
var o = 20
|
||||
for c in 0 .. count {
|
||||
if o + 4 > n { return }
|
||||
let kind = nb_get8(net__in, o)
|
||||
let rel = nb_get8(net__in, o + 1) == 1
|
||||
o += 2
|
||||
var id = 0
|
||||
if rel {
|
||||
if o + 4 > n { return }
|
||||
id = nb_get32(net__in, o)
|
||||
o += 4
|
||||
}
|
||||
let ml = nb_get8(net__in, o) | (nb_get8(net__in, o + 1) << 8)
|
||||
o += 2
|
||||
if ml < 0 or o + ml > n { return }
|
||||
var take = true
|
||||
if rel {
|
||||
p.ack_due = true
|
||||
if id != p.recv_id + 1 { take = false } else { p.recv_id = id }
|
||||
}
|
||||
if take { net__deliver(slot, from, kind, o, ml) }
|
||||
o += ml
|
||||
}
|
||||
}
|
||||
|
||||
# onto the inbox - except a heartbeat, and on a host anything but a hello before its welcome
|
||||
function net__deliver(slot: int, from: int, kind: int, o: int, ml: int) -> void {
|
||||
if kind == NET_BEAT { return }
|
||||
if net__state == NET_HOSTING and net__peers[slot].pid < 2 and kind != NET_HELLO { return }
|
||||
let m = new NetMessage
|
||||
m.slot = slot
|
||||
m.from = from
|
||||
m.kind = kind
|
||||
m.len = ml
|
||||
m.data = buffer(ml)
|
||||
nb_copy(m.data, 0, net__in, o, ml)
|
||||
q_push(net_inbox(), m)
|
||||
}
|
||||
89
packages/ludic.net/relay.ludic
Normal file
89
packages/ludic.net/relay.ludic
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# relay.ludic - a router that maps every destination to a new port defeats punching, so a guest
|
||||
# the host does not answer goes through the party server: "MLRG" + room + packet out, "MLRS" +
|
||||
# session + packet back. The host keeps its address there fresh ("MLRH" + room) and sees each
|
||||
# relayed guest as the relay's address with a port of relay_port0 + its session.
|
||||
var net__relay_ip: int = 0
|
||||
var net__relay_port: int = 0
|
||||
var net__via_relay: bool = false # a guest talking to its host through the relay
|
||||
var net__from_ip: int = 0
|
||||
var net__from_port: int = 0
|
||||
|
||||
function net__take_relay(v: Val) -> void {
|
||||
let r = net__json_str(v, "relay")
|
||||
if len(r) > 0 and net_parse_code(r) {
|
||||
net__relay_ip = net__code_ip
|
||||
net__relay_port = net__code_port
|
||||
}
|
||||
}
|
||||
|
||||
function net__code_bytes(b: []byte, o: int) -> void {
|
||||
let cp = net__room
|
||||
for k in 0 .. 6 {
|
||||
var c = 0
|
||||
if k < len(cp) { c = cp[k] }
|
||||
nb_put8(b, o + k, c)
|
||||
}
|
||||
}
|
||||
|
||||
function net__relay_hello() -> void {
|
||||
let b = buffer(10)
|
||||
nb_put8(b, 0, 'M')
|
||||
nb_put8(b, 1, 'L')
|
||||
nb_put8(b, 2, 'R')
|
||||
nb_put8(b, 3, 'H')
|
||||
net__code_bytes(b, 4)
|
||||
NetSocket.send(net__sock, net__relay_ip, net__relay_port, b, 10)
|
||||
}
|
||||
|
||||
function net__use_relay() -> void {
|
||||
net__via_relay = true
|
||||
net__log(`room {net__room}: no answer directly, through the relay`)
|
||||
net_dial(net__relay_ip, net__relay_port)
|
||||
}
|
||||
|
||||
export function net_via_relay() -> bool { return net__via_relay }
|
||||
|
||||
function net__relayed(p: NetPeer) -> bool {
|
||||
return net__relay_ip != 0 and p.ip == net__relay_ip and (p.port >= net__conf().relay_port0 or net__via_relay)
|
||||
}
|
||||
|
||||
# a packet of `o` bytes in net__out, to peer p: straight, or wrapped for the relay
|
||||
function net__relay_send(p: NetPeer, o: int) -> int {
|
||||
if not net__relayed(p) { return NetSocket.send(net__sock, p.ip, p.port, net__out, o) }
|
||||
var hdr = 10
|
||||
if not net__via_relay { hdr = 6 }
|
||||
var k = o - 1
|
||||
while k >= 0 {
|
||||
net__out[k + hdr] = net__out[k]
|
||||
k -= 1
|
||||
}
|
||||
nb_put8(net__out, 0, 'M')
|
||||
nb_put8(net__out, 1, 'L')
|
||||
nb_put8(net__out, 2, 'R')
|
||||
if net__via_relay {
|
||||
nb_put8(net__out, 3, 'G')
|
||||
net__code_bytes(net__out, 4)
|
||||
} else {
|
||||
let sid = p.port - net__conf().relay_port0
|
||||
nb_put8(net__out, 3, 'S')
|
||||
nb_put8(net__out, 4, (sid >> 8) & 255)
|
||||
nb_put8(net__out, 5, sid & 255)
|
||||
}
|
||||
let sent = NetSocket.send(net__sock, net__relay_ip, net__relay_port, net__out, o + hdr)
|
||||
nb_copy(net__out, 0, net__out, hdr, o) # the packet back where it was
|
||||
return sent
|
||||
}
|
||||
|
||||
# a relayed packet is unwrapped in place and given the sender it stands for; the game packet's
|
||||
# length comes back, 0 when there is nothing for the game
|
||||
function net__unwrap(n: int, ip: int, port: int) -> int {
|
||||
net__from_ip = ip
|
||||
net__from_port = port
|
||||
if n < 6 or net__relay_ip == 0 or ip != net__relay_ip or port != net__relay_port { return n }
|
||||
if nb_get8(net__in, 0) != 'M' or nb_get8(net__in, 1) != 'L' or nb_get8(net__in, 2) != 'R' or nb_get8(net__in, 3) != 'S' { return n }
|
||||
let sid = (nb_get8(net__in, 4) << 8) | nb_get8(net__in, 5)
|
||||
nb_copy(net__in, 0, net__in, 6, n - 6)
|
||||
# the host: this guest's session; a guest: the relay itself is its host
|
||||
if sid > 0 { net__from_port = net__conf().relay_port0 + sid }
|
||||
return n - 6
|
||||
}
|
||||
83
packages/ludic.net/room.ludic
Normal file
83
packages/ludic.net/room.ludic
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# room.ludic - a party across two home routers. The host registers its outside and inside
|
||||
# addresses with the matchmaker and is given a six-letter room code; a guest hands over the code
|
||||
# and its own addresses and gets the host's back. Behind one router, the inside addresses.
|
||||
const NET_R_IDLE: int = 0
|
||||
const NET_R_STUN: int = 1 # asking what this machine looks like from outside
|
||||
const NET_R_REGISTER: int = 2 # the host: asking for a room
|
||||
const NET_R_ROOM: int = 3 # the host: registered, watching for guests
|
||||
const NET_R_ASK: int = 4 # a guest: asking the room for the host
|
||||
const NET_R_DONE: int = 5 # a guest: the hello is on its way
|
||||
|
||||
var net__r_state: int = 0
|
||||
var net__r_role: int = 0 # 1 host, 2 guest
|
||||
var net__r_t: float = 0.0
|
||||
var net__r_tries: int = 0
|
||||
var net__pub_ip: int = 0
|
||||
var net__pub_port: int = 0
|
||||
var net__room: string = "" # the host's room code, or the one a guest asked for
|
||||
var net__http: int = 0
|
||||
var net__poll_t: float = 0.0
|
||||
var net__relay_t: float = 0.0
|
||||
var net__dial_heard: float = 0.0
|
||||
|
||||
# room codes are on when the game named a matchmaker
|
||||
export function net_room_on() -> bool { return len(net__conf().signal_url) > 0 }
|
||||
export function net_room() -> string { return net__room }
|
||||
export function net_room_asking() -> bool { return net__r_state == NET_R_STUN or net__r_state == NET_R_ASK }
|
||||
|
||||
# the host (after net_host or net_become_host): a room at the matchmaker, if there is one
|
||||
export function net_room_host() -> void {
|
||||
if not net_room_on() { return }
|
||||
net__r_role = 1
|
||||
net__room = ""
|
||||
net__seen = new []string
|
||||
net__stun_begin()
|
||||
}
|
||||
|
||||
# a guest by room code: the matchmaker's answer brings the host's address (the hello is set first)
|
||||
export function net_room_join(code: string) -> bool {
|
||||
if not net_room_on() {
|
||||
net__why = NET_WHY_NO_MATCHMAKER
|
||||
return false
|
||||
}
|
||||
net__init()
|
||||
if not net__socket() { return false }
|
||||
net__r_role = 2
|
||||
net__room = Text.upper(code)
|
||||
net__wait_more = 20 # the matchmaker answers before the host does
|
||||
net__wait_for_host()
|
||||
net__stun_begin()
|
||||
return true
|
||||
}
|
||||
|
||||
function net__stun_begin() -> void {
|
||||
net__r_state = NET_R_STUN
|
||||
net__r_t = 0.0
|
||||
net__r_tries = 0
|
||||
net__pub_ip = 0
|
||||
net__pub_port = 0
|
||||
if net__conf().no_stun {
|
||||
net__r_tries = 3
|
||||
return
|
||||
}
|
||||
net__stun_send()
|
||||
}
|
||||
|
||||
function net__room_body(role_key: string) -> string {
|
||||
var pub = ""
|
||||
if net__pub_ip != 0 { pub = net__addr_text(net__pub_ip, net__pub_port) }
|
||||
return "{\"" + role_key + "\": \"" + pub + "\", \"local\": \"" + net__local_text() + "\"}"
|
||||
}
|
||||
|
||||
function net__json_str(v: Val, key: string) -> string {
|
||||
if v == null or value_has(v, key) == 0 { return "" }
|
||||
return value_as_str(value_get(v, key))
|
||||
}
|
||||
|
||||
function net__addr_text(ip: int, port: int) -> string { return `{Udp.ip_text(ip)}:{port}` }
|
||||
|
||||
function net__local_text() -> string {
|
||||
var ip = NetSocket.local_ip()
|
||||
if ip == 0 { ip = Udp.ip("127.0.0.1") }
|
||||
return net__addr_text(ip, net_port())
|
||||
}
|
||||
47
packages/ludic.net/room_connect.ludic
Normal file
47
packages/ludic.net/room_connect.ludic
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# room_connect.ludic - the matchmaker's answer: a guest dials the host it named (or is out),
|
||||
# and leaving lets the room go
|
||||
# the matchmaker let this machine down: a guest is out, a host keeps its code-less party
|
||||
function net__room_fail(why: int) -> void {
|
||||
net__log(`matchmaker: {why}`)
|
||||
let was_guest = net__r_role == 2
|
||||
let room = net__room
|
||||
net__r_state = NET_R_IDLE
|
||||
net__r_role = 0
|
||||
if was_guest { net__lose_with(why, room) } else { net__fact(NET_ROOM_FAILED, -1, 0, why, room) }
|
||||
}
|
||||
|
||||
# the guest: to the host's outside address, or its inside one behind the same router
|
||||
function net__room_connect(host_pub: string, host_local: string) -> void {
|
||||
var target = host_pub
|
||||
var same_router = false
|
||||
if len(host_pub) > 0 and net__pub_ip != 0 and net_parse_code(host_pub) and net__code_ip == net__pub_ip { same_router = true }
|
||||
if len(host_pub) == 0 or same_router { target = host_local }
|
||||
if not net_parse_code(target) {
|
||||
net__room_fail(NET_WHY_BAD_ADDRESS)
|
||||
return
|
||||
}
|
||||
net__r_state = NET_R_DONE
|
||||
net__r_t = 0.0
|
||||
net__state = NET_IDLE # net_join wants a fresh start
|
||||
net_join(net__code_ip, net__code_port)
|
||||
net__dial_heard = net__peers[0].heard
|
||||
if net__conf().force_relay and net__relay_ip != 0 { # a test of the relay alone
|
||||
net__use_relay()
|
||||
return
|
||||
}
|
||||
net__log(`room {net__room}: dialling {target}`)
|
||||
}
|
||||
|
||||
function net__room_reset() -> void {
|
||||
if net__http != 0 {
|
||||
Http.free(net__http)
|
||||
net__http = 0
|
||||
}
|
||||
net__r_state = NET_R_IDLE
|
||||
net__r_role = 0
|
||||
net__room = ""
|
||||
net__via_relay = false
|
||||
net__relay_ip = 0
|
||||
net__relay_port = 0
|
||||
net__punch_clear()
|
||||
}
|
||||
106
packages/ludic.net/room_tick.ludic
Normal file
106
packages/ludic.net/room_tick.ludic
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# room_tick.ludic - the matchmaker's side of a frame: STUN asked three times, the room registered
|
||||
# or asked for, the host's look at who has asked, and a guest's switch to the relay
|
||||
function net__room_tick(dt: float) -> void {
|
||||
if net__r_state == NET_R_IDLE or net__r_role == 0 { return }
|
||||
net__r_t = net__r_t + dt
|
||||
if net__r_state == NET_R_STUN {
|
||||
net__room_stun_tick()
|
||||
return
|
||||
}
|
||||
if net__r_state == NET_R_REGISTER or net__r_state == NET_R_ASK or (net__r_state == NET_R_ROOM and net__http != 0) {
|
||||
net__room_answer()
|
||||
return
|
||||
}
|
||||
if net__r_state == NET_R_DONE {
|
||||
# four seconds and not one packet back from the host: through the relay instead (a slow
|
||||
# welcome is not a closed router - anything heard means the direct path works)
|
||||
let silent = net__peers[0].heard == net__dial_heard
|
||||
let wait = net__conf().force_relay or (silent and net__r_t > 4.0)
|
||||
if not net__via_relay and net__state == NET_JOINING and net__relay_ip != 0 and wait { net__use_relay() }
|
||||
return
|
||||
}
|
||||
if net__r_state == NET_R_ROOM { net__room_watch(dt) }
|
||||
}
|
||||
|
||||
# a second and a half for an answer, asked three times; without one, inside addresses only
|
||||
function net__room_stun_tick() -> void {
|
||||
if net__pub_ip == 0 and net__r_t > 0.5 and net__r_tries < 3 {
|
||||
net__r_tries += 1
|
||||
net__r_t = 0.0
|
||||
net__stun_send()
|
||||
return
|
||||
}
|
||||
if net__pub_ip == 0 and net__r_tries < 3 { return }
|
||||
let base = net__conf().signal_url
|
||||
if net__r_role == 1 {
|
||||
net__http = Http.post(`{base}/rooms`, net__room_body("host"))
|
||||
net__r_state = NET_R_REGISTER
|
||||
} else {
|
||||
net__http = Http.post(`{base}/rooms/{net__room}/join`, net__room_body("pub"))
|
||||
net__r_state = NET_R_ASK
|
||||
}
|
||||
net__r_t = 0.0
|
||||
}
|
||||
|
||||
function net__room_answer() -> void {
|
||||
let st = Http.poll(net__http)
|
||||
if st < 0 {
|
||||
if net__r_t > 12.0 {
|
||||
Http.free(net__http)
|
||||
net__http = 0
|
||||
net__room_fail(NET_WHY_MATCHMAKER_SILENT)
|
||||
}
|
||||
return
|
||||
}
|
||||
var v: Val = null
|
||||
if st > 0 and Http.ok(net__http) { v = Json.parse(Http.text(net__http)) }
|
||||
Http.free(net__http)
|
||||
net__http = 0
|
||||
if net__r_state == NET_R_REGISTER {
|
||||
let code = net__json_str(v, "code")
|
||||
if len(code) == 0 {
|
||||
net__room_fail(NET_WHY_NO_ROOM_GIVEN)
|
||||
return
|
||||
}
|
||||
net__room = code
|
||||
net__take_relay(v)
|
||||
net__r_state = NET_R_ROOM
|
||||
net__poll_t = 0.0
|
||||
net__log(`room {code}`)
|
||||
net__fact(NET_ROOM, -1, 0, 0, code)
|
||||
return
|
||||
}
|
||||
if net__r_state == NET_R_ASK {
|
||||
if v == null {
|
||||
net__room_fail(NET_WHY_NO_ROOM)
|
||||
return
|
||||
}
|
||||
net__take_relay(v)
|
||||
net__room_connect(net__json_str(v, "host"), net__json_str(v, "local"))
|
||||
return
|
||||
}
|
||||
# the host's look at the room
|
||||
if v != null and value_has(v, "guests") != 0 {
|
||||
let l = value_get(v, "guests")
|
||||
for i in 0 .. value_count(l) {
|
||||
let g = value_at(l, i)
|
||||
net__new_guest(net__json_str(g, "pub"), net__json_str(g, "local"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# the host, registered: its relay address kept fresh, new guests punched for, the room looked at
|
||||
function net__room_watch(dt: float) -> void {
|
||||
net__relay_t = net__relay_t - dt
|
||||
if net__relay_t < 0.0 and net__relay_ip != 0 {
|
||||
net__relay_t = 3.0
|
||||
net__relay_hello()
|
||||
}
|
||||
net__punch_tick(dt)
|
||||
net__poll_t = net__poll_t - dt
|
||||
if net__poll_t < 0.0 {
|
||||
net__poll_t = 1.0
|
||||
net__http = Http.get(`{net__conf().signal_url}/rooms/{net__room}`)
|
||||
net__r_t = 0.0
|
||||
}
|
||||
}
|
||||
81
packages/ludic.net/send.ludic
Normal file
81
packages/ludic.net/send.ludic
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# send.ludic - everything due goes out now, in as many packets as it takes (up to eight a call).
|
||||
# A packet is a 20-byte header - "ML", the protocol, 1, the session token, the sender's pid, the
|
||||
# highest reliable id taken in order, a message count - then each message: kind, flags, a
|
||||
# reliable id when it is reliable, a 16-bit length, the payload. A reliable message goes again
|
||||
# every quarter second until acknowledged; an unreliable one half a second old is dropped.
|
||||
export function np_flush(slot: int) -> void {
|
||||
if not net_peer_on(slot) { return }
|
||||
let p = net__peers[slot]
|
||||
for pk in 0 .. 8 {
|
||||
if not net__flush_one(p) { return }
|
||||
}
|
||||
}
|
||||
|
||||
function net__flush_one(p: NetPeer) -> bool {
|
||||
var o = 20
|
||||
var count = 0
|
||||
let keep = new []NetMsg
|
||||
for i in 0 .. len(p.queue) {
|
||||
let m = p.queue[i]
|
||||
if m.rel and m.id <= p.acked { continue } # taken: drop it
|
||||
if not m.rel and net__time - m.made > 0.5 { continue } # stale
|
||||
var due = true
|
||||
if m.rel and not (m.sent < 0.0) and net__time - m.sent < 0.25 { due = false }
|
||||
var size = 4 + m.len
|
||||
if m.rel { size += 4 }
|
||||
if due and o + size <= NET_MTU and count < 250 {
|
||||
o = net__put_msg(o, m)
|
||||
count += 1
|
||||
m.sent = net__time
|
||||
if net__kind_out != null and m.kind < 64 { net__kind_out[m.kind] += size }
|
||||
if m.rel { push(keep, m) } # kept until acknowledged
|
||||
} else {
|
||||
push(keep, m) # not due, or waits for the next packet
|
||||
}
|
||||
}
|
||||
p.queue = keep
|
||||
if count == 0 and not p.ack_due { return false }
|
||||
nb_put8(net__out, 0, 'M')
|
||||
nb_put8(net__out, 1, 'L')
|
||||
nb_put8(net__out, 2, net__conf().proto)
|
||||
nb_put8(net__out, 3, 1)
|
||||
nb_put32(net__out, 4, net__token)
|
||||
nb_put32(net__out, 8, net__my_pid)
|
||||
nb_put32(net__out, 12, p.recv_id)
|
||||
nb_put32(net__out, 16, count)
|
||||
let sent = net__relay_send(p, o)
|
||||
if sent > 0 { net__bytes_out += sent + 28 } # with the IP and UDP headers: what the line carries
|
||||
if NetWorld.trace() { net__log(`out {o} bytes to {Udp.ip_text(p.ip)}:{p.port}: {sent}`) }
|
||||
p.ack_due = false
|
||||
return count > 0
|
||||
}
|
||||
|
||||
function net__put_msg(at: int, m: NetMsg) -> int {
|
||||
var o = at
|
||||
nb_put8(net__out, o, m.kind)
|
||||
nb_put8(net__out, o + 1, 0)
|
||||
if m.rel { nb_put8(net__out, o + 1, 1) }
|
||||
o += 2
|
||||
if m.rel {
|
||||
nb_put32(net__out, o, m.id)
|
||||
o += 4
|
||||
}
|
||||
nb_put8(net__out, o, m.len & 255)
|
||||
nb_put8(net__out, o + 1, (m.len >> 8) & 255)
|
||||
o += 2
|
||||
nb_copy(net__out, o, m.data, 0, m.len)
|
||||
return o + m.len
|
||||
}
|
||||
|
||||
# the per-kind byte count starts again (a game measuring what a party costs its upload)
|
||||
export function net_kind_out_clear() -> void {
|
||||
net__kind_out = words(64)
|
||||
for k in 0 .. 64 { net__kind_out[k] = 0 }
|
||||
}
|
||||
|
||||
export function net_kind_bytes(kind: int) -> int {
|
||||
if net__kind_out == null or kind < 0 or kind >= 64 { return 0 }
|
||||
return net__kind_out[kind]
|
||||
}
|
||||
|
||||
export function net_bytes_out() -> int { return net__bytes_out }
|
||||
106
packages/ludic.net/session.ludic
Normal file
106
packages/ludic.net/session.ludic
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# session.ludic - hosting, joining and leaving. The host is pid 1 and hands out 2 upwards; this
|
||||
# machine's pid is what its packets say. A token picked by the host names the session.
|
||||
var net__why: int = 0
|
||||
|
||||
# open a socket on `port` and take guests; false (net_why) when the port is taken
|
||||
export function net_host(port: int, token: int) -> bool {
|
||||
net__init()
|
||||
if net_live() { return true }
|
||||
net__sock = NetSocket.open(port)
|
||||
if net__sock <= 0 {
|
||||
net__sock = 0
|
||||
net__why = NET_WHY_PORT_TAKEN
|
||||
return false
|
||||
}
|
||||
for i in 0 .. NET_PEERS { np_close(i) }
|
||||
net_become_host(token)
|
||||
net__log(`hosting on port {NetSocket.port(net__sock)}`)
|
||||
net_room_host()
|
||||
return true
|
||||
}
|
||||
|
||||
# dial a host at an address; the hello (net_hello_set) goes until a welcome or a refusal
|
||||
export function net_join(ip: int, port: int) -> bool {
|
||||
net__init()
|
||||
if net_live() { return false }
|
||||
if not net__socket() { return false }
|
||||
for i in 0 .. NET_PEERS { np_close(i) }
|
||||
net__token = 0
|
||||
net__my_pid = 0
|
||||
net__state = NET_JOINING
|
||||
net_dial(ip, port)
|
||||
net__log(`joining {Udp.ip_text(ip)}:{port}`)
|
||||
return true
|
||||
}
|
||||
|
||||
function net__socket() -> bool {
|
||||
if net__sock <= 0 { net__sock = NetSocket.open(0) }
|
||||
if net__sock > 0 { return true }
|
||||
net__sock = 0
|
||||
net__why = NET_WHY_NO_SOCKET
|
||||
return false
|
||||
}
|
||||
|
||||
# the message just written is the hello: what a host is asked to let this machine in with
|
||||
export function net_hello_set() -> void {
|
||||
net__init()
|
||||
nb_copy(net__hello_b, 0, net__wb, 0, net__wl)
|
||||
net__hello_l = net__wl
|
||||
if net__hello_l > NET_MTU - 40 { net__hello_l = NET_MTU - 40 }
|
||||
}
|
||||
|
||||
# (again) to that address, with a fresh connection and the hello sent anew
|
||||
export function net_dial(ip: int, port: int) -> void {
|
||||
net__init()
|
||||
np_close(0)
|
||||
net__join_ip = ip
|
||||
net__join_port = port
|
||||
net__open(0, ip, port, 1)
|
||||
net__hello_t = -1.0
|
||||
}
|
||||
|
||||
# a host that set out on a new trip, or a new host: the same socket asks to be let in there
|
||||
export function net_follow(ip: int, port: int) -> void {
|
||||
net_dial(ip, port)
|
||||
net__token = 0
|
||||
net__my_pid = 0
|
||||
net__state = NET_JOINING
|
||||
}
|
||||
|
||||
# the old host is gone and this machine keeps the socket, alone (it may become the host)
|
||||
export function net_stand_alone() -> void { net__state = NET_IDLE }
|
||||
|
||||
export function net_become_host(token: int) -> void {
|
||||
net__init()
|
||||
net__state = NET_HOSTING
|
||||
net__my_pid = 1
|
||||
net__next_pid = 2
|
||||
net__token = token
|
||||
}
|
||||
|
||||
# a welcome: this machine is `pid` in the session `token`
|
||||
export function net_joined_as(token: int, pid: int) -> void {
|
||||
net__token = token
|
||||
net__my_pid = pid
|
||||
net__state = NET_JOINED
|
||||
net__wait_more = 0
|
||||
}
|
||||
|
||||
# the host lets the stranger in `slot` in: its pid, from 2 upwards
|
||||
export function net_accept(slot: int) -> int {
|
||||
if net__state != NET_HOSTING or not net_peer_on(slot) { return 0 }
|
||||
let p = net__peers[slot]
|
||||
if p.pid >= 2 { return p.pid }
|
||||
p.pid = net__next_pid
|
||||
net__next_pid += 1
|
||||
return p.pid
|
||||
}
|
||||
|
||||
# a slow host is given this many more seconds to answer (a map switch, a matchmaker)
|
||||
export function net_hello_wait(secs: int) -> void { net__wait_more = secs }
|
||||
|
||||
# a room code: joining, with no address yet - the matchmaker's answer brings one
|
||||
function net__wait_for_host() -> void {
|
||||
net__state = NET_JOINING
|
||||
net__hello_t = 0.0
|
||||
}
|
||||
97
packages/ludic.net/state.ludic
Normal file
97
packages/ludic.net/state.ludic
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# state.ludic - the wire's numbers, a peer, a queued message, what came in, and the session
|
||||
export const NET_MTU: int = 1200
|
||||
export const NET_PEERS: int = 12 # a host: slot 0 unused, a guest each after; a guest: 0 is the host
|
||||
export const NET_TIMEOUT: int = 10 # seconds of silence before a peer is gone
|
||||
export const NET_HELLO: int = 1 # the one kind a stranger may send a host; every other is the game's
|
||||
const NET_BEAT: int = 0 # a heartbeat, never delivered
|
||||
|
||||
export const NET_IDLE: int = 0
|
||||
export const NET_HOSTING: int = 1
|
||||
export const NET_JOINING: int = 2
|
||||
export const NET_JOINED: int = 3
|
||||
export const NET_LOST: int = 4
|
||||
|
||||
# a message on its way out, kept until acknowledged when it is reliable
|
||||
property NetMsg {
|
||||
kind: int = 0,
|
||||
rel: bool = false,
|
||||
id: int = 0,
|
||||
sent: float = 0.0, # net seconds it was last sent; -1 never
|
||||
len: int = 0,
|
||||
made: float = 0.0, # net seconds it was queued
|
||||
data: []byte
|
||||
}
|
||||
|
||||
property NetPeer {
|
||||
on: bool = false,
|
||||
ip: int = 0,
|
||||
port: int = 0,
|
||||
pid: int = 0,
|
||||
heard: float = 0.0, # net seconds of the last packet from it
|
||||
next_id: int = 1, # the next reliable id to send it
|
||||
recv_id: int = 0, # the highest reliable id taken from it, in order
|
||||
acked: int = 0, # the highest id it has taken from us
|
||||
ack_due: bool = false,
|
||||
queue: []NetMsg,
|
||||
closing: bool = false, # let go once it has taken close_id
|
||||
close_id: int = 0,
|
||||
close_t: float = 0.0 # ... or at this net time, whichever comes first
|
||||
}
|
||||
|
||||
# a message that came in, in order: read it with net_read(m) and nr_*
|
||||
export property NetMessage {
|
||||
slot: int = 0, # the peer slot it came from
|
||||
from: int = 0, # the sender's pid, as its header says
|
||||
kind: int = 0,
|
||||
len: int = 0,
|
||||
data: []byte
|
||||
}
|
||||
|
||||
var net__state: int = 0
|
||||
var net__sock: int = 0
|
||||
var net__time: float = 0.0
|
||||
var net__token: int = 0
|
||||
var net__my_pid: int = 0
|
||||
var net__next_pid: int = 2
|
||||
var net__peers: []NetPeer = null
|
||||
var net__in: []byte = null
|
||||
var net__out: []byte = null
|
||||
var net__wb: []byte = null # the message being written
|
||||
var net__wl: int = 0
|
||||
var net__rb: []byte = null # the message being read
|
||||
var net__rl: int = 0
|
||||
var net__rp: int = 0
|
||||
var net__rbad: bool = false
|
||||
var net__tb: []byte = null # text read out of a message, through one scratch buffer
|
||||
var net__hello_b: []byte = null # the game's hello, sent again on every dial
|
||||
var net__hello_l: int = 0
|
||||
var net__hello_t: float = 0.0 # seconds since it went; -1 not yet
|
||||
var net__wait_more: int = 0 # extra seconds a slow host is given (a map switch, a matchmaker)
|
||||
var net__beat_t: float = 0.0
|
||||
var net__join_ip: int = 0
|
||||
var net__join_port: int = 0
|
||||
var net__code_ip: int = 0
|
||||
var net__code_port: int = 0
|
||||
var net__bytes_out: int = 0
|
||||
var net__kind_out: words = null
|
||||
var net__inbox: Queue<NetMessage> = null
|
||||
|
||||
export function net_inbox() -> Queue<NetMessage> {
|
||||
if net__inbox == null { net__inbox = queue_new("net.inbox") }
|
||||
return net__inbox
|
||||
}
|
||||
|
||||
function net__init() -> void {
|
||||
if net__peers != null { return }
|
||||
net__peers = new []NetPeer
|
||||
for i in 0 .. NET_PEERS {
|
||||
let p = new NetPeer
|
||||
p.queue = new []NetMsg
|
||||
push(net__peers, p)
|
||||
}
|
||||
net__in = buffer(NET_MTU + 64)
|
||||
net__out = buffer(NET_MTU + 64)
|
||||
net__wb = buffer(NET_MTU)
|
||||
net__rb = buffer(NET_MTU)
|
||||
net__hello_b = buffer(NET_MTU)
|
||||
}
|
||||
68
packages/ludic.net/stun.ludic
Normal file
68
packages/ludic.net/stun.ludic
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# stun.ludic - from the party's own socket, ask a public STUN server what this machine looks like
|
||||
# from outside: the router's address and the port it mapped
|
||||
var net__stun_ip: int = 0
|
||||
var net__stun_port: int = 0
|
||||
var net__txn: words = null
|
||||
var net__dice: Rng = null
|
||||
|
||||
function net__stun_send() -> void {
|
||||
if net__txn == null { net__txn = words(3) }
|
||||
if net__dice == null { net__dice = rng_new(net__conf().seed) }
|
||||
if net__stun_ip == 0 {
|
||||
let c = net__conf()
|
||||
if len(c.stun_at) > 0 and net_parse_code(c.stun_at) {
|
||||
net__stun_ip = net__code_ip
|
||||
net__stun_port = net__code_port
|
||||
} else if len(c.stun_host) > 0 {
|
||||
let host: pointer = c.stun_host
|
||||
net__stun_ip = Udp.resolve(host)
|
||||
net__stun_port = c.stun_port
|
||||
}
|
||||
if net__stun_ip == 0 { return }
|
||||
}
|
||||
let b = buffer(20)
|
||||
nb_put8(b, 0, 0) # a binding request, no attributes
|
||||
nb_put8(b, 1, 1)
|
||||
nb_put8(b, 2, 0)
|
||||
nb_put8(b, 3, 0)
|
||||
nb_put8(b, 4, 0x21) # the magic cookie
|
||||
nb_put8(b, 5, 0x12)
|
||||
nb_put8(b, 6, 0xA4)
|
||||
nb_put8(b, 7, 0x42)
|
||||
for k in 0 .. 3 { net__txn[k] = rng_between(net__dice, 0, 2000000000) }
|
||||
for k in 0 .. 12 { nb_put8(b, 8 + k, (net__txn[k / 4] >> ((k % 4) * 8)) & 255) }
|
||||
NetSocket.send(net__sock, net__stun_ip, net__stun_port, b, 20)
|
||||
}
|
||||
|
||||
function net__be16(p: []byte, o: int) -> int { return (nb_get8(p, o) << 8) | nb_get8(p, o + 1) }
|
||||
|
||||
function net__be32(p: []byte, o: int) -> int { return (net__be16(p, o) << 16) | net__be16(p, o + 2) }
|
||||
|
||||
# a STUN reply: true when it was one (whatever it said)
|
||||
function net__stun_packet(n: int) -> bool {
|
||||
if n < 20 or nb_get8(net__in, 0) != 1 or nb_get8(net__in, 1) != 1 { return false }
|
||||
if net__be32(net__in, 4) != 0x2112A442 { return false }
|
||||
let len = net__be16(net__in, 2)
|
||||
var o = 20
|
||||
while o + 4 <= 20 + len and o + 4 <= n {
|
||||
let t = net__be16(net__in, o)
|
||||
let l = net__be16(net__in, o + 2)
|
||||
if (t == 0x0020 or t == 0x0001) and l >= 8 and o + 12 <= n and nb_get8(net__in, o + 5) == 1 {
|
||||
var port = net__be16(net__in, o + 6)
|
||||
var ip = net__be32(net__in, o + 8)
|
||||
if t == 0x0020 {
|
||||
port = port ^ 0x2112
|
||||
ip = ip ^ 0x2112A442
|
||||
}
|
||||
net__pub_ip = ip
|
||||
net__pub_port = port
|
||||
net__log(`outside we are {net__addr_text(ip, port)}`)
|
||||
}
|
||||
o += 4 + ((l + 3) / 4) * 4
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
# what this machine looks like from outside, once STUN has said (0 until then)
|
||||
export function net_outside_ip() -> int { return net__pub_ip }
|
||||
export function net_outside_port() -> int { return net__pub_port }
|
||||
275
packages/ludic.net/tests/net_test.ludic
Normal file
275
packages/ludic.net/tests/net_test.ludic
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
# net_test.ludic - the codec, the codes, a host taking a stranger's hello, reliable delivery in
|
||||
# order, a guest's hello and its timeout, a silent guest and a slot let go - over a fake socket
|
||||
import "ludic.net"
|
||||
import "ludic.base"
|
||||
program NetTest {
|
||||
numbers float
|
||||
property Pkt { ip: int = 0, port: int = 0, len: int = 0, data: []byte }
|
||||
var sent: []Pkt = null
|
||||
var inbound: []Pkt = null
|
||||
var from_ip: int = 0
|
||||
var from_port: int = 0
|
||||
|
||||
function fake_open(port: int) -> int { return 5 }
|
||||
function fake_close(h: int) -> void { }
|
||||
function fake_port(h: int) -> int { return 40000 }
|
||||
function fake_local() -> int { return Udp.ip("10.0.0.5") }
|
||||
function fake_from_ip(h: int) -> int { return from_ip }
|
||||
function fake_from_port(h: int) -> int { return from_port }
|
||||
function fake_send(h: int, ip: int, port: int, b: []byte, n: int) -> int {
|
||||
let p = new Pkt
|
||||
p.ip = ip
|
||||
p.port = port
|
||||
p.len = n
|
||||
p.data = buffer(n)
|
||||
nb_copy(p.data, 0, b, 0, n)
|
||||
push(sent, p)
|
||||
return n
|
||||
}
|
||||
function fake_recv(h: int, b: []byte, cap: int) -> int {
|
||||
if len(inbound) == 0 { return 0 }
|
||||
let p = inbound[0]
|
||||
let rest = new []Pkt
|
||||
for i in 1 .. len(inbound) { push(rest, inbound[i]) }
|
||||
inbound = rest
|
||||
nb_copy(b, 0, p.data, 0, p.len)
|
||||
from_ip = p.ip
|
||||
from_port = p.port
|
||||
return p.len
|
||||
}
|
||||
bind NetSocket { open: fn fake_open, close: fn fake_close, send: fn fake_send, recv: fn fake_recv, from_ip: fn fake_from_ip, from_port: fn fake_from_port, port: fn fake_port, local_ip: fn fake_local }
|
||||
|
||||
function fresh() -> void {
|
||||
sent = new []Pkt
|
||||
inbound = new []Pkt
|
||||
net_reset()
|
||||
q_clear(net_inbox())
|
||||
q_clear(net_facts())
|
||||
}
|
||||
|
||||
# a packet from ip:port: token, pid, ack, then one message of `kind` (reliable with `id` > 0)
|
||||
function arrive(ip: int, port: int, token: int, pid: int, ack: int, kind: int, id: int, payload: int) -> void {
|
||||
let b = buffer(40)
|
||||
nb_put8(b, 0, 'M')
|
||||
nb_put8(b, 1, 'L')
|
||||
nb_put8(b, 2, 1)
|
||||
nb_put8(b, 3, 1)
|
||||
nb_put32(b, 4, token)
|
||||
nb_put32(b, 8, pid)
|
||||
nb_put32(b, 12, ack)
|
||||
nb_put32(b, 16, 1)
|
||||
var o = 20
|
||||
nb_put8(b, o, kind)
|
||||
nb_put8(b, o + 1, 0)
|
||||
o += 2
|
||||
if id > 0 {
|
||||
nb_put8(b, o - 1, 1)
|
||||
nb_put32(b, o, id)
|
||||
o += 4
|
||||
}
|
||||
nb_put8(b, o, 4)
|
||||
nb_put8(b, o + 1, 0)
|
||||
nb_put32(b, o + 2, payload)
|
||||
let p = new Pkt
|
||||
p.ip = ip
|
||||
p.port = port
|
||||
p.len = o + 6
|
||||
p.data = b
|
||||
push(inbound, p)
|
||||
}
|
||||
|
||||
function frame(dt: float) -> void {
|
||||
net_begin(dt)
|
||||
net_update(dt)
|
||||
net_end()
|
||||
}
|
||||
|
||||
# the host with a guest let in at 10.0.0.9:5000 as pid 2
|
||||
function host_with_guest() -> int {
|
||||
fresh()
|
||||
expect(net_host(27415, 77))
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 0, 0, 0, NET_HELLO, 1, 42)
|
||||
net_begin(0.01)
|
||||
let got = q_drain(net_inbox())
|
||||
expect_eq(len(got), 1)
|
||||
return net_accept(got[0].slot)
|
||||
}
|
||||
|
||||
function facts_of(what: int) -> []NetFact {
|
||||
let all = q_drain(net_facts())
|
||||
let out = new []NetFact
|
||||
for i in 0 .. len(all) { if all[i].what == what { push(out, all[i]) } }
|
||||
return out
|
||||
}
|
||||
|
||||
test "the codec carries ints, bools, floats and text, and a short message reads bad" {
|
||||
fresh()
|
||||
nw_begin()
|
||||
nw_i(-7)
|
||||
nw_b(true)
|
||||
nw_f(2.5)
|
||||
nw_s("maroon")
|
||||
net_read(net_written())
|
||||
expect_eq(nr_i(), -7)
|
||||
expect(nr_b())
|
||||
expect_near(nr_f(), 2.5, 0.0001)
|
||||
expect(nr_s() == "maroon")
|
||||
expect(not nr_bad())
|
||||
expect_eq(nr_i(), 0)
|
||||
expect(nr_bad())
|
||||
}
|
||||
|
||||
test "a join code decodes to its address and port; an address and a room code are taken too" {
|
||||
fresh()
|
||||
let ip = Udp.ip("192.168.1.20")
|
||||
let code = net_code_of(ip, 27415)
|
||||
expect_eq(len(code), 11)
|
||||
expect(net_parse_code(code))
|
||||
expect_eq(net_code_ip(), ip)
|
||||
expect_eq(net_code_port(), 27415)
|
||||
expect(net_parse_code(Text.lower(code)))
|
||||
expect(net_parse_code("10.1.2.3:4000"))
|
||||
expect_eq(net_code_port(), 4000)
|
||||
expect(net_parse_code("10.1.2.3"))
|
||||
expect_eq(net_code_port(), 27415)
|
||||
expect(not net_parse_code("HELLO"))
|
||||
expect(net_is_room("abcdef"))
|
||||
expect(not net_is_room("ABCDE1"))
|
||||
}
|
||||
|
||||
test "a host takes a stranger only with a hello, and hands out pids from 2" {
|
||||
let pid = host_with_guest()
|
||||
expect_eq(pid, 2)
|
||||
expect(net_hosting())
|
||||
expect_eq(net_my_pid(), 1)
|
||||
arrive(Udp.ip("10.0.0.10"), 6000, 0, 0, 0, 9, 1, 1)
|
||||
net_begin(0.01)
|
||||
expect_eq(q_len(net_inbox()), 0)
|
||||
arrive(Udp.ip("10.0.0.11"), 6000, 0, 0, 0, NET_HELLO, 1, 1)
|
||||
net_begin(0.01)
|
||||
let got = q_drain(net_inbox())
|
||||
expect_eq(net_accept(got[0].slot), 3)
|
||||
}
|
||||
|
||||
test "a message's payload reads back as it was written" {
|
||||
host_with_guest()
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 2, 1234)
|
||||
net_begin(0.01)
|
||||
let got = q_drain(net_inbox())
|
||||
expect_eq(len(got), 1)
|
||||
expect_eq(got[0].kind, 9)
|
||||
expect_eq(got[0].from, 2)
|
||||
net_read(got[0])
|
||||
expect_eq(nr_i(), 1234)
|
||||
expect(not nr_bad())
|
||||
nr_i()
|
||||
expect(nr_bad())
|
||||
}
|
||||
|
||||
test "a reliable message that arrives early is dropped and taken in order" {
|
||||
let slot = np_of_pid(host_with_guest())
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 3, 30)
|
||||
net_begin(0.01)
|
||||
expect_eq(q_len(net_inbox()), 0)
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 2, 20)
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 3, 30)
|
||||
net_begin(0.01)
|
||||
let got = q_drain(net_inbox())
|
||||
expect_eq(len(got), 2)
|
||||
net_read(got[1])
|
||||
expect_eq(nr_i(), 30)
|
||||
expect_eq(got[1].slot, slot)
|
||||
}
|
||||
|
||||
test "a reliable message goes again until it is taken" {
|
||||
let slot = np_of_pid(host_with_guest())
|
||||
net_end()
|
||||
sent = new []Pkt
|
||||
nw_begin()
|
||||
nw_i(5)
|
||||
np_queue(slot, 9, true)
|
||||
frame(0.01)
|
||||
expect_eq(len(sent), 1)
|
||||
frame(0.1)
|
||||
expect_eq(len(sent), 1)
|
||||
frame(0.2)
|
||||
expect_eq(len(sent), 2)
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 1, 9, 0, 0)
|
||||
frame(0.3)
|
||||
let n = len(sent)
|
||||
frame(0.3)
|
||||
expect_eq(len(sent), n)
|
||||
}
|
||||
|
||||
test "an unreliable message half a second old is dropped" {
|
||||
let slot = np_of_pid(host_with_guest())
|
||||
net_end()
|
||||
sent = new []Pkt
|
||||
nw_begin()
|
||||
np_queue(slot, 9, false)
|
||||
net_begin(0.6)
|
||||
net_end()
|
||||
expect_eq(len(sent), 0)
|
||||
}
|
||||
|
||||
test "a guest sends its hello until nobody has answered for too long" {
|
||||
fresh()
|
||||
nw_begin()
|
||||
nw_s("v1")
|
||||
net_hello_set()
|
||||
expect(net_join(Udp.ip("10.0.0.1"), 27415))
|
||||
expect(net_joining())
|
||||
frame(0.1)
|
||||
expect_eq(len(sent), 1)
|
||||
expect_eq(nb_get8(sent[0].data, 20), NET_HELLO)
|
||||
for i in 0 .. 110 { frame(0.1) }
|
||||
expect_eq(net_state(), NET_LOST)
|
||||
let down = facts_of(NET_DOWN)
|
||||
expect_eq(len(down), 1)
|
||||
expect_eq(down[0].why, NET_WHY_NO_ANSWER)
|
||||
net_fresh()
|
||||
expect_eq(net_state(), NET_IDLE)
|
||||
}
|
||||
|
||||
test "a welcome makes the guest that pid in that session" {
|
||||
fresh()
|
||||
nw_begin()
|
||||
net_hello_set()
|
||||
net_join(Udp.ip("10.0.0.1"), 27415)
|
||||
net_joined_as(99, 4)
|
||||
expect(net_live())
|
||||
expect_eq(net_my_pid(), 4)
|
||||
expect_eq(net_token(), 99)
|
||||
}
|
||||
|
||||
test "a guest silent for the timeout is gone" {
|
||||
let pid = host_with_guest()
|
||||
let slot = np_of_pid(pid)
|
||||
for i in 0 .. 105 { frame(0.1) }
|
||||
let gone = facts_of(NET_GONE)
|
||||
expect_eq(len(gone), 1)
|
||||
expect_eq(gone[0].pid, pid)
|
||||
expect_eq(gone[0].slot, slot)
|
||||
expect(not net_peer_on(slot))
|
||||
}
|
||||
|
||||
test "a slot is let go once it has taken its last message" {
|
||||
let slot = np_of_pid(host_with_guest())
|
||||
nw_begin()
|
||||
np_queue(slot, 9, true)
|
||||
net_close_when_sent(slot, 20.0)
|
||||
frame(0.01)
|
||||
expect(net_peer_on(slot))
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 1, 9, 0, 0)
|
||||
frame(0.01)
|
||||
expect(not net_peer_on(slot))
|
||||
expect_eq(len(facts_of(NET_CLOSED)), 1)
|
||||
}
|
||||
|
||||
test "with no matchmaker, a room code is refused" {
|
||||
fresh()
|
||||
expect(not net_room_on())
|
||||
expect(not net_room_join("ABCDEF"))
|
||||
expect_eq(net_why(), NET_WHY_NO_MATCHMAKER)
|
||||
}
|
||||
}
|
||||
111
packages/ludic.net/tick.ludic
Normal file
111
packages/ludic.net/tick.ludic
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# tick.ludic - a frame of the session, in three calls with the game's between them: net_begin
|
||||
# (time, and what came in onto the inbox), net_update (the matchmaker, the hello, silence) and,
|
||||
# once the game has queued its own, net_end (heartbeats, sending, letting slots go)
|
||||
export function net_begin(dt: float) -> void {
|
||||
if not net_active() { return }
|
||||
net__time = net__time + dt
|
||||
net__pump()
|
||||
}
|
||||
|
||||
export function net_update(dt: float) -> void {
|
||||
if not net_active() { return }
|
||||
net__room_tick(dt)
|
||||
if net__state == NET_JOINING {
|
||||
net__hello_tick(dt)
|
||||
return
|
||||
}
|
||||
for i in 0 .. NET_PEERS {
|
||||
let p = net__peers[i]
|
||||
if not p.on or net__time - p.heard <= float(NET_TIMEOUT) { continue }
|
||||
if net__state == NET_JOINED {
|
||||
net_lose(NET_WHY_HOST_SILENT)
|
||||
return
|
||||
}
|
||||
let gone = p.pid
|
||||
np_close(i)
|
||||
net__fact(NET_GONE, i, gone, 0, "")
|
||||
}
|
||||
}
|
||||
|
||||
function net__hello_tick(dt: float) -> void {
|
||||
if net__hello_t < 0.0 {
|
||||
nb_copy(net__wb, 0, net__hello_b, 0, net__hello_l)
|
||||
net__wl = net__hello_l
|
||||
np_queue(0, NET_HELLO, true)
|
||||
net__hello_t = 0.0
|
||||
}
|
||||
net__hello_t = net__hello_t + dt
|
||||
if net__hello_t > float(NET_TIMEOUT + net__wait_more) {
|
||||
net_lose(NET_WHY_NO_ANSWER)
|
||||
return
|
||||
}
|
||||
np_flush(0)
|
||||
}
|
||||
|
||||
export function net_end() -> void {
|
||||
if not net_live() { return }
|
||||
let beat = net__time - net__beat_t >= 1.0
|
||||
if beat { net__beat_t = net__time }
|
||||
for i in 0 .. NET_PEERS {
|
||||
let p = net__peers[i]
|
||||
if not p.on { continue }
|
||||
if beat {
|
||||
nw_begin()
|
||||
np_queue(i, NET_BEAT, false)
|
||||
}
|
||||
np_flush(i)
|
||||
if p.closing and (p.acked >= p.close_id or net__time > p.close_t) {
|
||||
let pid = p.pid
|
||||
np_close(i)
|
||||
net__fact(NET_CLOSED, i, pid, 0, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# while a load holds the frame: keep answering, so nobody times out on this machine
|
||||
export function net_keepalive() -> void {
|
||||
if not net_live() { return }
|
||||
net__pump()
|
||||
for i in 0 .. NET_PEERS {
|
||||
let p = net__peers[i]
|
||||
if p.on {
|
||||
p.heard = net__time
|
||||
nw_begin()
|
||||
np_queue(i, NET_BEAT, false)
|
||||
np_flush(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# every queue to its peer now, three times over (a machine about to let go)
|
||||
export function net_flush_all() -> void {
|
||||
for t in 0 .. 3 { for i in 0 .. NET_PEERS { np_flush(i) } }
|
||||
}
|
||||
|
||||
# the session is over for this machine: every slot, the socket and the matchmaker let go
|
||||
export function net_reset() -> void {
|
||||
if net__peers != null { for i in 0 .. NET_PEERS { np_close(i) } }
|
||||
if net__sock > 0 {
|
||||
NetSocket.close(net__sock)
|
||||
net__sock = 0
|
||||
}
|
||||
net__state = NET_IDLE
|
||||
net__my_pid = 0
|
||||
net__wait_more = 0
|
||||
net__room_reset()
|
||||
}
|
||||
|
||||
# out of the session, and why: the game hears NET_DOWN and says so in its own words
|
||||
export function net_lose(why: int) -> void { net__lose_with(why, "") }
|
||||
|
||||
# ... with what the reason is about (the room code nobody had)
|
||||
function net__lose_with(why: int, text: string) -> void {
|
||||
net_reset()
|
||||
net__state = NET_LOST
|
||||
net__why = why
|
||||
net__log(`lost ({why})`)
|
||||
net__fact(NET_DOWN, -1, 0, why, text)
|
||||
}
|
||||
|
||||
# a lost session read and done with: idle again
|
||||
export function net_fresh() -> void { if net__state == NET_LOST { net__state = NET_IDLE } }
|
||||
Loading…
Add table
Add a link
Reference in a new issue