# Hurry Curry! - a game about cooking # Copyright (C) 2025 Hurry Curry! contributors # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, version 3 of the License only. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # class_name Multiplayer extends Node signal packet(packet: Dictionary) signal connection_closed() static var VERSION_MAJOR: int = 12 static var VERSION_MINOR: int = 0 var connected := false var socket: WebSocketPeer var keep_alive := Timer.new() func _ready(): add_child(keep_alive) keep_alive.wait_time = 1. keep_alive.timeout.connect(send_keep_alive) func connect_to_urls(urls: Array[String]): if urls.is_empty(): connection_closed.emit("No connection address available.") return var error_info: Dictionary[String, int] = {} # Create a WebSocketPeer for each url var peers: Array[WebSocketPeer] = [] for url: String in urls: var ws := WebSocketPeer.new() ws.inbound_buffer_size = 1024 * 1024 * 4 var err := ws.connect_to_url(url) if err == OK: peers.append(ws) else: error_info[url] = err # Now keep polling until one of them is succesful, or we run out of peers. # Peers are removed from the peers array when they fail to connect. var open_peer_found := false while not peers.is_empty() and not open_peer_found: await get_tree().physics_frame for peer: WebSocketPeer in peers: peer.poll() var state := peer.get_ready_state() match state: WebSocketPeer.STATE_CLOSED: print("URL %s failed" % peer.get_requested_url()) error_info[peer.get_requested_url()] = peer.get_close_code() peers.erase(peer) WebSocketPeer.STATE_OPEN: # We found a connection that works. Close all others. print("URL %s connected!" % peer.get_requested_url()) socket = peer var other_peers := peers.filter(func (p): return p != peer) for p: WebSocketPeer in other_peers: p.close() open_peer_found = true break _: pass if not open_peer_found: var err_msg: String = tr("c.error.could_not_connect") for url: String in error_info.keys(): err_msg += "\nURL %s failed with code %d" % [url, error_info[url]] connection_closed.emit(err_msg) return connected = true keep_alive.start() func _notification(what): if what == NOTIFICATION_PREDELETE and socket != null: socket.close() connected = false func _process(_delta): if connected: socket.poll() var state = socket.get_ready_state() while socket.get_available_packet_count(): handle_packet(socket.get_packet()) if state == WebSocketPeer.STATE_CLOSED: connection_closed.emit("c.error.connection_closed") connected = false func fix_packet_types(val: Variant): match typeof(val): TYPE_FLOAT: return val TYPE_STRING: return val TYPE_BOOL: return val TYPE_ARRAY: return val.map(fix_packet_types) TYPE_DICTIONARY: var new_dict = {} for k in val.keys(): if val[k] is Array and val[k].size() == 2: # A Vector2 is represented as an array with 2 elements in our protocol. # We need to convert it to Godot's Vector2 type for easier handling. if k in ["tile"]: new_dict[k] = Vector2i(val[k][0], val[k][1]) # TODO: Are these still necessary? elif k in ["pos", "position", "dir"]: new_dict[k] = Vector2(val[k][0], val[k][1]) else: new_dict[k] = fix_packet_types(val[k]) else: new_dict[k] = fix_packet_types(val[k]) return new_dict func handle_packet(coded): var p = decode_packet(coded) if p == null: return p = fix_packet_types(p) match p["type"]: "version": var major = p["major"] var minor = p["minor"] if major != VERSION_MAJOR or minor > VERSION_MINOR: socket.close() connected = false connection_closed.emit(tr("c.error.version_mismatch").format([major, minor, VERSION_MAJOR, VERSION_MINOR])) _: packet.emit(p) func send_join(player_name: String, character_style: Dictionary): send_packet({ "type": "join", "name": player_name, "character": character_style }) func send_movement(player, pos: Vector2, direction: Vector2, boost: bool): send_packet({ "type": "movement", "player": player, "pos": [pos.x, pos.y], "dir": [direction.x, direction.y], "boost": boost }) func send_tile_interact(player, pos: Vector2i, edge: bool, hand: int): @warning_ignore("incompatible_ternary") send_packet({ "type": "interact", "player": player, "target": {"tile": [pos.x, pos.y]} if edge else null, "hand": hand, }) func send_player_interact(player, target_player, target_hand: int, edge: bool, hand: int): @warning_ignore("incompatible_ternary") send_packet({ "type": "interact", "player": player, "target": {"player": [target_player, target_hand]} if edge else null, "hand": hand, }) func send_chat(player, message: String): send_packet({ "type": "communicate", "player": player, "persist": false, "message": { "text": message } }) func send_replay_tick(dt: float): send_packet({ "type": "replay_tick", "dt": dt }) func send_idle(paused: bool): send_packet({ "type": "idle", "paused": paused, }) func send_leave(player): send_packet({ "type": "leave", "player": player, }) func send_ready(): send_packet({ "type": "ready" }) func send_keep_alive() -> void: send_packet({ "type": "keepalive" }) func send_packet(p): var json = JSON.stringify(p) if socket.get_ready_state() != WebSocketPeer.State.STATE_OPEN: push_warning("Can not send packet: Socket not open") return socket.send_text(json) func decode_packet(bytes: PackedByteArray): var json = JSON.new() var in_str = bytes.get_string_from_utf8() var error = json.parse(in_str) if error == OK: return json.data else: print("Decode of packet failed: %s in %s" % [json.get_error_message(), in_str]) return null