ludic.shop, ludic.net: nothing made per tick - the week's lists and dice reused, the net's facts from a ring, the room's watch address made once, pools and kept lists through kept_push; what runs once per host, join, room or guest says so in @alloc_ok

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 16:09:32 +03:00
parent f5834f8f63
commit 4b533b5ff6
18 changed files with 86 additions and 23 deletions

View file

@ -3,6 +3,7 @@
# six of the same letters.
const NET_ALPHA: string = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
@alloc_ok("once per join code shown")
export function net_code_of(ip: int, port: int) -> string {
let a = NET_ALPHA
let buf = buffer(11)

View file

@ -59,7 +59,7 @@ export function np_queue(net_st: mut NetState, slot: int, kind: int, rel: bool)
m.id = p.next_id
p.next_id += 1
}
push(p.queue, m)
kept_push(p.queue, m)
}
# to everyone this machine talks to (a host: every guest but `except`; a guest: the host)

View file

@ -3,14 +3,12 @@
# taken or goes stale; an inbox one comes back two drains after it was handed out
function net__msg_take(net_st: mut NetState) -> NetMsg {
if len(net_st.net__free) > 0 { return List.pop(net_st.net__free) }
let m = new NetMsg
m.data = buffer(NET_MTU)
return m
return net__msg_new()
}
# everything a peer had queued, back to the pool (its slot opened again, or let go)
function net__queue_free(net_st: mut NetState, p: NetPeer) -> void {
for i in 0 .. len(p.queue) { push(net_st.net__free, p.queue[i]) }
for i in 0 .. len(p.queue) { kept_push(net_st.net__free, p.queue[i]) }
List.clear(p.queue)
}
@ -24,9 +22,7 @@ function net__in_take(net_st: mut NetState) -> NetMessage {
var pool = net_st.net__ina
if net_st.net__in_on_b { pool = net_st.net__inb }
if net_st.net__inn >= len(pool) {
let m = new NetMessage
m.data = buffer(NET_MTU)
push(pool, m)
net__in_grow(pool)
}
let m = pool[net_st.net__inn]
net_st.net__inn += 1
@ -34,3 +30,31 @@ function net__in_take(net_st: mut NetState) -> NetMessage {
}
export function net_spare_count(net_st: NetState) -> int { return len(net_st.net__free) }
@alloc_ok("a pool miss: past the most messages in flight at once only, then kept")
function net__msg_new() -> NetMsg {
let m = new NetMsg
m.data = buffer(NET_MTU)
return m
}
@alloc_ok("a pool miss: past the most messages in one drain only, then kept")
function net__in_grow(pool: []NetMessage) -> void {
let m = new NetMessage
m.data = buffer(NET_MTU)
push(pool, m)
}
# a fact's record, from a ring made with the state: far more than a frame reports between drains
const NET_FACTS_MOST: int = 64
function net__fpool__new() -> []NetFact {
let xs = new []NetFact
for i in 0 .. NET_FACTS_MOST { push(xs, new NetFact) }
return xs
}
function net__fact_rec(net_st: mut NetState) -> NetFact {
let f = net_st.net__fpool[net_st.net__fat]
net_st.net__fat = (net_st.net__fat + 1) % len(net_st.net__fpool)
return f
}

View file

@ -69,8 +69,8 @@ export function net_facts(net_st: NetState) -> Queue<NetFact> {
return net_st.net__facts
}
function net__fact(net_st: NetState, what: int, slot: int, pid: int, why: int, text: string) -> void {
let f = new NetFact
function net__fact(net_st: mut NetState, what: int, slot: int, pid: int, why: int, text: string) -> void {
let f = net__fact_rec(net_st)
f.what = what
f.slot = slot
f.pid = pid

View file

@ -2,6 +2,7 @@
# expects the guest, while the guest's hello goes the other way: both routers have seen traffic
# go out, so the replies come in
@alloc_ok("once per guest punched for")
function net__new_guest(net_st: mut NetState, pub: string, local: string) -> void {
let key = `{pub}|{local}`
for i in 0 .. len(net_st.net__seen) { if net_st.net__seen[i] == key { return } }
@ -11,6 +12,7 @@ function net__new_guest(net_st: mut NetState, pub: string, local: string) -> voi
if net_parse_code(net_st, local) { net__punch_add(net_st, net_st.net__code_ip, net_st.net__code_port) }
}
@alloc_ok("once per guest punched for")
function net__punch_add(net_st: mut NetState, ip: int, port: int) -> void {
if net_st.net__punch_ip == null {
net_st.net__punch_ip = words(16)
@ -34,6 +36,7 @@ function net__punch_tick(net_st: mut NetState, dt: float) -> void {
if net_st.net__punch_t > 0.0 { return }
net_st.net__punch_t = 0.5
if net_st.net__punch_b == null {
@alloc_ok("once: the punch's four bytes")
net_st.net__punch_b = buffer(4)
nb_put8(net_st.net__punch_b, 0, 'M')
nb_put8(net_st.net__punch_b, 1, 'P')

View file

@ -12,6 +12,7 @@ function net__pump(net_st: mut NetState) -> void {
}
function net__packet(net_st: mut NetState, n: int, ip: int, port: int) -> void {
@alloc_ok("only while tracing every packet")
if NetWorld.trace() { net__log(`in {n} bytes from {Udp.ip_text(ip)}:{port}`) }
if n < 20 { return }
if net__stun_packet(net_st, n) { return } # what the router looks like from outside

View file

@ -30,6 +30,7 @@ function net__relay_hello(net_st: NetState) -> void {
NetSocket.send(net_st.net__sock, net_st.net__relay_ip, net_st.net__relay_port, b, 10)
}
@alloc_ok("once, when the room falls back to the relay")
function net__use_relay(net_st: mut NetState) -> void {
net_st.net__via_relay = true
net__log(`room {net_st.net__room}: no answer directly, through the relay`)

View file

@ -15,6 +15,7 @@ export function net_room(net_st: NetState) -> string { return net_st.net__room }
export function net_room_asking(net_st: NetState) -> bool { return net_st.net__r_state == NET_R_STUN or net_st.net__r_state == NET_R_ASK }
# the host (after net_host or net_become_host): a room at the matchmaker, if there is one
@alloc_ok("once per room opened")
export function net_room_host(net_st: mut NetState) -> void {
if not net_room_on(net_st) { return }
net_st.net__r_role = 1
@ -51,6 +52,7 @@ function net__stun_begin(net_st: mut NetState) -> void {
net__stun_send(net_st)
}
@alloc_ok("once per request to the party server")
function net__room_body(net_st: NetState, role_key: string) -> string {
var pub = ""
if net_st.net__pub_ip != 0 { pub = net__addr_text(net_st.net__pub_ip, net_st.net__pub_port) }
@ -62,6 +64,7 @@ function net__json_str(v: Val, key: string) -> string {
return value_as_str(value_get(v, key))
}
@alloc_ok("a STUN answer or a log line: a few per session")
function net__addr_text(ip: int, port: int) -> string { return `{Udp.ip_text(ip)}:{port}` }
function net__local_text(net_st: NetState) -> string {

View file

@ -1,6 +1,7 @@
# 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
@alloc_ok("once, when a room cannot be had")
function net__room_fail(net_st: mut NetState, why: int) -> void {
net__log(`matchmaker: {why}`)
let was_guest = net_st.net__r_role == 2
@ -11,6 +12,7 @@ function net__room_fail(net_st: mut NetState, why: int) -> void {
}
# the guest: to the host's outside address, or its inside one behind the same router
@alloc_ok("once per room answer")
function net__room_connect(net_st: mut NetState, host_pub: string, host_local: string) -> void {
var target = host_pub
var same_router = false

View file

@ -23,6 +23,7 @@ function net__room_tick(net_st: mut NetState, dt: float) -> void {
}
# a second and a half for an answer, asked three times; without one, inside addresses only
@alloc_ok("a request to the party server: once per room stage")
function net__room_stun_tick(net_st: mut NetState) -> void {
if net_st.net__pub_ip == 0 and net_st.net__r_t > 0.5 and net_st.net__r_tries < 3 {
net_st.net__r_tries += 1
@ -63,6 +64,8 @@ function net__room_answer(net_st: mut NetState) -> void {
return
}
net_st.net__room = code
@alloc_ok("once per room")
net_st.net__watch_url = `{net__conf(net_st).signal_url}/rooms/{code}`
net__take_relay(net_st, v)
net_st.net__r_state = NET_R_ROOM
net_st.net__poll_t = 0.0
@ -100,7 +103,7 @@ function net__room_watch(net_st: mut NetState, dt: float) -> void {
net_st.net__poll_t = net_st.net__poll_t - dt
if net_st.net__poll_t < 0.0 {
net_st.net__poll_t = 1.0
net_st.net__http = Http.get(`{net__conf(net_st).signal_url}/rooms/{net_st.net__room}`)
net_st.net__http = Http.get(net_st.net__watch_url)
net_st.net__r_t = 0.0
}
}

View file

@ -21,7 +21,7 @@ function net__flush_one(net_st: mut NetState, p: NetPeer) -> bool {
let taken = m.rel and m.id <= p.acked
let stale = not m.rel and net_st.net__time - m.made > 0.5
if taken or stale {
push(net_st.net__free, m) # back to the pool
kept_push(net_st.net__free, m) # back to the pool
continue
}
var due = true
@ -33,9 +33,9 @@ function net__flush_one(net_st: mut NetState, p: NetPeer) -> bool {
count += 1
m.sent = net_st.net__time
if net_st.net__kind_out != null and m.kind < 64 { net_st.net__kind_out[m.kind] += size }
if m.rel { push(keep, m) } else { push(net_st.net__free, m) } # a reliable one kept until acknowledged
if m.rel { kept_push(keep, m) } else { kept_push(net_st.net__free, m) } # a reliable one kept until acknowledged
} else {
push(keep, m) # not due, or waits for the next packet
kept_push(keep, m) # not due, or waits for the next packet
}
}
p.back = p.queue
@ -51,6 +51,7 @@ function net__flush_one(net_st: mut NetState, p: NetPeer) -> bool {
nb_put32(net_st.net__out, 16, count)
let sent = net__relay_send(net_st, p, o)
if sent > 0 { net_st.net__bytes_out += sent + 28 } # with the IP and UDP headers: what the line carries
@alloc_ok("only while tracing every packet")
if NetWorld.trace() { net__log(`out {o} bytes to {Udp.ip_text(p.ip)}:{p.port}: {sent}`) }
p.ack_due = false
return count > 0
@ -75,7 +76,10 @@ function net__put_msg(net_st: NetState, at: int, m: NetMsg) -> int {
# the per-kind byte count starts again (a game measuring what a party costs its upload)
export function net_kind_out_clear(net_st: mut NetState) -> void {
net_st.net__kind_out = words(64)
if net_st.net__kind_out == null {
@alloc_ok("once: the counts are made the first time they are cleared")
net_st.net__kind_out = words(64)
}
for k in 0 .. 64 { net_st.net__kind_out[k] = 0 }
}

View file

@ -2,6 +2,7 @@
# machine's pid is what its packets say. A token picked by the host names the session.
# open a socket on `port` and take guests; false (net_why) when the port is taken
@alloc_ok("once per party hosted")
export function net_host(net_st: mut NetState, port: int, token: int) -> bool {
if net_live(net_st) { return true }
net_st.net__sock = NetSocket.open(port)
@ -18,6 +19,7 @@ export function net_host(net_st: mut NetState, port: int, token: int) -> bool {
}
# dial a host at an address; the hello (net_hello_set) goes until a welcome or a refusal
@alloc_ok("once per join")
export function net_join(net_st: mut NetState, ip: int, port: int) -> bool {
if net_live(net_st) { return false }
if not net__socket(net_st) { return false }

View file

@ -51,6 +51,9 @@ export property NetMessage {
export state NetState {
net__cfg: NetConfig = new NetConfig
net__facts: Queue<NetFact> = net__facts__new()
net__fpool: []NetFact = net__fpool__new() # the facts' records, taken in turn (pool.ludic)
net__fat: int = 0
net__watch_url: string = "" # the room's address, made once per room
net__seen: []string = new []string # guests already punched for
net__free: []NetMsg = new []NetMsg # sent and taken (or stale): made again, never new (pool.ludic)
net__ina: []NetMessage = new []NetMessage # the inbox's records, two halves

View file

@ -99,6 +99,7 @@ export function net_reset(net_st: mut NetState) -> void {
export function net_lose(net_st: mut NetState, why: int) -> void { net__lose_with(net_st, why, "") }
# ... with what the reason is about (the room code nobody had)
@alloc_ok("once, when the party is lost")
function net__lose_with(net_st: mut NetState, why: int, text: string) -> void {
net_reset(net_st)
net_st.net__state = NET_LOST

View file

@ -25,11 +25,12 @@ function shop_pick_one(shop_st: ShopState, v: int, taken: []int) -> int {
}
function shop_roll_vendor(shop_st: mut ShopState, v: int) -> void {
let taken = new []int
let taken = shop_st.shop_taken
List.clear(taken)
for k in 0 .. shop_st.shop_wants_n {
let it = shop_pick_one(shop_st, v, taken)
shop_st.shop_want_list[v * shop_st.shop_wants_n + k] = it
if it >= 0 { push(taken, it) }
if it >= 0 { kept_push(taken, it) }
}
shop_st.shop_full_list[v] = shop_pick_one(shop_st, v, taken)
}
@ -39,11 +40,8 @@ export function shop_roll(shop_st: mut ShopState) -> void {
let week = shop_week(shop_st)
if week == shop_st.shop_week_rolled and shop_st.shop_want_list != null { return }
shop_st.shop_week_rolled = week
shop_st.shop_dice = rng_new(shop_st.shop_seed + week * 37)
shop_st.shop_want_list = new []int
shop_st.shop_full_list = new []int
for i in 0 .. shop_vendors(shop_st) * shop_st.shop_wants_n { push(shop_st.shop_want_list, -1) }
for v in 0 .. shop_vendors(shop_st) { push(shop_st.shop_full_list, -1) }
rng_seed(shop_st.shop_dice, shop_st.shop_seed + week * 37)
shop_lists_empty(shop_st)
if shop_items(shop_st) == 0 { return }
for v in 0 .. shop_vendors(shop_st) { shop_roll_vendor(shop_st, v) }
}
@ -90,3 +88,4 @@ function shop_demand_reset(shop_st: mut ShopState) -> void {
shop_st.shop_want_list = null
shop_st.shop_full_list = null
}

View file

@ -5,6 +5,7 @@ numbers float
import "ludic.base"
import "state.ludic"
import "demand.ludic"
import "week_lists.ludic"
import "prices.ludic"
import "trade.ludic"
import "system.ludic"

View file

@ -40,7 +40,8 @@ export state ShopState {
shop_week_rolled: int = -1
shop_want_list: []int = null # shop_wants_n per vendor, -1 empty
shop_full_list: []int = null # one per vendor, -1 none
shop_dice: Rng = null
shop_dice: Rng = rng_new(0)
shop_taken: []int = new []int # a vendor's picks while its week is rolled, kept
shop_base: []int = null # an item's base price
shop_sell_frac: []float = null # what fraction of the curve a vendor pays for it
shop_vendor_list: []ShopVendor = null

View file

@ -0,0 +1,14 @@
# ludic.shop/week_lists.ludic - the lists a week is rolled into, made once and cleared after
# the week's lists, every entry -1: made the first week and cleared after, the vendors being fixed
function shop_lists_empty(shop_st: mut ShopState) -> void {
if shop_st.shop_want_list == null {
@alloc_ok("once: the lists are made the first week they are rolled")
shop_st.shop_want_list = new []int
@alloc_ok("once: as above")
shop_st.shop_full_list = new []int
}
List.clear(shop_st.shop_want_list)
List.clear(shop_st.shop_full_list)
for i in 0 .. shop_vendors(shop_st) * shop_st.shop_wants_n { kept_push(shop_st.shop_want_list, -1) }
for v in 0 .. shop_vendors(shop_st) { kept_push(shop_st.shop_full_list, -1) }
}