From 6cff8d620cd92f9ddc63c6891b3e669b5294619e Mon Sep 17 00:00:00 2001 From: ubq323 Date: Sun, 14 Apr 2024 00:35:19 +0100 Subject: multiple chunks on server and unload not near players. just need client to req chunks now --- client/main.ha | 25 ++++++-- drawing/drawing.ha | 2 + packet_reader/packet_reader.ha | 65 ++++++++++++++----- server/main.ha | 142 ++++++++++++++++++++++++++++++----------- server/save_load.ha | 4 +- todo | 2 - 6 files changed, 179 insertions(+), 61 deletions(-) diff --git a/client/main.ha b/client/main.ha index e720b65..de6b2fd 100644 --- a/client/main.ha +++ b/client/main.ha @@ -4,12 +4,11 @@ use math; use net; use net::dial; use drawing; -use drawing::{pos}; +use drawing::{pos,CHUNKSIZE}; use client::paintui; use unix::poll; use packet_reader; -def CHUNKSIZE = 512; def NCHUNKS = 16; def WIN_H: i32 = 480; @@ -42,13 +41,19 @@ export fn main() void = { drawing::outline_picture(p); }; - // connect to server const conn = match(dial::dial("tcp","localhost","41460")) { case let c: net::socket => yield c; case let err: net::error => fmt::fatal("couldn't connect to server:",net::strerror(err)); }; + + for (let i = 0; i < 3; i += 1) { + packet_reader::send(conn, ( + i*CHUNKSIZE,i*CHUNKSIZE + ):packet_reader::packet_reqchunk)!; + }; + const pollfd: [1]poll::pollfd = [ poll::pollfd { fd = conn, events=poll::event::POLLIN, revents = 0 }]; @@ -66,7 +71,7 @@ export fn main() void = { let mouse_pos: pos = (0,0); let mouse_down = false; for (!quit) { - do_movement(&camera_pos); + const did_move = do_movement(&camera_pos); // let nvis = 0; // for (const pic &.. pictures) { @@ -95,7 +100,6 @@ export fn main() void = { case => void; }; - match (paintui::tick(&pstate, mouse_pos, mouse_down)) { case void => yield; case let op: drawing::op => @@ -103,6 +107,10 @@ export fn main() void = { packet_reader::send(conn, op: packet_reader::packet_drawop)!; }; + if (did_move) { + packet_reader::send(conn, camera_pos: packet_reader::packet_position)!; + }; + const n = poll::poll(pollfd, poll::NONBLOCK)!; if (n > 0) { fmt::println("data available")!; @@ -155,7 +163,8 @@ fn render_picture(pic: *drawing::picture, surf: *sdl2::SDL_Surface, winsurf: *sd def SPEED = 17; def DIAG_SPEED = 12; // thereabouts -fn do_movement(pos: *pos) void = { +// returned whether movement happened +fn do_movement(pos: *pos) bool = { const kb = sdl2::SDL_GetKeyboardState(); let dx = 0; let dy = 0; @@ -164,11 +173,15 @@ fn do_movement(pos: *pos) void = { if (kb[sdl2::SDL_Scancode::A]) dx -= 1; if (kb[sdl2::SDL_Scancode::D]) dx += 1; + const did_move = dx!=0 || dy!=0; + let speed = SPEED; if (dx != 0 && dy != 0) speed = DIAG_SPEED; pos.0 += dx * speed; pos.1 += dy * speed; + + return did_move; }; fn is_picture_visible(camera_pos: pos, pic_pos: pos) bool = { diff --git a/drawing/drawing.ha b/drawing/drawing.ha index 70bd7af..fc59e1b 100644 --- a/drawing/drawing.ha +++ b/drawing/drawing.ha @@ -4,6 +4,8 @@ use io; use endian; use math::random; +export def CHUNKSIZE = 512; + export type pos = (i32, i32); export type picture = struct { diff --git a/packet_reader/packet_reader.ha b/packet_reader/packet_reader.ha index 30911df..df4d417 100644 --- a/packet_reader/packet_reader.ha +++ b/packet_reader/packet_reader.ha @@ -2,6 +2,7 @@ use io; use fmt; use endian; use drawing; +use drawing::{pos,CHUNKSIZE}; export type error = !str; @@ -27,15 +28,23 @@ fn cast_u8s_to_u32s(in: []u8) []u32 = export type packet_type = enum u8 { DRAW_OP, SEND_CHUNK, + POSITION, + REQ_CHUNK }; export type packet_drawop = drawing::op; export type packet_sendchunk = struct { - world_pos: drawing::pos, + world_pos: pos, chunk_data: []u32, }; +export type packet_position = pos; +export type packet_reqchunk = pos; -export type packet = (packet_drawop | packet_sendchunk); +export type packet = ( + packet_drawop + | packet_sendchunk + | packet_position + | packet_reqchunk); // call when input is ready. could block otherwise export fn read(pr: *packet_reader, sock: io::handle) (void | io::error | io::EOF) = { @@ -43,7 +52,7 @@ export fn read(pr: *packet_reader, sock: io::handle) (void | io::error | io::EOF const read_pos = if (remaining_amt > 0) { // still some unconsumed content in the buffer // move unconsumed stuff to start of buffer - fmt::printfln("moving {} remaining bytes",remaining_amt)!; + // fmt::printfln("moving {} remaining bytes",remaining_amt)!; pr.buf[0..remaining_amt] = pr.good; yield remaining_amt; } else 0z; @@ -51,10 +60,10 @@ export fn read(pr: *packet_reader, sock: io::handle) (void | io::error | io::EOF case io::EOF => return io::EOF; case let n: size => yield n; }; - fmt::printfln("read {} bytes",nread)!; + // fmt::printfln("read {} bytes",nread)!; const total_amt = read_pos + nread; pr.good = pr.buf[0..total_amt]; - fmt::printfln("now {} bytes in buffer",total_amt)!; + // fmt::printfln("now {} bytes in buffer",total_amt)!; }; // packet format: @@ -69,34 +78,50 @@ export fn next(pr: *packet_reader) (packet | done | error) = { // and return the packet, // or, ascertain there is no full packet, and return done if (len(pr.good) < size(u32)) return done; - fmt::println("a")!; const packet_len = endian::legetu32(pr.good[0..4]); if (packet_len < 8) return "packet size field too small": error; - fmt::println("b")!; if (len(pr.good) < packet_len) return done; - fmt::println("c")!; const packet_bytes = pr.good[..packet_len]; pr.good = pr.good[packet_len..]; const ty = endian::legetu32(packet_bytes[4..8]): packet_type; + const payload = packet_bytes[8..]; switch (ty) { case packet_type::DRAW_OP => - const op = drawing::deser_op(packet_bytes[8..]); - fmt::println("d")!; + const op = drawing::deser_op(payload); return op: packet_drawop; case packet_type::SEND_CHUNK => // return value is BORROWED from the BUFFER - const pos_bytes = packet_bytes[8..16]; - const chunk_data_bytes = packet_bytes[16..]; + const pos_bytes = payload[0..8]; + const chunk_data_bytes = payload[8..]; const pos = ( endian::legetu32(pos_bytes[0..4]): i32, endian::legetu32(pos_bytes[4..8]): i32 - ): drawing::pos; + ): pos; const d = cast_u8s_to_u32s(chunk_data_bytes); - assert(len(d) == 512*512,"wrong chunk size??"); - fmt::println("e")!; + if (len(d) != CHUNKSIZE*CHUNKSIZE) + return "wrong chunk size??": error; return packet_sendchunk { world_pos = pos, chunk_data = d }; + case packet_type::POSITION => + if (len(payload) != 8) + return "position packet wrong size": error; + const pos = ( + endian::legetu32(payload[0..4]): i32, + endian::legetu32(payload[4..8]): i32 + ): pos; + return pos: packet_position; + case packet_type::REQ_CHUNK => + if (len(payload) != 8) + return "reqchunk packet wrong size": error; + const world_pos = ( + endian::legetu32(payload[0..4]): i32, + endian::legetu32(payload[4..8]): i32 + ): pos; + if (world_pos.0 % CHUNKSIZE != 0 || world_pos.1 % CHUNKSIZE != 0) { + return "invalid world_pos": error; + }; + return world_pos: packet_reqchunk; }; }; @@ -124,5 +149,15 @@ export fn send(sock: io::file, packet: packet) (void | io::error) = { endian::leputu32(pos_buf[4..8],packet.world_pos.1: u32); const chunk_data_bytes = cast_u32s_to_u8s(packet.chunk_data); send_raw(sock, packet_type::SEND_CHUNK, pos_buf, chunk_data_bytes)?; + case let pos: packet_position => + const pos_buf: [8]u8 = [0...]; + endian::leputu32(pos_buf[0..4], pos.0: u32); + endian::leputu32(pos_buf[4..8], pos.1: u32); + send_raw(sock, packet_type::POSITION, pos_buf)?; + case let world_pos: packet_reqchunk => + const pos_buf: [8]u8 = [0...]; + endian::leputu32(pos_buf[0..4], world_pos.0: u32); + endian::leputu32(pos_buf[4..8], world_pos.1: u32); + send_raw(sock, packet_type::REQ_CHUNK, pos_buf)?; }; }; diff --git a/server/main.ha b/server/main.ha index 1dffbb5..19ecc51 100644 --- a/server/main.ha +++ b/server/main.ha @@ -15,15 +15,15 @@ use time; use unix::poll; use drawing; -use drawing::{pos}; +use drawing::{pos,CHUNKSIZE}; use packet_reader; def PORT: u16 = 41460; -def CHUNKSIZE = 512; type server_state = struct { connections: []connection, + // nb we use 'picture' and 'chunk' interchangeably pictures: []drawing::picture, listener: net::socket, running: bool @@ -39,20 +39,13 @@ type connection = struct { def save_interval = 20 * time::SECOND; export fn main() void = { - // create 4 pictures. later we will have more pictures - // but for now there are just 4, and they are at fixed positions - const offs: [_]pos = [ - (0,0), (0,CHUNKSIZE), (CHUNKSIZE,0), (CHUNKSIZE,CHUNKSIZE), - ]; - - let pictures: []drawing::picture = alloc([],len(offs)); - for (let i = 0z; i < len(offs); i +=1){ - append(pictures, match (load_picture_from_file(offs[i])) { - case let pic: drawing::picture => yield pic; - case let err: fs::error => fmt::fatal(fs::strerror(err)); - case bad_header => fmt::fatal("bad ppm header"); - }); - }; + // for (let i = 0z; i < len(offs); i +=1){ + // append(pictures, match (load_picture_from_file(offs[i])) { + // case let pic: drawing::picture => yield pic; + // case let err: fs::error => fmt::fatal(fs::strerror(err)); + // case bad_header => fmt::fatal("bad ppm header"); + // }); + // }; const listener = match ( tcp::listen(ip::ANY_V4, PORT, tcp::reuseaddr) @@ -65,7 +58,7 @@ export fn main() void = { let state = server_state { connections = [], - pictures = pictures, + pictures = [], listener = listener, running = true, }; @@ -75,7 +68,7 @@ export fn main() void = { for (state.running) { const timeout = time::diff(now, next); - fmt::println("running loop, timeout {}",timeout)!; + fmt::printfln("running loop, timeout {} sec",timeout/time::SECOND)!; loop(&state, timeout); now = time::now(time::clock::MONOTONIC); @@ -87,11 +80,9 @@ export fn main() void = { case void => yield; }; next = time::add(now, save_interval); + unload_distant_chunks(&state); }; }; - - - }; fn loop(state: *server_state, timeout: time::duration) void = { @@ -123,7 +114,14 @@ fn loop(state: *server_state, timeout: time::duration) void = { }); fmt::printfln("there are now {} connections", len(state.connections))!; - greet_connection(state, len(state.connections)); + const conn_idx = len(state.connections) - 1; + match (greet_connection(state, conn_idx)) { + case void => yield; + case let e: io::error => + fmt::printfln("error greeting {}: {}",conn_idx, + io::strerror(e))!; + delete(state.connections[conn_idx]); + }; }; // for each connection, handle packet(s) @@ -131,7 +129,8 @@ fn loop(state: *server_state, timeout: time::duration) void = { // try to look at the nonexistent pollfd for a connection we just accepted for (let pollfd_idx = 1z: size; pollfd_idx < len(pollfds); pollfd_idx += 1) { const conn_idx = pollfd_idx - 1; - fmt::println("checking conn {} with pollfd {}",conn_idx, pollfd_idx)!; + if (state.connections[conn_idx].should_delete) continue; + fmt::printfln("checking conn {} with pollfd {}",conn_idx, pollfd_idx)!; if (0 != pollfds[conn_idx+1].revents & poll::event::POLLIN) { read_from_connection(state, conn_idx); }; @@ -141,6 +140,7 @@ fn loop(state: *server_state, timeout: time::duration) void = { }; +// kicks connections in an error state fn perform_deletions(state: *server_state) void = { for (let i1 = len(state.connections); i1 > 0; i1 -= 1) { const i = i1 - 1; @@ -152,7 +152,9 @@ fn perform_deletions(state: *server_state) void = { }; }; -fn greet_connection(state: *server_state, conn_idx: size) void = void; +fn greet_connection(state: *server_state, conn_idx: size) +(void|io::error) = void; + fn read_from_connection(state: *server_state, conn_idx: size) void = { @@ -166,10 +168,17 @@ fn read_from_connection(state: *server_state, conn_idx: size) void = { conn.should_delete = true; case void => for (true) match (packet_reader::next(&conn.pr)) { - // foreach loop seems to break match exhaustivity here + // xxx foreach loop seems to break match exhaustivity here + // investigate that at some point case done => break; case let p: packet_reader::packet => - handle_packet(state, conn_idx, p); + match (handle_packet(state, conn_idx, p)) { + case void => yield; + case let e: io::error => + fmt::printfln("handle packet error. #{}. {}", + conn_idx, io::strerror(e))!; + conn.should_delete = true; + }; case let e: packet_reader::error => fmt::printfln("packet error {}",e)!; conn.should_delete = true; @@ -181,11 +190,13 @@ fn handle_packet( state: *server_state, conn_idx: size, packet: packet_reader::packet -) void = { +) (void|io::error) = { + const conn = &state.connections[conn_idx]; match (packet) { case let op: packet_reader::packet_drawop => const opc = op as drawing::op_circle; - fmt::printfln("#{}: drawop ({:+6},{:+6})",conn_idx,opc.0,opc.1)!; + fmt::printfln("#{}: drawop ({:+6},{:+6})", + conn_idx,opc.0,opc.1)!; drawing::perform(state.pictures, opc); for (let other_idx = 0z; other_idx < len(state.connections); @@ -203,19 +214,78 @@ fn handle_packet( other_conn.should_delete = true; }; }; + case let pos: packet_reader::packet_position => + fmt::printfln("pos of #{} is now {},{}", + conn_idx, pos.0, pos.1)!; + state.connections[conn_idx].pos = pos; + case let world_pos: packet_reader::packet_reqchunk => + // xxx it is probably easy to ddos the server by sending lots of these + fmt::printfln("#{} requests chunk {},{}", + conn_idx, world_pos.0, world_pos.1)!; + + const pic = ensure_chunk(state, world_pos); + packet_reader::send(conn.sock, packet_reader::packet_sendchunk { + world_pos = world_pos, + chunk_data = pic.d[..pic.w*pic.h], + })?; + case => - fmt::printfln("shouldn't be getting other packets from #{}",conn_idx)!; + fmt::printfln("shouldn't be getting other packets from #{}", + conn_idx)!; state.connections[conn_idx].should_delete = true; }; }; -fn send_world(conn: io::file, pictures: []drawing::picture) (void | io::error) = { - for (const pic &.. pictures) { - fmt::printfln(" sending {},{}",pic.world_pos.0, pic.world_pos.1)!; - packet_reader::send(conn, packet_reader::packet_sendchunk { - world_pos = pic.world_pos, - chunk_data = pic.d[..pic.w*pic.h], - })?; +fn ensure_chunk(state: *server_state, world_pos: pos) *drawing::picture = { + // if that chunk is already loaded, do nothing + // if not, load it from disk, or create it newly if not on disk + // in either case, return it + for (const pic &.. state.pictures) { + if (pic.world_pos.0 == world_pos.0 && pic.world_pos.1 == world_pos.1) { + return pic; + }; }; + const pic = load_picture_from_file(world_pos)!; + append(state.pictures, pic); + return &state.pictures[len(state.pictures)-1]; +}; + +fn floor_div(a: i32, b: i32) i32 = { + if (a < 0) return -1-floor_div(-(a+1),b); + return a / b; }; + +fn unload_distant_chunks(state: *server_state) void = { + const n_pics = len(state.pictures); + let should_unload = alloc([true...], n_pics); + defer free(should_unload); + + for (const conn &.. state.connections) { + const chunk_in_x = floor_div(conn.pos.0, CHUNKSIZE)*CHUNKSIZE; + const chunk_in_y = floor_div(conn.pos.1, CHUNKSIZE)*CHUNKSIZE; + // pic_chunk_pos - player_chunk_pos must be either 0 or 512 or 1024 + // can't be less than 0 or more than 1024 + + for (let i = 0z; i < n_pics; i += 1) { + const pic = &state.pictures[i]; + const dcx = pic.world_pos.0 - chunk_in_x; + const dcy = pic.world_pos.1 - chunk_in_y; + if (0 <= dcx && dcx <= 2*CHUNKSIZE && 0 <= dcy && dcy <= 2*CHUNKSIZE) + should_unload[i] = false; + }; + }; + + for (let i1 = n_pics; i1 > 0; i1 -= 1) { + const i = i1 - 1; + if (should_unload[i]) { + fmt::printfln("unloading {},{}", + state.pictures[i].world_pos.0, + state.pictures[i].world_pos.1)!; + delete(state.pictures[i]); + }; + }; +}; + + + diff --git a/server/save_load.ha b/server/save_load.ha index cf82a0d..db5a18a 100644 --- a/server/save_load.ha +++ b/server/save_load.ha @@ -7,14 +7,13 @@ use os; use strings; use drawing; -use drawing::{pos}; +use drawing::{pos,CHUNKSIZE}; // caller should free return value fn filename_from_world_pos(pos: pos) str = fmt::asprintf("./c.{}.{}.ppm",pos.0,pos.1); fn save_world(state: *server_state) (void | fs::error) = { - fmt::printfln("saving world!")!; for (const pic &.. state.pictures) { const filename = filename_from_world_pos(pic.world_pos); fmt::printfln("\t-> {}",filename)!; @@ -35,6 +34,7 @@ fn save_world(state: *server_state) (void | fs::error) = { buf[3*i+2] = ((px) &0xff): u8; }; io::writeall(file, buf)?; + }; }; diff --git a/todo b/todo index 70c70e8..58de6a7 100644 --- a/todo +++ b/todo @@ -1,4 +1,2 @@ multiple chunks on server -client send position to server -server list of clients, properly server periodically unloads chunks not near any player -- cgit v1.2.3