diff options
Diffstat (limited to 'client.ha')
-rw-r--r-- | client.ha | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/client.ha b/client.ha new file mode 100644 index 0000000..d5093b3 --- /dev/null +++ b/client.ha @@ -0,0 +1,239 @@ +use gl::*; +use glm; +use io; +use mcproto; +use net; +use net::dial; +use net::ip; +use net::tcp; +use strings; +use time; +use trace; +use unix::poll; +use rt; + +def TCP_NODELAY: int = 1; // XXX: linux + +def PROTO_VER = 761i32; // TODO + +type ClientState = enum u8 { + IDLE, + CONNECT, + LOGIN, + JOIN, + GAME, +}; + +let CLIENT_STATE = ClientState::IDLE; + +let CLIENT_ADDR = ""; +let CLIENT_USERNAME = ""; + +let CLIENT_SOCK: net::socket = -1; +// TODO: use tagged union once possible +let CLIENT_RECV: nullable *Receiver = null; +let CLIENT_RBUF: [0x1fffff]u8 = [0...]; + +fn client_connect(addr: str, username: str) void = { + trace::info(&trace::root, "connecting to {}...", addr); + + // TODO: async dns? + let (addrs, port) = match (dial::resolve("tcp", addr, "minecraft")) { + case let res: ([]ip::addr, u16) => + yield res; + case let err: dial::error => + die("resolve {}: {}", addr, dial::strerror(err)); + }; + + // TODO: try secondary addresses + trace::info(&trace::root, "trying {}:{}...", + ip::string(addrs[0]), port); + match (tcp::connect(addrs[0], port, net::sockflag::NONBLOCK)) { + case let sock: net::socket => + // TODO: error handling; + // maybe don't use rt module (if there was an option) + rt::setsockopt(sock, rt::IPPROTO_TCP, TCP_NODELAY, &1i, + size(int): u32)!; + + CLIENT_STATE = ClientState::CONNECT; + CLIENT_SOCK = sock; + CLIENT_ADDR = strings::dup(addr); + CLIENT_USERNAME = strings::dup(username); + case let err: net::error => + die("connect {}: {}", addr, net::strerror(err)); + }; +}; + +fn client_destroy() void = { + match (io::close(CLIENT_SOCK)) { + case void => void; + case let err: io::error => + trace::error(&trace::root, "failed to close socket: {}", + io::strerror(err)): void; + }; + + match (CLIENT_RECV) { + case let recv: *Receiver => + recv_finish(CLIENT_RECV as *Receiver); + free(CLIENT_RECV); + CLIENT_RECV = null; + case null => void; + }; + + if (CLIENT_STATE == ClientState::GAME) { + game_destroy(); + }; + + free(CLIENT_ADDR); + CLIENT_ADDR = ""; + free(CLIENT_USERNAME); + CLIENT_USERNAME = ""; + CLIENT_STATE = ClientState::IDLE; +}; + +fn client_frame() void = { + const t0 = time::now(time::clock::MONOTONIC); + client_update_connection(); + const t1 = time::now(time::clock::MONOTONIC); + //trace::debug(&trace::root, "client_update_connection took {} µs", + // time::diff(t0, t1) / 1000); + + if (CLIENT_STATE == ClientState::GAME) { + game_frame(); + }; +}; + +fn client_update_connection() void = { + for (true) switch (CLIENT_STATE) { + case ClientState::IDLE => + abort(); + case ClientState::CONNECT => + let fds = [poll::pollfd { + fd = CLIENT_SOCK, + events = poll::event::POLLOUT, + ... + }]; + match (poll::poll(fds, 0)) { + case let n: uint => + if (n == 0) { + return; + }; + case let err: poll::error => + die("poll: {}", poll::strerror(err)); + }; + + let err: int = 0; + rt::getsockopt(CLIENT_SOCK, rt::SOL_SOCKET, rt::SO_ERROR, + &err, &(size(int): u32))!; + if (err != 0) { + die("connect {}: {}", CLIENT_ADDR, rt::strerror(err)); + }; + + trace::info(&trace::root, "connection established"); + + CLIENT_RECV = alloc(newrecv(0x200000)); + + CLIENT_STATE = ClientState::LOGIN; + login_start(); + case ClientState::LOGIN, + ClientState::JOIN, + ClientState::GAME => + const recv = CLIENT_RECV as *Receiver; + match (recv_poll(recv, CLIENT_SOCK, CLIENT_RBUF)) { + case let length: size => + let payload = CLIENT_RBUF[..length]; + match (client_handle_packet(payload)) { + case void => void; + case trace::failed => + die("invalid packet"); + }; + case void => + return; + }; + }; +}; + +fn client_handle_packet(payload: []u8) + (void | trace::failed) = { + const tr = trace::ctx(&trace::root, "handle packet"); + const dec = mcproto::Decoder { + input = payload, + pos = 0, + tracer = &tr, + }; + const ctx = mcproto::root(&dec); + const ctx_ = mcproto::context(&ctx, "packet id"); + const packet_id = mcproto::decode_varint(&ctx_)?; + switch (CLIENT_STATE) { + case ClientState::IDLE, ClientState::CONNECT => + abort(); + case ClientState::LOGIN => + login_handle_packet(&ctx, packet_id)?; + case ClientState::JOIN, ClientState::GAME => + switch (packet_id) { + case 0x1f => + const ctx_ = mcproto::context(&ctx, "0x1f keep alive"); + handle_keep_alive(&ctx_)?; + case 0x24 => + if (CLIENT_STATE == ClientState::GAME) { + game_destroy(); + CLIENT_STATE = ClientState::JOIN; + }; + const ctx_ = mcproto::context(&ctx, "0x24 login"); + handle_login(&ctx_)?; + CLIENT_STATE = ClientState::GAME; + case => + if (CLIENT_STATE == ClientState::JOIN) { + return mcproto::error(&ctx, "Login packet expected"); + }; + game_handle_packet(&ctx, packet_id)?; + }; + }; +}; + +fn network_send(packet_id: i32, payload: []u8) void = { + match (mcproto::write_packet(CLIENT_SOCK, packet_id, payload)) { + case void => void; + case let err: io::error => + die("network: error: write: {}", io::strerror(err)); + }; +}; + +const LAYER_CLIENT_WAITING = Layer { + blocks_input = &layer_client_waiting_blocks_input, + input = &layer_client_waiting_input, + is_opaque = &layer_client_waiting_is_opaque, + render = &layer_client_waiting_render, +}; + +fn client_waiting_status() (str | void) = { + switch (CLIENT_STATE) { + case ClientState::CONNECT => + return "Connecting to the server..."; + case ClientState::LOGIN => + return "Logging in..."; + case ClientState::JOIN => + return "Joining world..."; + case ClientState::IDLE, ClientState::GAME => void; + }; +}; + +fn layer_client_waiting_blocks_input() bool = { + return client_waiting_status() is str; +}; + +fn layer_client_waiting_input(event: InputEvent) bool = { + return false; +}; + +fn layer_client_waiting_is_opaque() bool = { + return client_waiting_status() is str; +}; + +fn layer_client_waiting_render() void = { + match (client_waiting_status()) { + case let status: str => + render_wait_screen(status); + case void => void; + }; +}; |