From a84a6ce4466aee9fb71ffbd7e5f5fc00a4b6e4c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Mon, 20 Oct 2025 21:56:04 +0200 Subject: [PATCH 01/13] import addons --- addons/nohub.gd/lobby.gd | 10 + addons/nohub.gd/nohub.gd | 2 + addons/nohub.gd/nohub_client.gd | 140 +++++++ addons/nohub.gd/plugin.cfg | 7 + addons/nohub.gd/result.gd | 84 ++++ addons/trimsock.gd/command.gd | 373 ++++++++++++++++++ addons/trimsock.gd/command.gd.uid | 1 + addons/trimsock.gd/conventions.gd | 86 ++++ addons/trimsock.gd/conventions.gd.uid | 1 + addons/trimsock.gd/exchange.gd | 140 +++++++ addons/trimsock.gd/exchange.gd.uid | 1 + .../id/incremental_id_generator.gd | 10 + .../id/incremental_id_generator.gd.uid | 1 + addons/trimsock.gd/id/random_id_generator.gd | 20 + .../trimsock.gd/id/random_id_generator.gd.uid | 1 + addons/trimsock.gd/id_generator.gd | 5 + addons/trimsock.gd/id_generator.gd.uid | 1 + addons/trimsock.gd/line_parser.gd | 112 ++++++ addons/trimsock.gd/line_parser.gd.uid | 1 + addons/trimsock.gd/line_reader.gd | 61 +++ addons/trimsock.gd/line_reader.gd.uid | 1 + addons/trimsock.gd/plugin.cfg | 7 + addons/trimsock.gd/reactor.gd | 132 +++++++ addons/trimsock.gd/reactor.gd.uid | 1 + .../reactors/tcp_client_reactor.gd | 36 ++ .../reactors/tcp_client_reactor.gd.uid | 1 + .../reactors/tcp_server_reactor.gd | 37 ++ .../reactors/tcp_server_reactor.gd.uid | 1 + addons/trimsock.gd/reader.gd | 49 +++ addons/trimsock.gd/reader.gd.uid | 1 + addons/trimsock.gd/trimsock.gd | 2 + addons/trimsock.gd/trimsock.gd.uid | 1 + 32 files changed, 1326 insertions(+) create mode 100644 addons/nohub.gd/lobby.gd create mode 100644 addons/nohub.gd/nohub.gd create mode 100644 addons/nohub.gd/nohub_client.gd create mode 100644 addons/nohub.gd/plugin.cfg create mode 100644 addons/nohub.gd/result.gd create mode 100644 addons/trimsock.gd/command.gd create mode 100644 addons/trimsock.gd/command.gd.uid create mode 100644 addons/trimsock.gd/conventions.gd create mode 100644 addons/trimsock.gd/conventions.gd.uid create mode 100644 addons/trimsock.gd/exchange.gd create mode 100644 addons/trimsock.gd/exchange.gd.uid create mode 100644 addons/trimsock.gd/id/incremental_id_generator.gd create mode 100644 addons/trimsock.gd/id/incremental_id_generator.gd.uid create mode 100644 addons/trimsock.gd/id/random_id_generator.gd create mode 100644 addons/trimsock.gd/id/random_id_generator.gd.uid create mode 100644 addons/trimsock.gd/id_generator.gd create mode 100644 addons/trimsock.gd/id_generator.gd.uid create mode 100644 addons/trimsock.gd/line_parser.gd create mode 100644 addons/trimsock.gd/line_parser.gd.uid create mode 100644 addons/trimsock.gd/line_reader.gd create mode 100644 addons/trimsock.gd/line_reader.gd.uid create mode 100644 addons/trimsock.gd/plugin.cfg create mode 100644 addons/trimsock.gd/reactor.gd create mode 100644 addons/trimsock.gd/reactor.gd.uid create mode 100644 addons/trimsock.gd/reactors/tcp_client_reactor.gd create mode 100644 addons/trimsock.gd/reactors/tcp_client_reactor.gd.uid create mode 100644 addons/trimsock.gd/reactors/tcp_server_reactor.gd create mode 100644 addons/trimsock.gd/reactors/tcp_server_reactor.gd.uid create mode 100644 addons/trimsock.gd/reader.gd create mode 100644 addons/trimsock.gd/reader.gd.uid create mode 100644 addons/trimsock.gd/trimsock.gd create mode 100644 addons/trimsock.gd/trimsock.gd.uid diff --git a/addons/nohub.gd/lobby.gd b/addons/nohub.gd/lobby.gd new file mode 100644 index 00000000..1be23fa0 --- /dev/null +++ b/addons/nohub.gd/lobby.gd @@ -0,0 +1,10 @@ +extends RefCounted +class_name NohubLobby + +var id: String = "" +var is_visible: bool = true +var is_locked: bool = false +var data: Dictionary = {} + +func _to_string() -> String: + return "NohubLobby(id=%s, is_visible=%s, is_locked=%s, data=%s)" % [id, is_visible, is_locked, data] diff --git a/addons/nohub.gd/nohub.gd b/addons/nohub.gd/nohub.gd new file mode 100644 index 00000000..5af87f6c --- /dev/null +++ b/addons/nohub.gd/nohub.gd @@ -0,0 +1,2 @@ +@tool +extends EditorPlugin diff --git a/addons/nohub.gd/nohub_client.gd b/addons/nohub.gd/nohub_client.gd new file mode 100644 index 00000000..04d7ddd3 --- /dev/null +++ b/addons/nohub.gd/nohub_client.gd @@ -0,0 +1,140 @@ +extends RefCounted +class_name NohubClient + + +var _connection: StreamPeerTCP +var _reactor: TrimsockTCPClientReactor + + +func _init(connection: StreamPeerTCP): + _connection = connection + _connection.set_no_delay(true) + + _reactor = TrimsockTCPClientReactor.new(connection) + +func poll() -> void: + _reactor.poll() + +func set_game(id: String) -> NohubResult: + var request := TrimsockCommand.request("session/set-game")\ + .with_params([id]) + return await _bool_request(request) + +func create_lobby(address: String, data: Dictionary) -> NohubResult.Lobby: + var request := TrimsockCommand.request("lobby/create")\ + .with_params([address]) + for key in data: + request.with_kv_pairs([TrimsockCommand.pair_of(key, data[key])]) + + var xchg := _reactor.submit_request(request) + var response := await xchg.read() + + if response.is_success(): + return NohubResult.Lobby.of_value(_command_to_lobby(response)) + else: + return _command_to_error(response) + +func get_lobby(id: String, properties: Array[String] = []) -> NohubResult.Lobby: + var request := TrimsockCommand.request("lobby/get")\ + .with_params([id] + properties) + var xchg := _reactor.submit_request(request) + var response := await xchg.read() + + if response.is_success(): + return NohubResult.Lobby.of_value(_command_to_lobby(response)) + else: + return _command_to_error(response) + +func list_lobbies(fields: Array[String] = []) -> NohubResult.LobbyList: + var result := [] as Array[NohubLobby] + var request := TrimsockCommand.request("lobby/list")\ + .with_params(fields) + + var xchg := _reactor.submit_request(request) + while xchg.is_open(): + var cmd := await xchg.read() + + if cmd.is_error(): + return _command_to_error(cmd) + if not cmd.is_stream_chunk(): + continue + + result.append(_command_to_lobby(cmd)) + + return NohubResult.LobbyList.of_value(result) + +func delete_lobby(lobby_id: String) -> NohubResult: + var request := TrimsockCommand.request("lobby/delete")\ + .with_params([lobby_id]) + return await _bool_request(request) + +func join_lobby(lobby_id: String) -> NohubResult.Address: + var request := TrimsockCommand.request("lobby/join")\ + .with_params([lobby_id]) + + var xchg := _reactor.submit_request(request) + var response := await xchg.read() + + if response.is_success(): + return NohubResult.Address.of_value(response.params[0]) + else: + return _command_to_error(response) + +func lock_lobby(lobby_id: String) -> NohubResult: + var request := TrimsockCommand.request("lobby/lock")\ + .with_params([lobby_id]) + return await _bool_request(request) + +func unlock_lobby(lobby_id: String) -> NohubResult: + var request := TrimsockCommand.request("lobby/unlock")\ + .with_params([lobby_id]) + return await _bool_request(request) + +func hide_lobby(lobby_id: String) -> NohubResult: + var request := TrimsockCommand.request("lobby/hide")\ + .with_params([lobby_id]) + return await _bool_request(request) + +func publish_lobby(lobby_id: String) -> NohubResult: + var request := TrimsockCommand.request("lobby/publish")\ + .with_params([lobby_id]) + return await _bool_request(request) + +func set_lobby_data(lobby_id: String, data: Dictionary) -> NohubResult: + var request := TrimsockCommand.request("lobby/set-data")\ + .with_params([lobby_id])\ + .with_kv_map(data) + return await _bool_request(request) + +func whereami() -> String: + var request := TrimsockCommand.request("whereami") + var xchg := _reactor.submit_request(request) + var response := await xchg.read() + + if response.is_success(): + return response.text + else: + return "" + +func _bool_request(request: TrimsockCommand) -> NohubResult: + var xchg := _reactor.submit_request(request) + var response := await xchg.read() + if response.is_success(): + return NohubResult.of_success() + else: + return _command_to_error(response) + +func _command_to_lobby(command: TrimsockCommand) -> NohubLobby: + var lobby := NohubLobby.new() + lobby.id = command.params[0] + lobby.is_locked = command.params.find("locked", 1) >= 0 + lobby.is_visible = command.params.find("hidden", 1) < 0 + lobby.data = command.kv_map + + return lobby + +func _command_to_error(command: TrimsockCommand) -> NohubResult: + if command.is_error() and command.params.size() >= 2: + return NohubResult.of_error(command.params[0], command.params[1]) + else: + return NohubResult.of_error(command.name, "") diff --git a/addons/nohub.gd/plugin.cfg b/addons/nohub.gd/plugin.cfg new file mode 100644 index 00000000..2e46bf2d --- /dev/null +++ b/addons/nohub.gd/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="nohub.gd" +description="A Godot client for nohub, the open source lobby service" +author="Tamás Gálffy" +version="0.9.0" +script="nohub.gd" diff --git a/addons/nohub.gd/result.gd b/addons/nohub.gd/result.gd new file mode 100644 index 00000000..7207b328 --- /dev/null +++ b/addons/nohub.gd/result.gd @@ -0,0 +1,84 @@ +extends RefCounted +class_name NohubResult + +class ErrorData: + var name: String + var message: String + + func _init(p_name: String, p_message: String): + name = p_name + message = p_message + + func _to_string() -> String: + return "%s: %s" % [name, message] + +class Lobby extends NohubResult: + static func of_value(value: NohubLobby) -> Lobby: + var result := Lobby.new() + result._is_success = true + result._value = value + return result + + func value() -> NohubLobby: + if _is_success: + return _value as NohubLobby + else: + return null + +class LobbyList extends NohubResult: + static func of_value(value: Array[NohubLobby]) -> LobbyList: + var result := LobbyList.new() + result._is_success = true + result._value = value + return result + + func value() -> Array[NohubLobby]: + if _is_success: + return _value as Array[NohubLobby] + else: + return [] + +class Address extends NohubResult: + static func of_value(value: String) -> Address: + var result := Address.new() + result._is_success = true + result._value = value + return result + + func value() -> String: + if _is_success: + return _value as String + else: + return "" + +var _is_success: bool +var _value: Variant +var _error: ErrorData + + +static func of_error(error: String, message: String) -> NohubResult: + var result := NohubResult.new() + result._is_success = false + result._error = ErrorData.new(error, message) + return result + +static func of_success() -> NohubResult: + var result := NohubResult.new() + result._is_success = true + return result + + +func is_success() -> bool: + return _is_success + +func error() -> ErrorData: + if _is_success: + return null + else: + return _error + +func _to_string() -> String: + if _is_success: + return str(_value) + else: + return str(_error) diff --git a/addons/trimsock.gd/command.gd b/addons/trimsock.gd/command.gd new file mode 100644 index 00000000..d0b25213 --- /dev/null +++ b/addons/trimsock.gd/command.gd @@ -0,0 +1,373 @@ +extends RefCounted +class_name TrimsockCommand + +class Chunk: + var text: String + var is_quoted: bool + + static func quoted(p_text: String) -> Chunk: + var chunk := Chunk.new() + chunk.is_quoted = true + chunk.text = p_text + return chunk + + static func unquoted(p_text: String) -> Chunk: + var chunk := Chunk.new() + chunk.is_quoted = false + chunk.text = p_text + return chunk + + static func of_text(p_text: String) -> Chunk: + var chunk := Chunk.new() + chunk.is_quoted = p_text.contains(" ") + chunk.text = p_text + return chunk + +class Pair: + var key: String + var value: String + +enum Type { + SIMPLE, + REQUEST, + SUCCESS_RESPONSE, + ERROR_RESPONSE, + STREAM_CHUNK, + STREAM_FINISH +} + +# Core properties +var name: String = "" +var text: String = "" +var chunks: Array[Chunk] = [] +var is_raw: bool = false +var raw: PackedByteArray + +# Multiparam +var params: Array[String] + +# Key-value pairs +var kv_pairs: Array[Pair] +var kv_map: Dictionary + +# Request-response + Stream +var exchange_id: String +var type: Type = Type.SIMPLE + +static func from_buffer(name: String, data: PackedByteArray) -> TrimsockCommand: + var command := TrimsockCommand.new() + command.is_raw = true + command.raw = data + return command + +static func simple(name: String, text: String = "") -> TrimsockCommand: + var command := TrimsockCommand.new() + command.name = name + if text: + command.chunks.append(Chunk.of_text(text)) + + return command + +static func request(name: String, exchange_id: String = "") -> TrimsockCommand: + var command := TrimsockCommand.new() + command.name = name + command.type = Type.REQUEST + command.exchange_id = exchange_id + return command + +static func success_response(name: String, exchange_id: String = "") -> TrimsockCommand: + var command := TrimsockCommand.new() + command.name = name + command.type = Type.SUCCESS_RESPONSE + command.exchange_id = exchange_id + return command + +static func error_response(name: String, exchange_id: String = "") -> TrimsockCommand: + var command := TrimsockCommand.new() + command.name = name + command.type = Type.ERROR_RESPONSE + command.exchange_id = exchange_id + return command + +static func stream_chunk(name: String, exchange_id: String = "") -> TrimsockCommand: + var command := TrimsockCommand.new() + command.name = name + command.type = Type.STREAM_CHUNK + command.exchange_id = exchange_id + return command + +static func stream_finish(name: String, exchange_id: String = "") -> TrimsockCommand: + var command := TrimsockCommand.new() + command.name = name + command.type = Type.STREAM_FINISH + command.exchange_id = exchange_id + return command + +static func error_from(command: TrimsockCommand, name: String, data) -> TrimsockCommand: + var result := TrimsockCommand.new() + + if not result.is_simple(): + result.name = "" + result.type = Type.ERROR_RESPONSE + result.exchange_id = command.exchange_id + else: + result.name = name + + if typeof(data) == TYPE_ARRAY: + for param in data: + result.params.append(str(param)) + else: + result.chunks.append(Chunk.of_text(str(data))) + + return result + +static func unescape(what: String) -> String: + return (what + .replace("\\n", "\n") + .replace("\\r", "\r") + .replace("\\\"", "\"") + ) + +static func escape_quoted(what: String) -> String: + return what.replace("\"", "\\\"") + +static func escape_unquoted(what: String) -> String: + return (what + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\"", "\\\"") + ) + +static func pair_of(key: String, value: String) -> Pair: + var pair := Pair.new() + pair.key = key + pair.value = value + return pair + +static func type_string(type: Type) -> String: + match type: + Type.SIMPLE: return "Simple" + Type.REQUEST: return "Request" + Type.SUCCESS_RESPONSE: return "Success Response" + Type.ERROR_RESPONSE: return "Error Response" + Type.STREAM_CHUNK: return "Stream Chunk" + Type.STREAM_FINISH: return "Stream Finish" + return "%d???" % [type] + + +func is_simple() -> bool: + return type == Type.SIMPLE + +func is_request() -> bool: + return type == Type.REQUEST + +func is_success() -> bool: + return type == Type.SUCCESS_RESPONSE + +func is_error() -> bool: + return type == Type.ERROR_RESPONSE + +func is_stream() -> bool: + return is_stream_chunk() or is_stream_end() + +func is_stream_chunk() -> bool: + return type == Type.STREAM_CHUNK + +func is_stream_end() -> bool: + return type == Type.STREAM_FINISH + +func is_empty() -> bool: + if is_raw: + return raw.is_empty() + else: + return text.is_empty() and chunks.is_empty() and params.is_empty() and kv_pairs.is_empty() and kv_map.is_empty() + +func clear(): + raw.clear() + chunks.clear() + params.clear() + kv_pairs.clear() + kv_map.clear() + text = "" + +func with_name(p_name: String) -> TrimsockCommand: + name = p_name + return self + +func with_text(p_text: String) -> TrimsockCommand: + text = p_text + return self + +func with_chunks(p_chunks: Array[Chunk]) -> TrimsockCommand: + chunks += p_chunks + return self + +func as_raw() -> TrimsockCommand: + is_raw = true + text = "" + chunks = [] + return self + +func with_data(data: PackedByteArray) -> TrimsockCommand: + as_raw() + raw = data + return self + +func with_params(p_params: Array[String]) -> TrimsockCommand: + params += p_params + return self + +func with_kv_pairs(p_kv_pairs: Array[Pair]) -> TrimsockCommand: + kv_pairs += p_kv_pairs + for pair in kv_pairs: + kv_map[pair.key] = pair.value + return self + +func with_kv_map(p_kv_map: Dictionary) -> TrimsockCommand: + for key in p_kv_map: + var value = p_kv_map[key] + kv_pairs.append(pair_of(key, value)) + kv_map.merge(p_kv_map, true) + return self + +func with_exchange_id(p_exchange_id: String) -> TrimsockCommand: + exchange_id = p_exchange_id + return self + +func as_request() -> TrimsockCommand: + type = Type.REQUEST + return self + +func as_success_response() -> TrimsockCommand: + type = Type.SUCCESS_RESPONSE + return self + +func as_error_response() -> TrimsockCommand: + type = Type.ERROR_RESPONSE + return self + +func as_stream() -> TrimsockCommand: + type = Type.STREAM_FINISH if is_empty() else Type.STREAM_CHUNK + return self + +func serialize() -> PackedByteArray: + var out := PackedByteArray() + serialize_to_array(out) + return out + +func serialize_to_array(out: PackedByteArray) -> void: + var buffer := StreamPeerBuffer.new() + serialize_to_stream(buffer) + out.append_array(buffer.data_array) + +func serialize_to_stream(out: StreamPeer) -> void: + # Add raw marker + if is_raw: + out.put_8(_ord("\r")) + + # Add name + if name: + out.put_data(_escape_name(name).to_utf8_buffer()) + + # Add separator if request / stream + match type: + Type.REQUEST: out.put_u8(_ord("?")) + Type.SUCCESS_RESPONSE: out.put_u8(_ord(".")) + Type.ERROR_RESPONSE: out.put_u8(_ord("!")) + Type.STREAM_CHUNK, Type.STREAM_FINISH: + out.put_u8(_ord("|")) + + # Add ID + if type != Type.SIMPLE: + out.put_data(exchange_id.to_utf8_buffer()) + + # Short-circuit on empty command + if is_empty() and not is_raw: + out.put_u8(_ord("\n")) + return + + # Space after name + out.put_u8(_ord(" ")) + + # Short-circuit if raw + if is_raw: + out.put_data(str(raw.size()).to_ascii_buffer()) + out.put_u8(_ord("\n")) + out.put_data(raw) + out.put_u8(_ord("\n")) + return + + # Print content + if not chunks.is_empty(): + # Prefer chunks, if available + for chunk in chunks: + if chunk.is_quoted: + out.put_data(_quoted_chunk(chunk.text).to_utf8_buffer()) + else: + out.put_data(_unquoted_chunk(chunk.text).to_utf8_buffer()) + elif not kv_pairs.is_empty() or not kv_map.is_empty() or not params.is_empty(): + # Fall back to params if no chunks + var tokens := PackedStringArray() + + # Print params first + for param in params: + tokens.append(_autoquoted_chunk(param)) + + # Print kv-params, either from `kv_pairs`, or `kv_map` + if not kv_pairs.is_empty(): + for pair in kv_pairs: + tokens.append(_autoquoted_chunk(pair.key) + "=" + _autoquoted_chunk(pair.value)) + else: + for key in kv_map: + var value = kv_map[key] + tokens.append(_autoquoted_chunk(key) + "=" + _autoquoted_chunk(value)) + + # Push to buffer + out.put_data(" ".join(tokens).to_utf8_buffer()) + else: + # Use `text` as last resort + out.put_data(_autoquoted_chunk(text).to_utf8_buffer()) + + # Add closing NL + out.put_u8(_ord("\n")) + +func equals(what) -> bool: + if not what is TrimsockCommand: + return false + + var command := what as TrimsockCommand + + if not command.name == name or \ + not command.type == type: + return false + + if not is_simple() and exchange_id != command.exchange_id: + return false + + if not is_raw: + return text == command.text + else: + return raw == command.raw + +func _ord(chr: String) -> int: + return chr.unicode_at(0) + +func _escape_name(what: String) -> String: + return _autoquoted_chunk(what) + +func _quoted_chunk(what: String) -> String: + return "\"%s\"" % [escape_quoted(what)] + +func _unquoted_chunk(what: String) -> String: + return escape_unquoted(what) + +func _autoquoted_chunk(what: String) -> String: + if what.contains(" "): + return _quoted_chunk(what) + else: + return _unquoted_chunk(what) + +func _to_string() -> String: + if is_raw: + return "(raw)" + serialize().get_string_from_utf8() + return serialize().get_string_from_utf8() diff --git a/addons/trimsock.gd/command.gd.uid b/addons/trimsock.gd/command.gd.uid new file mode 100644 index 00000000..cfda87c9 --- /dev/null +++ b/addons/trimsock.gd/command.gd.uid @@ -0,0 +1 @@ +uid://cdylwggrco2m6 diff --git a/addons/trimsock.gd/conventions.gd b/addons/trimsock.gd/conventions.gd new file mode 100644 index 00000000..fc9c9eac --- /dev/null +++ b/addons/trimsock.gd/conventions.gd @@ -0,0 +1,86 @@ +extends Object +class_name _TrimsockConventions + + +static func apply(command: TrimsockCommand) -> void: + parse_type(command) + parse_params(command) + +static func parse_type(command: TrimsockCommand) -> void: + var at := 0 + + # Figure out command type + while true: + at = command.name.find("?") + if at >= 0: + command.type = TrimsockCommand.Type.REQUEST + break + + at = command.name.find(".") + if at >= 0: + command.type = TrimsockCommand.Type.SUCCESS_RESPONSE + break + + at = command.name.find("!") + if at >= 0: + command.type = TrimsockCommand.Type.ERROR_RESPONSE + break + + at = command.name.find("|") + if at >= 0: + if ((command.is_raw and command.raw.is_empty()) or (not command.is_raw and command.text.is_empty())): + command.type = TrimsockCommand.Type.STREAM_FINISH + else: + command.type = TrimsockCommand.Type.STREAM_CHUNK + break + return + + # Extract data + var name := command.name.substr(0, at) + var id := command.name.substr(at + 1) + + command.name = name + command.exchange_id = id + +static func parse_params(command: TrimsockCommand) -> void: + if command.is_raw or command.chunks.is_empty(): + return + + var chunks := [] as Array[String] + for chunk in command.chunks: + if chunk.is_quoted: + # Quoted chunks go in verbatim + chunks.append(chunk.text) + else: + # Unquoted chunks are separated by spaces, and then each separated + # word is checked for equal signs + for word in chunk.text.split(" ", false): + var at := word.find("=") + if at >= 0: + chunks.append(word.substr(0, at)) + chunks.append("=") + chunks.append(word.substr(at + 1)) + else: + chunks.append(word) + chunks = chunks.filter(func(it): return it) + + # Extract params and kv-pairs + for i in range(chunks.size()): + var chunk := chunks[i] + var prev := chunks[i-1] if i > 0 else "" + var next := chunks[i+1] if i < chunks.size() - 1 else "" + + if next == "=" or prev == "=": + continue + if chunk == "=" and prev and next: + command.kv_pairs.append(TrimsockCommand.pair_of(prev, next)) + else: + command.params.append(chunk) + + # Calculate kv-map + if not command.kv_pairs.is_empty(): + for pair in command.kv_pairs: + command.kv_map[pair.key] = pair.value + +func _init(): + assert(false, "This class shouldn't be instantiated!") diff --git a/addons/trimsock.gd/conventions.gd.uid b/addons/trimsock.gd/conventions.gd.uid new file mode 100644 index 00000000..b4440a3e --- /dev/null +++ b/addons/trimsock.gd/conventions.gd.uid @@ -0,0 +1 @@ +uid://do2ws0pyjmpvs diff --git a/addons/trimsock.gd/exchange.gd b/addons/trimsock.gd/exchange.gd new file mode 100644 index 00000000..8933f800 --- /dev/null +++ b/addons/trimsock.gd/exchange.gd @@ -0,0 +1,140 @@ +extends RefCounted +class_name TrimsockExchange + +var _source: Variant +var _reactor: TrimsockReactor +var _command: TrimsockCommand + +var _is_open: bool = true +var _queue: Array[TrimsockCommand] = [] + + +signal _on_command(command: TrimsockCommand) + + +func _init(command: TrimsockCommand, source: Variant, reactor: TrimsockReactor): + _command = command + _source = source + _reactor = reactor + +#region Properties +func source() -> Variant: + return _source + +func id() -> String: + return _command.exchange_id + +func session() -> Variant: + return _reactor.get_session(_source) + +func set_session(data: Variant) -> void: + _reactor.set_session(_source, data) + +func is_open() -> bool: + return _is_open + +func can_reply() -> bool: + return _command.type != TrimsockCommand.Type.SIMPLE + +func close() -> void: + _is_open = false +#endregion + +#region Write +func send(command: TrimsockCommand) -> bool: + if not is_open(): + return false + + _reactor._write(_source, command) + return true + +func send_and_close(command: TrimsockCommand) -> bool: + if not send(command): + return false + + close() + return true + +func reply(command: TrimsockCommand) -> bool: + if not can_reply() or not is_open(): + return false + + command.as_success_response() + command.name = "" + command.exchange_id = id() + + send(command) + close() + return true + +func fail(command: TrimsockCommand) -> bool: + if not can_reply() or not is_open(): + return false + + command.as_error_response() + command.name = "" + command.exchange_id = id() + + send(command) + close() + return true + +func stream(command: TrimsockCommand) -> bool: + if not can_reply() or not is_open(): + return false + + command.as_stream() + command.name = "" + command.exchange_id = id() + + send(command) + return true + +func stream_finish(command: TrimsockCommand) -> bool: + if not can_reply() or not is_open(): + return false + + command.clear() + command.as_stream() + command.name = "" + command.exchange_id = id() + + send(command) + close() + return true + +func reply_or_send(command: TrimsockCommand) -> bool: + if not is_open(): + return false + + if not reply(command): + send_and_close(command) + + return true + +func fail_or_send(command: TrimsockCommand) -> bool: + if not is_open(): + return false + + if not fail(command): + send_and_close(command) + + return true +#endregion + +#region Read +func push(command: TrimsockCommand) -> void: + match command.type: + TrimsockCommand.Type.SUCCESS_RESPONSE,\ + TrimsockCommand.Type.ERROR_RESPONSE,\ + TrimsockCommand.Type.STREAM_FINISH: + close() + + _queue.append(command) + _on_command.emit(command) + +func read() -> TrimsockCommand: + while _queue.is_empty(): + await _on_command + return _queue.pop_front() +#endregion diff --git a/addons/trimsock.gd/exchange.gd.uid b/addons/trimsock.gd/exchange.gd.uid new file mode 100644 index 00000000..54252795 --- /dev/null +++ b/addons/trimsock.gd/exchange.gd.uid @@ -0,0 +1 @@ +uid://6n8wdgeod2wt diff --git a/addons/trimsock.gd/id/incremental_id_generator.gd b/addons/trimsock.gd/id/incremental_id_generator.gd new file mode 100644 index 00000000..2031985e --- /dev/null +++ b/addons/trimsock.gd/id/incremental_id_generator.gd @@ -0,0 +1,10 @@ +extends TrimsockIDGenerator +class_name IncrementalTrimsockIDGenerator + + +var _at := -1 + + +func get_id() -> String: + _at += 1 + return "%x" % _at diff --git a/addons/trimsock.gd/id/incremental_id_generator.gd.uid b/addons/trimsock.gd/id/incremental_id_generator.gd.uid new file mode 100644 index 00000000..4855753d --- /dev/null +++ b/addons/trimsock.gd/id/incremental_id_generator.gd.uid @@ -0,0 +1 @@ +uid://ckr74u3yx2182 diff --git a/addons/trimsock.gd/id/random_id_generator.gd b/addons/trimsock.gd/id/random_id_generator.gd new file mode 100644 index 00000000..dceebae5 --- /dev/null +++ b/addons/trimsock.gd/id/random_id_generator.gd @@ -0,0 +1,20 @@ +extends TrimsockIDGenerator +class_name RandomTrimsockIDGenerator + + +var charset := "abcdeghijklmnopqrstuvwxyz" + "ABCDEFGHIJLKMNOPQRSTUVWXYZ" + "0123456789" +var length := 8 + +var _rng := RandomNumberGenerator.new() + + +func _init(p_length: int = 8, p_charset: String = ""): + length = p_length + if p_charset: + charset = p_charset + +func get_id() -> String: + var id := "" + for i in length: + id += charset[_rng.randi() % charset.length()] + return id diff --git a/addons/trimsock.gd/id/random_id_generator.gd.uid b/addons/trimsock.gd/id/random_id_generator.gd.uid new file mode 100644 index 00000000..a4afc19f --- /dev/null +++ b/addons/trimsock.gd/id/random_id_generator.gd.uid @@ -0,0 +1 @@ +uid://xafiylpfvpnj diff --git a/addons/trimsock.gd/id_generator.gd b/addons/trimsock.gd/id_generator.gd new file mode 100644 index 00000000..549a2e6a --- /dev/null +++ b/addons/trimsock.gd/id_generator.gd @@ -0,0 +1,5 @@ +extends RefCounted +class_name TrimsockIDGenerator + +func get_id() -> String: + return "" diff --git a/addons/trimsock.gd/id_generator.gd.uid b/addons/trimsock.gd/id_generator.gd.uid new file mode 100644 index 00000000..5e2e5165 --- /dev/null +++ b/addons/trimsock.gd/id_generator.gd.uid @@ -0,0 +1 @@ +uid://cu054kqjijxe diff --git a/addons/trimsock.gd/line_parser.gd b/addons/trimsock.gd/line_parser.gd new file mode 100644 index 00000000..5dc0e868 --- /dev/null +++ b/addons/trimsock.gd/line_parser.gd @@ -0,0 +1,112 @@ +extends RefCounted +class_name _TrimsockLineParser + +var line := "" +var at := 0 + +func parse(p_line: String) -> TrimsockCommand: + rewind(p_line) + var command := TrimsockCommand.new() + + # Empty command + if is_eol(): + return command + + # Check for raw message + command.is_raw = chr() == "\r" + if command.is_raw: at += 1 + + # Read command name + command.name = read_name() + at += 1 # Skip over space after name + + # Read chunks until available + while not is_eol(): + command.chunks.append(read_chunk()) + + # Calculate text + command.text = "" + for chunk in command.chunks: + command.text += chunk.text + + unescape(command) + + return command + +func read_name() -> String: + if chr() == "\"": + return read_quoted() + else: + return read_identifier() + +func read_chunk() -> TrimsockCommand.Chunk: + var chunk := TrimsockCommand.Chunk.new() + if chr() == "\"": + chunk.is_quoted = true + chunk.text = read_quoted() + else: + chunk.is_quoted = false + chunk.text = read_unquoted() + return chunk + +func read_identifier() -> String: + var from := at + + while not is_eol() and chr() != " ": + at += 1 + + return line.substr(from, at - from) + +func read_unquoted() -> String: + var from := at + + while not is_eol(): + if chr() == "\\": + at += 1 + elif chr() == "\n" or chr() == "\"": + break + at += 1 + + return line.substr(from, at - from) + +func read_quoted() -> String: + var from := at + + # Skip opening quote + at += 1 + + # Iterate until end + while true: + if chr() == "\\": + # Skip escape + at += 1 + elif chr() == "\"": + # Found closing quote, stop + break + elif is_eol(): + # String ended unexpectedly + push_warning("Command line ended unexpectedly while reading quoted data: " + line) + break + at += 1 + + # Step over closing quotes + at += 1 + + # Return string between quotes + return line.substr(from + 1, (at - 1) - (from + 1)) + +func unescape(command: TrimsockCommand) -> void: + command.name = TrimsockCommand.unescape(command.name) + for chunk in command.chunks: + chunk.text = TrimsockCommand.unescape(chunk.text) + command.text = TrimsockCommand.unescape(command.text) + +func chr() -> String: + return line[at] + +func is_eol() -> bool: + return at >= line.length() + +func rewind(p_line: String) -> void: + line = p_line + at = 0 diff --git a/addons/trimsock.gd/line_parser.gd.uid b/addons/trimsock.gd/line_parser.gd.uid new file mode 100644 index 00000000..9431b8b5 --- /dev/null +++ b/addons/trimsock.gd/line_parser.gd.uid @@ -0,0 +1 @@ +uid://b2e1q00pni4ud diff --git a/addons/trimsock.gd/line_reader.gd b/addons/trimsock.gd/line_reader.gd new file mode 100644 index 00000000..fabb6bb0 --- /dev/null +++ b/addons/trimsock.gd/line_reader.gd @@ -0,0 +1,61 @@ +extends RefCounted +class_name _TrimsockLineReader + +var buffer := PackedByteArray() +var max_size := 16384 +var at := 0 +var is_quote := false +var is_escape := false + +func ingest(data: PackedByteArray) -> Error: + var new_size := buffer.size() + data.size() + if new_size > max_size: + buffer.clear() + return ERR_OUT_OF_MEMORY + + buffer.append_array(data) + return OK + +func read_text() -> String: + while not is_eob(): + if is_escape: + is_escape = false + elif chr() == "\"": + is_quote = not is_quote + elif chr() == "\\": + is_escape = true + elif chr() == "\n" and not is_quote: + return _flush_line() + at += 1 + return "" + +func has_data(size: int) -> bool: + return buffer.size() >= size + +func read_data(size: int) -> PackedByteArray: + assert(has_data(size), "Trying to read more bytes than available!") + + # Grab result + var result := buffer.slice(0, size) + buffer = buffer.slice(size) + + # Reset flags + is_escape = false + is_quote = false + + return result + +func chr() -> String: + return String.chr(buffer[at]) + +func is_eob() -> bool: + return at >= buffer.size() + +# Return string up to the current character ( exclusive ), and discard +# everything before ( inclusive ) the current character +func _flush_line() -> String: + var line := buffer.slice(0, at).get_string_from_utf8() + buffer = buffer.slice(at + 1) + at = 0 + + return line diff --git a/addons/trimsock.gd/line_reader.gd.uid b/addons/trimsock.gd/line_reader.gd.uid new file mode 100644 index 00000000..02e1298d --- /dev/null +++ b/addons/trimsock.gd/line_reader.gd.uid @@ -0,0 +1 @@ +uid://cycjokupoirxj diff --git a/addons/trimsock.gd/plugin.cfg b/addons/trimsock.gd/plugin.cfg new file mode 100644 index 00000000..b60b59ea --- /dev/null +++ b/addons/trimsock.gd/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="trimsock.gd" +description="Godot implementation of the trimsock protocol" +author="Tamás Gálffy" +version="0.12.0" +script="trimsock.gd" diff --git a/addons/trimsock.gd/reactor.gd b/addons/trimsock.gd/reactor.gd new file mode 100644 index 00000000..b2389ab3 --- /dev/null +++ b/addons/trimsock.gd/reactor.gd @@ -0,0 +1,132 @@ +extends RefCounted +class_name TrimsockReactor + +var _sources: Array = [] +var _sessions: Dictionary = {} # source to session data +var _readers: Dictionary = {} # source to reader +var _handlers: Dictionary = {} # command name to handler method +var _exchanges: Array[TrimsockExchange] = [] +var _unknown_handler: Callable = func(_cmd, _xchg): pass # TODO(trimsock): Add _xchg param +var _id_generator: TrimsockIDGenerator = RandomTrimsockIDGenerator.new(12) + + +signal on_attach(source: Variant) +signal on_detach(source: Variant) + + +func poll() -> void: + _poll() + + for source in _sources: + var reader := _readers[source] as TrimsockReader + while true: + var command := reader.read() + if not command: + break + + _handle(command, source) + +func send(target: Variant, command: TrimsockCommand) -> TrimsockExchange: + # Send command + _write(target, command) + + # Ensure exchange + var xchg := _get_exchange_for(command, target) + if xchg == null: + xchg = _make_exchange_for(command, target) + + return xchg + +func request(target: Variant, command: TrimsockCommand) -> TrimsockExchange: + command.as_request() + if not command.exchange_id: + command.exchange_id = _id_generator.get_id() + return send(target, command) + +func stream(target: Variant, command: TrimsockCommand) -> TrimsockExchange: + command.as_stream() + if not command.exchange_id: + command.exchange_id = _id_generator.get_id() + return send(target, command) + +func attach(source: Variant) -> void: + if _sources.has(source): + return + + _sources.append(source) + _readers[source] = TrimsockReader.new() + on_attach.emit(source) + +func detach(source: Variant) -> void: + if not _sources.has(source): + return + + _sources.erase(source) + _sessions.erase(source) + _readers.erase(source) + on_detach.emit(source) + +func set_session(source: Variant, data: Variant) -> void: + _sessions[source] = data + +func get_session(source: Variant) -> Variant: + return _sessions.get(source) + +func set_id_generator(id_generator: TrimsockIDGenerator) -> void: + _id_generator = id_generator + +func on(command_name: String, handler: Callable) -> TrimsockReactor: + _handlers[command_name] = handler + return self + +func on_unknown(handler: Callable) -> TrimsockReactor: + _unknown_handler = handler + return self + + +# Grab incoming data, call `_ingest()` +func _poll() -> void: + pass + +# Send command to target +func _write(target: Variant, command: TrimsockCommand) -> void: + pass + +func _ingest(source: Variant, data: PackedByteArray) -> Error: + assert(_readers.has(source), "Ingesting data from unknown source! Did you call `attach()`?") + var reader := _readers[source] as TrimsockReader + return reader.ingest_bytes(data) + +func _handle(command: TrimsockCommand, source: Variant) -> void: + var xchg := _get_exchange_for(command, source) + if xchg != null: + # Known exchange, handle it there + xchg.push(command) + else: + # New exchange, create instance and pass to handler + xchg = _make_exchange_for(command, source) + var handler := (_handlers.get(command.name) if _handlers.has(command.name) else _unknown_handler) as Callable + + var result := await handler.call(command, xchg) + if xchg.is_open() and result is TrimsockCommand: + xchg.send_and_close(result) + + # Free exchange if needed + if not xchg.is_open(): + _exchanges.erase(xchg) + +func _get_exchange_for(command: TrimsockCommand, source: Variant) -> TrimsockExchange: + if not command.is_simple(): + # Try and find known exchange + for xchg in _exchanges: + if xchg.id() == command.exchange_id and xchg._source == source: + return xchg + + # Command has no ID, or ID not found + return null + +func _make_exchange_for(command: TrimsockCommand, source: Variant) -> TrimsockExchange: + var xchg := TrimsockExchange.new(command, source, self) + if not command.is_simple(): + _exchanges.append(xchg) + return xchg diff --git a/addons/trimsock.gd/reactor.gd.uid b/addons/trimsock.gd/reactor.gd.uid new file mode 100644 index 00000000..64b32e49 --- /dev/null +++ b/addons/trimsock.gd/reactor.gd.uid @@ -0,0 +1 @@ +uid://cn2bot4vb3q24 diff --git a/addons/trimsock.gd/reactors/tcp_client_reactor.gd b/addons/trimsock.gd/reactors/tcp_client_reactor.gd new file mode 100644 index 00000000..25a176cd --- /dev/null +++ b/addons/trimsock.gd/reactors/tcp_client_reactor.gd @@ -0,0 +1,36 @@ +extends TrimsockReactor +class_name TrimsockTCPClientReactor + +var _connection: StreamPeerTCP + + +func _init(connection: StreamPeerTCP): + _connection = connection + attach(_connection) + +func submit(command: TrimsockCommand) -> TrimsockExchange: + return send(_connection, command) + +func submit_request(command: TrimsockCommand) -> TrimsockExchange: + return request(_connection, command) + +func submit_stream(command: TrimsockCommand) -> TrimsockExchange: + return stream(_connection, command) + +func _poll() -> void: + _connection.poll() + + if _connection.get_status() != StreamPeerTCP.STATUS_CONNECTED: + # Can't read + return + + # Grab available data + var available := _connection.get_available_bytes() + var res := _connection.get_partial_data(available) + if res[0] == OK: + _ingest(_connection, res[1]) + +func _write(target: Variant, command: TrimsockCommand) -> void: + assert(target is StreamPeerTCP, "Invalid target!") + var peer := target as StreamPeerTCP + command.serialize_to_stream(peer) diff --git a/addons/trimsock.gd/reactors/tcp_client_reactor.gd.uid b/addons/trimsock.gd/reactors/tcp_client_reactor.gd.uid new file mode 100644 index 00000000..31a8aecf --- /dev/null +++ b/addons/trimsock.gd/reactors/tcp_client_reactor.gd.uid @@ -0,0 +1 @@ +uid://baw6mwso5an43 diff --git a/addons/trimsock.gd/reactors/tcp_server_reactor.gd b/addons/trimsock.gd/reactors/tcp_server_reactor.gd new file mode 100644 index 00000000..93bd3a1b --- /dev/null +++ b/addons/trimsock.gd/reactors/tcp_server_reactor.gd @@ -0,0 +1,37 @@ +extends TrimsockReactor +class_name TrimsockTCPServerReactor + +var _server: TCPServer + +func _init(server: TCPServer): + _server = server + +func _poll() -> void: + # Handle incoming connections + while _server.is_connection_available(): + attach(_server.take_connection()) + + # Poll each connection + for source in _sources: + var stream := source as StreamPeerTCP + + # Update status + stream.poll() + + # Detach closed connections + # Don't process any further data from them if we can't reply + var status := stream.get_status() + if status == StreamPeerTCP.STATUS_NONE or status == StreamPeerTCP.STATUS_ERROR: + detach(stream) + continue + + # Grab available data + var available := stream.get_available_bytes() + var res := stream.get_partial_data(available) + if res[0] == OK: + _ingest(stream, res[1]) + +func _write(target: Variant, command: TrimsockCommand) -> void: + assert(target is StreamPeerTCP, "Invalid target!") + var peer := target as StreamPeerTCP + command.serialize_to_stream(peer) diff --git a/addons/trimsock.gd/reactors/tcp_server_reactor.gd.uid b/addons/trimsock.gd/reactors/tcp_server_reactor.gd.uid new file mode 100644 index 00000000..40296208 --- /dev/null +++ b/addons/trimsock.gd/reactors/tcp_server_reactor.gd.uid @@ -0,0 +1 @@ +uid://bstql6w3f5fnu diff --git a/addons/trimsock.gd/reader.gd b/addons/trimsock.gd/reader.gd new file mode 100644 index 00000000..48d00d42 --- /dev/null +++ b/addons/trimsock.gd/reader.gd @@ -0,0 +1,49 @@ +extends RefCounted +class_name TrimsockReader + +var _line_reader: _TrimsockLineReader = _TrimsockLineReader.new() +var _line_parser: _TrimsockLineParser = _TrimsockLineParser.new() +var _queued_raw: TrimsockCommand = null + +func ingest_text(text: String) -> Error: + return _line_reader.ingest(text.to_utf8_buffer()) + +func ingest_bytes(bytes: PackedByteArray) -> Error: + return _line_reader.ingest(bytes) + +func read() -> TrimsockCommand: + var command := _pop() + if command: + _TrimsockConventions.apply(command) + return command + +func _pop() -> TrimsockCommand: + # We read a raw command earlier, waiting to have enough data + if _queued_raw: + var data_size := int(_queued_raw.text) + if not _line_reader.has_data(data_size): + return null + + _queued_raw.raw = _line_reader.read_data(data_size) + _queued_raw.text = "" + _queued_raw.chunks.clear() + + var result := _queued_raw + _queued_raw = null + return result + + # No queued command, try to read a new one + var line := _line_reader.read_text() + if not line: + return null + + var command := _line_parser.parse(line) + if command.is_raw: + # Command is raw, we'll keep it in the queue until we read the binary + # data for it + _queued_raw = command + + # Try getting it immediately, in case we already have the data in buffer + return _pop() + + return command diff --git a/addons/trimsock.gd/reader.gd.uid b/addons/trimsock.gd/reader.gd.uid new file mode 100644 index 00000000..b1852a34 --- /dev/null +++ b/addons/trimsock.gd/reader.gd.uid @@ -0,0 +1 @@ +uid://ch6fw168jf1jt diff --git a/addons/trimsock.gd/trimsock.gd b/addons/trimsock.gd/trimsock.gd new file mode 100644 index 00000000..5af87f6c --- /dev/null +++ b/addons/trimsock.gd/trimsock.gd @@ -0,0 +1,2 @@ +@tool +extends EditorPlugin diff --git a/addons/trimsock.gd/trimsock.gd.uid b/addons/trimsock.gd/trimsock.gd.uid new file mode 100644 index 00000000..a69927f6 --- /dev/null +++ b/addons/trimsock.gd/trimsock.gd.uid @@ -0,0 +1 @@ +uid://nb3mrm43j6ux From 7dc1cd6ead28a2a3445dd13e2ca0b9473a2cbbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Tue, 21 Oct 2025 11:43:07 +0200 Subject: [PATCH 02/13] ui --- examples/forest-brawl/menu.tscn | 380 ++++++++++++++++++++++++++++++++ project.godot | 15 +- 2 files changed, 392 insertions(+), 3 deletions(-) create mode 100644 examples/forest-brawl/menu.tscn diff --git a/examples/forest-brawl/menu.tscn b/examples/forest-brawl/menu.tscn new file mode 100644 index 00000000..8356d367 --- /dev/null +++ b/examples/forest-brawl/menu.tscn @@ -0,0 +1,380 @@ +[gd_scene load_steps=12 format=3 uid="uid://dbnx63lgo7288"] + +[ext_resource type="Texture2D" uid="uid://cdb8di7e1p6h6" path="res://icon.png" id="1_6gbgt"] +[ext_resource type="Script" path="res://examples/forest-brawl/scripts/random-name-input.gd" id="1_h5ah4"] +[ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd" id="2_llkcl"] +[ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/vsync-checkbutton.gd" id="3_amefg"] +[ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd" id="4_s5qgy"] +[ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/volume-slider.gd" id="5_5ifsx"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wbjcp"] +bg_color = Color(0.25, 0.25, 0.25, 0.752941) +corner_radius_top_left = 2 +corner_radius_top_right = 2 +corner_radius_bottom_right = 2 +corner_radius_bottom_left = 2 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1tayi"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_2kkly"] + +[sub_resource type="Theme" id="Theme_87epe"] +EmbossLabel/styles/normal = SubResource("StyleBoxFlat_wbjcp") +MainMenuButton/base_type = &"Button" +MainMenuButton/colors/font_color = Color(1, 1, 1, 0.752941) +MainMenuButton/colors/font_focus_color = Color(1, 1, 1, 0.878431) +MainMenuButton/colors/font_hover_color = Color(1, 1, 1, 0.878431) +MainMenuButton/colors/font_hover_pressed_color = Color(1, 1, 1, 1) +MainMenuButton/font_sizes/font_size = 48 +MainMenuButton/styles/focus = SubResource("StyleBoxEmpty_1tayi") +MainMenuButton/styles/pressed = SubResource("StyleBoxEmpty_2kkly") +MarginContainer/constants/margin_bottom = 4 +MarginContainer/constants/margin_left = 4 +MarginContainer/constants/margin_right = 4 +MarginContainer/constants/margin_top = 4 + +[sub_resource type="GDScript" id="GDScript_43xrg"] +script/source = "extends Button + +@onready var target: Control = self.get_parent_control().get_child(0) +@onready var panel: Control = self.get_node(\"../..\") + +func _pressed(): + if target.visible: + target.hide() + text = \">\" + else: + target.show() + text = \"<\" + panel.size_flags_horizontal ^= Control.SIZE_EXPAND +" + +[node name="Menu" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme = SubResource("Theme_87epe") + +[node name="Main" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +alignment = 1 + +[node name="Quick Play Button" type="Button" parent="Main"] +layout_mode = 2 +theme_type_variation = &"MainMenuButton" +text = "Quick Play" +flat = true + +[node name="Find a Game Button" type="Button" parent="Main"] +layout_mode = 2 +theme_type_variation = &"MainMenuButton" +text = "Find a Game" +flat = true + +[node name="Settings Button" type="Button" parent="Main"] +layout_mode = 2 +theme_type_variation = &"MainMenuButton" +text = "Settings" +flat = true + +[node name="Quit Button" type="Button" parent="Main"] +layout_mode = 2 +theme_type_variation = &"MainMenuButton" +text = "Quit" +flat = true + +[node name="Quick Play" type="VBoxContainer" parent="."] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +alignment = 1 + +[node name="Label" type="Label" parent="Quick Play"] +layout_mode = 2 +theme_type_variation = &"HeaderLarge" +text = "Searching..." +horizontal_alignment = 1 + +[node name="Spinner" type="TextureRect" parent="Quick Play"] +layout_mode = 2 +texture = ExtResource("1_6gbgt") +stretch_mode = 5 + +[node name="Games" type="Control" parent="."] +visible = false +custom_minimum_size = Vector2(960, 540) +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -20.0 +offset_top = -20.0 +offset_right = 20.0 +offset_bottom = 20.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="MarginContainer" type="MarginContainer" parent="Games"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="Games/MarginContainer"] +layout_mode = 2 + +[node name="PanelContainer" type="PanelContainer" parent="Games/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer"] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="VBoxContainer2" type="GridContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer"] +layout_mode = 2 +columns = 2 + +[node name="Label3" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +layout_mode = 2 +text = "Presets:" + +[node name="OptionButton" type="OptionButton" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +layout_mode = 2 +item_count = 1 +selected = 0 +popup/item_0/text = "foxssake.studio" +popup/item_0/id = 0 + +[node name="Label" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +layout_mode = 2 +text = "noray:" + +[node name="LineEdit" type="LineEdit" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "foxssake.studio:8890" + +[node name="Label2" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +layout_mode = 2 +text = "nohub:" + +[node name="LineEdit2" type="LineEdit" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +layout_mode = 2 +text = "foxssake.studio:12980" + +[node name="Button" type="Button" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +layout_mode = 2 +text = "Connect" + +[node name="Button" type="Button" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer"] +layout_mode = 2 +text = "<" +script = SubResource("GDScript_43xrg") + +[node name="PanelContainer2" type="PanelContainer" parent="Games/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_stretch_ratio = 2.0 + +[node name="MarginContainer2" type="MarginContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer2"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Title Label" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer"] +layout_mode = 2 +theme_type_variation = &"HeaderMedium" +text = "Games" +horizontal_alignment = 1 + +[node name="ScrollContainer" type="ScrollContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Lobbies Container" type="GridContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +columns = 3 + +[node name="Name Header" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_type_variation = &"EmbossLabel" +text = "Name" + +[node name="Players Header" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] +layout_mode = 2 +theme_type_variation = &"EmbossLabel" +text = "Players" + +[node name="Tail Header" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] +layout_mode = 2 +theme_type_variation = &"EmbossLabel" +text = " " + +[node name="Label" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] +layout_mode = 2 +text = "Cool Lobby" + +[node name="Label2" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] +layout_mode = 2 +text = "8 / 16" + +[node name="Button" type="Button" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] +layout_mode = 2 +text = "Join" + +[node name="GridContainer" type="GridContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer"] +layout_mode = 2 +columns = 2 + +[node name="Name Label" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer"] +layout_mode = 2 +text = "Lobby name" + +[node name="Player Limit Label" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer"] +layout_mode = 2 +text = "Player Limit" + +[node name="Lobby Name Input" type="LineEdit" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Lobby Player Limit Input" type="LineEdit" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer"] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 4 + +[node name="Back Button" type="Button" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Back" + +[node name="Host Button" type="Button" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Host Game" + +[node name="Settings" type="PanelContainer" parent="."] +visible = false +custom_minimum_size = Vector2(640, 360) +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="MarginContainer" type="MarginContainer" parent="Settings"] +layout_mode = 2 + +[node name="Settings VBox" type="VBoxContainer" parent="Settings/MarginContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Player Name" type="HBoxContainer" parent="Settings/MarginContainer/Settings VBox"] +layout_mode = 2 + +[node name="Player Name Label" type="Label" parent="Settings/MarginContainer/Settings VBox/Player Name"] +layout_mode = 2 +text = "Player Name:" + +[node name="Player Name Input" type="LineEdit" parent="Settings/MarginContainer/Settings VBox/Player Name"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Nameless Brawler" +clear_button_enabled = true +script = ExtResource("1_h5ah4") + +[node name="Button" type="Button" parent="Settings/MarginContainer/Settings VBox/Player Name"] +layout_mode = 2 +text = "RNG" + +[node name="HSeparator" type="HSeparator" parent="Settings/MarginContainer/Settings VBox"] +layout_mode = 2 + +[node name="GridContainer" type="GridContainer" parent="Settings/MarginContainer/Settings VBox"] +layout_mode = 2 +columns = 8 + +[node name="Fullscreen Label" type="Label" parent="Settings/MarginContainer/Settings VBox/GridContainer"] +layout_mode = 2 +text = "Fullscreen:" + +[node name="Fullscreen CheckButton" type="CheckButton" parent="Settings/MarginContainer/Settings VBox/GridContainer"] +layout_mode = 2 +script = ExtResource("2_llkcl") + +[node name="V-Sync Label" type="Label" parent="Settings/MarginContainer/Settings VBox/GridContainer"] +layout_mode = 2 +text = "V-Sync:" + +[node name="V-Sync CheckButton" type="CheckButton" parent="Settings/MarginContainer/Settings VBox/GridContainer"] +layout_mode = 2 +script = ExtResource("3_amefg") + +[node name="Confine Mouse Label" type="Label" parent="Settings/MarginContainer/Settings VBox/GridContainer"] +layout_mode = 2 +text = "Confine mouse:" + +[node name="Confine Mouse CheckButton" type="CheckButton" parent="Settings/MarginContainer/Settings VBox/GridContainer"] +layout_mode = 2 +script = ExtResource("4_s5qgy") + +[node name="HSeparator2" type="HSeparator" parent="Settings/MarginContainer/Settings VBox"] +layout_mode = 2 + +[node name="Volume" type="HBoxContainer" parent="Settings/MarginContainer/Settings VBox"] +layout_mode = 2 + +[node name="Volume Label" type="Label" parent="Settings/MarginContainer/Settings VBox/Volume"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Volume:" + +[node name="Volume Slider" type="HSlider" parent="Settings/MarginContainer/Settings VBox/Volume"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +size_flags_stretch_ratio = 3.0 +value = 100.0 +script = ExtResource("5_5ifsx") + +[node name="HBoxContainer" type="HBoxContainer" parent="Settings/MarginContainer/Settings VBox"] +layout_mode = 2 +size_flags_vertical = 10 +alignment = 1 + +[node name="Save Button" type="Button" parent="Settings/MarginContainer/Settings VBox/HBoxContainer"] +layout_mode = 2 +text = "Save" + +[node name="Cancel Button" type="Button" parent="Settings/MarginContainer/Settings VBox/HBoxContainer"] +layout_mode = 2 +text = "Cancel" diff --git a/project.godot b/project.godot index 6ec07406..b98a55e8 100644 --- a/project.godot +++ b/project.godot @@ -33,19 +33,28 @@ NetworkSimulator="*res://addons/netfox.extras/network-simulator.gd" [display] -window/size/viewport_width=540 -window/size/viewport_height=540 +window/size/viewport_width=1280 +window/size/viewport_height=720 +window/size/window_width_override=540 +window/size/window_height_override=540 +window/stretch/mode="canvas_items" +window/stretch/aspect="expand" window/vsync/vsync_mode=0 [editor_plugins] -enabled=PackedStringArray("res://addons/netfox.extras/plugin.cfg", "res://addons/netfox.internals/plugin.cfg", "res://addons/netfox.noray/plugin.cfg", "res://addons/netfox/plugin.cfg", "res://addons/vest/plugin.cfg") +enabled=PackedStringArray("res://addons/netfox.extras/plugin.cfg", "res://addons/netfox.internals/plugin.cfg", "res://addons/netfox.noray/plugin.cfg", "res://addons/netfox/plugin.cfg", "res://addons/nohub.gd/plugin.cfg", "res://addons/trimsock.gd/plugin.cfg", "res://addons/vest/plugin.cfg") [filesystem] import/blender/enabled=false import/fbx/enabled=false +[gui] + +theme/default_font_multichannel_signed_distance_field=true +theme/default_font_generate_mipmaps=true + [input] move_west={ From 17f9ebfb6bd1c20235041236caa01c3ca07bc9ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Tue, 21 Oct 2025 20:49:28 +0200 Subject: [PATCH 03/13] list lobbies --- examples/forest-brawl/forest-brawl.tscn | 6 +- examples/forest-brawl/menu.tscn | 518 +++++++++++++----- examples/forest-brawl/ui/menu-theme.tres | 33 ++ .../player-stat-label.tres | 0 4 files changed, 428 insertions(+), 129 deletions(-) create mode 100644 examples/forest-brawl/ui/menu-theme.tres rename examples/forest-brawl/{ui-settings => ui}/player-stat-label.tres (100%) diff --git a/examples/forest-brawl/forest-brawl.tscn b/examples/forest-brawl/forest-brawl.tscn index f93ea239..c9f77f39 100644 --- a/examples/forest-brawl/forest-brawl.tscn +++ b/examples/forest-brawl/forest-brawl.tscn @@ -6,7 +6,6 @@ [ext_resource type="PackedScene" uid="uid://wi4owat0bml3" path="res://examples/forest-brawl/scenes/brawler.tscn" id="7_tcy3g"] [ext_resource type="PackedScene" uid="uid://bpf1jdr255nr0" path="res://examples/shared/ui/time-display.tscn" id="9_d2tot"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/score-manager.gd" id="9_vxjwh"] -[ext_resource type="LabelSettings" uid="uid://b4u1aluftkajy" path="res://examples/forest-brawl/ui-settings/player-stat-label.tres" id="10_0ix7v"] [ext_resource type="Script" path="res://addons/netfox/tick-interpolator.gd" id="10_ld676"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/vsync-checkbutton.gd" id="11_4x74a"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/random-name-input.gd" id="11_cf8pu"] @@ -18,6 +17,7 @@ [ext_resource type="PackedScene" uid="uid://ojh5xofoserg" path="res://examples/forest-brawl/scenes/score_screen.tscn" id="14_85lvt"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd" id="14_h1iqv"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/volume-slider.gd" id="16_6pky3"] +[ext_resource type="LabelSettings" uid="uid://b4u1aluftkajy" path="res://examples/forest-brawl/ui/player-stat-label.tres" id="17_48up8"] [sub_resource type="LabelSettings" id="LabelSettings_l686d"] font_size = 64 @@ -322,13 +322,13 @@ layout_mode = 2 [node name="Score Label" type="Label" parent="UI/Player stats/VBoxContainer/Score HBox"] layout_mode = 2 text = "Score:" -label_settings = ExtResource("10_0ix7v") +label_settings = ExtResource("17_48up8") [node name="Score Value" type="Label" parent="UI/Player stats/VBoxContainer/Score HBox"] layout_mode = 2 text = "8 " -label_settings = ExtResource("10_0ix7v") +label_settings = ExtResource("17_48up8") [node name="Joining Screen" type="Control" parent="UI"] visible = false diff --git a/examples/forest-brawl/menu.tscn b/examples/forest-brawl/menu.tscn index 8356d367..afff9dcb 100644 --- a/examples/forest-brawl/menu.tscn +++ b/examples/forest-brawl/menu.tscn @@ -2,51 +2,306 @@ [ext_resource type="Texture2D" uid="uid://cdb8di7e1p6h6" path="res://icon.png" id="1_6gbgt"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/random-name-input.gd" id="1_h5ah4"] +[ext_resource type="Theme" uid="uid://cg8p4yow3i3ly" path="res://examples/forest-brawl/ui/menu-theme.tres" id="1_xmgyy"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd" id="2_llkcl"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/vsync-checkbutton.gd" id="3_amefg"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd" id="4_s5qgy"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/volume-slider.gd" id="5_5ifsx"] -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wbjcp"] -bg_color = Color(0.25, 0.25, 0.25, 0.752941) -corner_radius_top_left = 2 -corner_radius_top_right = 2 -corner_radius_bottom_right = 2 -corner_radius_bottom_left = 2 - -[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1tayi"] - -[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_2kkly"] - -[sub_resource type="Theme" id="Theme_87epe"] -EmbossLabel/styles/normal = SubResource("StyleBoxFlat_wbjcp") -MainMenuButton/base_type = &"Button" -MainMenuButton/colors/font_color = Color(1, 1, 1, 0.752941) -MainMenuButton/colors/font_focus_color = Color(1, 1, 1, 0.878431) -MainMenuButton/colors/font_hover_color = Color(1, 1, 1, 0.878431) -MainMenuButton/colors/font_hover_pressed_color = Color(1, 1, 1, 1) -MainMenuButton/font_sizes/font_size = 48 -MainMenuButton/styles/focus = SubResource("StyleBoxEmpty_1tayi") -MainMenuButton/styles/pressed = SubResource("StyleBoxEmpty_2kkly") -MarginContainer/constants/margin_bottom = 4 -MarginContainer/constants/margin_left = 4 -MarginContainer/constants/margin_right = 4 -MarginContainer/constants/margin_top = 4 - -[sub_resource type="GDScript" id="GDScript_43xrg"] -script/source = "extends Button - -@onready var target: Control = self.get_parent_control().get_child(0) -@onready var panel: Control = self.get_node(\"../..\") - -func _pressed(): - if target.visible: - target.hide() - text = \">\" +[sub_resource type="GDScript" id="GDScript_jmdql"] +script/source = "extends Control + +@onready var quick_play_button := $\"Quick Play Button\" as Button +@onready var find_game_button := $\"Find a Game Button\" as Button +@onready var play_lan_button := $\"Play LAN Button\" as Button +@onready var settings_button := $\"Settings Button\" as Button +@onready var quit_button := $\"Quit Button\" as Button + +@onready var quick_play_menu := %\"Quick Play Menu\" as Control +@onready var games_menu := %\"Games Menu\" as Control +@onready var settings_menu := %\"Settings Menu\" as Control + +func _ready() -> void: + quick_play_button.pressed.connect(_quick_play) + find_game_button.pressed.connect(_find_game) + play_lan_button.pressed.connect(_play_lan) + settings_button.pressed.connect(_settings) + quit_button.pressed.connect(_quit) + +func _quick_play() -> void: + _switch_to(quick_play_menu) + +func _find_game() -> void: + _switch_to(games_menu) + +func _play_lan() -> void: + pass + +func _settings() -> void: + _switch_to(settings_menu) + +func _quit() -> void: + get_tree().quit() + +# TODO: Deduplicate? +func _switch_to(menu: Control) -> void: + menu.show() + hide() +" + +[sub_resource type="GDScript" id="GDScript_pewsl"] +script/source = "extends Control + +@onready var label := $Label as Label +@onready var back_button := $\"HBoxContainer/Back Button\" as Button +@onready var host_button := $\"HBoxContainer/Host Button\" as Button + +@onready var main_menu := %\"Main Menu\" as Control + +func _ready() -> void: + visibility_changed.connect(func(): + if visible: _execute() + else: _cancel() + ) + + back_button.pressed.connect(_back) + host_button.pressed.connect(_host) + +func _execute() -> void: + pass # TODO + +func _cancel() -> void: + pass # TODO + +func _back() -> void: + _switch_to(main_menu) + +func _host() -> void: + pass # TODO + +# TODO: Deduplicate? +func _switch_to(menu: Control) -> void: + menu.show() + hide() +" + +[sub_resource type="GDScript" id="GDScript_2payw"] +script/source = "extends Control + +class Preset: + var name: String + var noray_address: String + var nohub_address: String + + func _init(p_name: String, p_noray_address: String, p_nohub_address: String): + name = p_name + noray_address = p_noray_address + nohub_address = p_nohub_address + +@onready var presets_option := $\"MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/Presets Option\" as OptionButton +@onready var noray_input := $\"MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/noray Input\" as LineEdit +@onready var nohub_input := $\"MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/nohub Input\" as LineEdit +@onready var connect_button := $\"MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/Connect Button\" as Button +@onready var status_label := $\"MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/Status Label\" as Label +@onready var dock_button := $\"MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/Dock Button\" as Button + +@onready var lobbies_container := $\"MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container\" as GridContainer +@onready var lobby_name_input := $\"MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer/Lobby Name Input\" as LineEdit +@onready var lobby_player_limit_input := $\"MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer/Lobby Player Limit Input\" as LineEdit +@onready var back_button := $\"MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/HBoxContainer/Back Button\" as Button +@onready var host_button := $\"MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/HBoxContainer/Host Button\" as Button + +@onready var dock_container := $MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer as Control +@onready var dock_panel := $MarginContainer/HBoxContainer/PanelContainer + +@onready var main_menu := %\"Main Menu\" as Control + +var presets: Array[Preset] = [ + Preset.new(\"foxssake.studio\", \"foxssake.studio:8890\", \"foxssake.studio:12980\"), + Preset.new(\"localhost\", \"localhost:8890\", \"localhost:9980\") +] + +var _nohub_peer: StreamPeerTCP +var _nohub_client: NohubClient +var _poll_interval := 2. +var _poll_wait := 0. + +var _nohub_game_id := \"WK6koYfZ7cEMjcsba3ovxQF1lM9XjkWh\" + +func _ready() -> void: + # TODO: Deduplicate? + visibility_changed.connect(func(): + if visible: _execute() + else: _cancel() + ) + + presets_option.item_selected.connect(_select_preset) + connect_button.pressed.connect(_connect) + dock_button.pressed.connect(_dock) + back_button.pressed.connect(_back) + host_button.pressed.connect(_host) + + # Populate presets + presets_option.clear() + for preset in presets: + presets_option.add_item(preset.name) + presets_option.select(0) + _select_preset(0) + +func _process(dt: float) -> void: + # Update status + if _nohub_peer == null: + status_label.text = \"Offline\" else: - target.show() - text = \"<\" - panel.size_flags_horizontal ^= Control.SIZE_EXPAND + _nohub_peer.poll() + match _nohub_peer.get_status(): + StreamPeerTCP.STATUS_CONNECTED: status_label.text = \"Online\" + StreamPeerTCP.STATUS_CONNECTING: status_label.text = \"Connecting...\" + StreamPeerTCP.STATUS_ERROR: status_label.text = \"Error\" + + _poll_wait -= dt + if _nohub_client: + _nohub_client.poll() + if _poll_wait < 0.: + print(\"Listing lobbies...\") + _poll_wait = _poll_interval + var response := await _nohub_client.list_lobbies() + if not response.is_success(): + print(\"Failed listing lobbies: %s\" % [response]) + else: + print(\"Found lobbies: %s\" % [response.value()]) + _render_lobbies(response.value()) + +func _execute() -> void: + _connect.call_deferred() + +func _cancel() -> void: + _disconnect.call_deferred() + _render_lobbies([]) + +func _select_preset(idx: int) -> void: + var preset := presets[idx] + noray_input.text = preset.noray_address + nohub_input.text = preset.nohub_address + +func _connect() -> void: + _disconnect() + + var noray_address = _parse_address(noray_input.text, 8890) + var nohub_address = _parse_address(nohub_input.text, 12980) + + # Connect to noray + print(\"Connecting to noray at %s:%d...\" % [noray_address[0], noray_address[1]]) + var err := await Noray.connect_to_host(noray_address[0], noray_address[1]) + if err != OK: + print(\"Failed to connect to noray: %s\" % [error_string(err)]) + return + print(\"Success!\") + + # Connect to nohub + print(\"Connecting to nohub at %s:%d...\" % [noray_address[0], noray_address[1]]) + var peer := StreamPeerTCP.new() + peer.connect_to_host(nohub_address[0], nohub_address[1]) + while true: + peer.poll() + match peer.get_status(): + StreamPeerTCP.STATUS_CONNECTED: + print(\"Success!\") + break + StreamPeerTCP.STATUS_ERROR: + print(\"Failed to connect!\") + return + await get_tree().process_frame + + _nohub_peer = peer + _nohub_client = NohubClient.new(peer) + + var response := await _nohub_client.set_game(_nohub_game_id) + if not response.is_success(): + print(\"Failed to set game ID! %s\" % [response]) + _disconnect() + return + +func _disconnect() -> void: + _render_lobbies([]) + + if Noray.is_connected_to_host(): + Noray.disconnect_from_host() + + if _nohub_peer != null: + _nohub_peer.disconnect_from_host() + _nohub_peer = null + _nohub_client = null + +func _dock() -> void: + if dock_container.visible: + dock_container.hide() + dock_button.text = \">\" + else: + dock_container.show() + dock_button.text = \"<\" + dock_panel.size_flags_horizontal ^= Control.SIZE_EXPAND + +func _back() -> void: + _switch_to(main_menu) + +func _host() -> void: + pass # TODO + +# TODO: Deduplicate? +func _switch_to(menu: Control) -> void: + menu.show() + hide() + +func _render_lobbies(lobbies: Array[NohubLobby]) -> void: + # Clear container, retain header + var children := lobbies_container.get_children() + for i in range(lobbies_container.columns, lobbies_container.get_child_count()): + children[i].queue_free() + + # Render list + for lobby in lobbies: + var name_label := Label.new() + name_label.text = lobby.data.get(\"name\", \"???\") + + var players_label := Label.new() + players_label.text = \"%s / %s\" % [lobby.data.get(\"player-count\", \"?\"), lobby.data.get(\"player-capacity\", \"?\")] + + var join_button := Button.new() + join_button.text = \">\" + join_button.tooltip_text = \"Join this lobby\" + + lobbies_container.add_child(name_label) + lobbies_container.add_child(players_label) + lobbies_container.add_child(join_button) + +func _parse_address(address: String, default_port: int = 0) -> Array: + var result = [\"\", default_port] + if address.contains(\":\"): + var idx := address.rfind(\":\") + result[0] = address.substr(0, idx) + result[1] = int(address.substr(idx + 1)) + else: + result[0] = address + return result +" + +[sub_resource type="GDScript" id="GDScript_xlfi6"] +script/source = "extends Control + +@onready var confirm_button := $\"MarginContainer/Settings VBox/HBoxContainer/Confirm Button\" as Button +@onready var main_menu := %\"Main Menu\" as Control + +func _ready() -> void: + confirm_button.pressed.connect(_confirm) + +func _confirm() -> void: + _switch_to(main_menu) + +# TODO: Deduplicate? +func _switch_to(menu: Control) -> void: + menu.show() + hide() " [node name="Menu" type="Control"] @@ -56,9 +311,11 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -theme = SubResource("Theme_87epe") +theme = ExtResource("1_xmgyy") -[node name="Main" type="VBoxContainer" parent="."] +[node name="Main Menu" type="VBoxContainer" parent="."] +unique_name_in_owner = true +visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -66,32 +323,40 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 alignment = 1 +script = SubResource("GDScript_jmdql") -[node name="Quick Play Button" type="Button" parent="Main"] +[node name="Quick Play Button" type="Button" parent="Main Menu"] layout_mode = 2 theme_type_variation = &"MainMenuButton" text = "Quick Play" flat = true -[node name="Find a Game Button" type="Button" parent="Main"] +[node name="Find a Game Button" type="Button" parent="Main Menu"] layout_mode = 2 theme_type_variation = &"MainMenuButton" text = "Find a Game" flat = true -[node name="Settings Button" type="Button" parent="Main"] +[node name="Play LAN Button" type="Button" parent="Main Menu"] +layout_mode = 2 +theme_type_variation = &"MainMenuButton" +text = "Play on LAN" +flat = true + +[node name="Settings Button" type="Button" parent="Main Menu"] layout_mode = 2 theme_type_variation = &"MainMenuButton" text = "Settings" flat = true -[node name="Quit Button" type="Button" parent="Main"] +[node name="Quit Button" type="Button" parent="Main Menu"] layout_mode = 2 theme_type_variation = &"MainMenuButton" text = "Quit" flat = true -[node name="Quick Play" type="VBoxContainer" parent="."] +[node name="Quick Play Menu" type="VBoxContainer" parent="."] +unique_name_in_owner = true visible = false layout_mode = 1 anchors_preset = 15 @@ -100,20 +365,35 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 alignment = 1 +script = SubResource("GDScript_pewsl") -[node name="Label" type="Label" parent="Quick Play"] +[node name="Label" type="Label" parent="Quick Play Menu"] layout_mode = 2 theme_type_variation = &"HeaderLarge" text = "Searching..." horizontal_alignment = 1 -[node name="Spinner" type="TextureRect" parent="Quick Play"] +[node name="Spinner" type="TextureRect" parent="Quick Play Menu"] layout_mode = 2 texture = ExtResource("1_6gbgt") stretch_mode = 5 -[node name="Games" type="Control" parent="."] -visible = false +[node name="HBoxContainer" type="HBoxContainer" parent="Quick Play Menu"] +layout_mode = 2 +alignment = 1 + +[node name="Back Button" type="Button" parent="Quick Play Menu/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +text = "Back" + +[node name="Host Button" type="Button" parent="Quick Play Menu/HBoxContainer"] +layout_mode = 2 +text = "Host" + +[node name="Games Menu" type="Control" parent="."] +unique_name_in_owner = true custom_minimum_size = Vector2(960, 540) layout_mode = 1 anchors_preset = 8 @@ -127,8 +407,9 @@ offset_right = 20.0 offset_bottom = 20.0 grow_horizontal = 2 grow_vertical = 2 +script = SubResource("GDScript_2payw") -[node name="MarginContainer" type="MarginContainer" parent="Games"] +[node name="MarginContainer" type="MarginContainer" parent="Games Menu"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -136,150 +417,138 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="HBoxContainer" type="HBoxContainer" parent="Games/MarginContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="Games Menu/MarginContainer"] layout_mode = 2 -[node name="PanelContainer" type="PanelContainer" parent="Games/MarginContainer/HBoxContainer"] +[node name="PanelContainer" type="PanelContainer" parent="Games Menu/MarginContainer/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 -[node name="HBoxContainer" type="HBoxContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer"] layout_mode = 2 -[node name="MarginContainer" type="MarginContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer"] +[node name="MarginContainer" type="MarginContainer" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 -[node name="VBoxContainer2" type="GridContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer"] +[node name="VBoxContainer" type="GridContainer" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer"] layout_mode = 2 columns = 2 -[node name="Label3" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +[node name="Presets Label" type="Label" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer"] layout_mode = 2 text = "Presets:" -[node name="OptionButton" type="OptionButton" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +[node name="Presets Option" type="OptionButton" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer"] layout_mode = 2 -item_count = 1 -selected = 0 -popup/item_0/text = "foxssake.studio" -popup/item_0/id = 0 +allow_reselect = true -[node name="Label" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +[node name="noray Label" type="Label" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer"] layout_mode = 2 text = "noray:" -[node name="LineEdit" type="LineEdit" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +[node name="noray Input" type="LineEdit" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 -text = "foxssake.studio:8890" -[node name="Label2" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +[node name="nohub Label" type="Label" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer"] layout_mode = 2 text = "nohub:" -[node name="LineEdit2" type="LineEdit" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +[node name="nohub Input" type="LineEdit" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer"] layout_mode = 2 -text = "foxssake.studio:12980" -[node name="Button" type="Button" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer2"] +[node name="Connect Button" type="Button" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer"] layout_mode = 2 text = "Connect" -[node name="Button" type="Button" parent="Games/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer"] +[node name="Status Label" type="Label" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "Status: " + +[node name="Dock Button" type="Button" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer/HBoxContainer"] layout_mode = 2 text = "<" -script = SubResource("GDScript_43xrg") -[node name="PanelContainer2" type="PanelContainer" parent="Games/MarginContainer/HBoxContainer"] +[node name="PanelContainer2" type="PanelContainer" parent="Games Menu/MarginContainer/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 size_flags_stretch_ratio = 2.0 -[node name="MarginContainer2" type="MarginContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer2"] +[node name="MarginContainer2" type="MarginContainer" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2"] layout_mode = 2 size_flags_horizontal = 3 -[node name="VBoxContainer" type="VBoxContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2"] +[node name="VBoxContainer" type="VBoxContainer" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2"] layout_mode = 2 size_flags_horizontal = 3 -[node name="Title Label" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer"] +[node name="Title Label" type="Label" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer"] layout_mode = 2 theme_type_variation = &"HeaderMedium" text = "Games" horizontal_alignment = 1 -[node name="ScrollContainer" type="ScrollContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer"] +[node name="ScrollContainer" type="ScrollContainer" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer"] layout_mode = 2 size_flags_vertical = 3 -[node name="Lobbies Container" type="GridContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer"] +[node name="Lobbies Container" type="GridContainer" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer"] layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 columns = 3 -[node name="Name Header" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] +[node name="Name Header" type="Label" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] layout_mode = 2 size_flags_horizontal = 3 theme_type_variation = &"EmbossLabel" text = "Name" -[node name="Players Header" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] +[node name="Players Header" type="Label" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] layout_mode = 2 theme_type_variation = &"EmbossLabel" text = "Players" -[node name="Tail Header" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] +[node name="Tail Header" type="Label" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] +custom_minimum_size = Vector2(32, 0) layout_mode = 2 theme_type_variation = &"EmbossLabel" text = " " -[node name="Label" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] -layout_mode = 2 -text = "Cool Lobby" - -[node name="Label2" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] -layout_mode = 2 -text = "8 / 16" - -[node name="Button" type="Button" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/ScrollContainer/Lobbies Container"] -layout_mode = 2 -text = "Join" - -[node name="GridContainer" type="GridContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer"] +[node name="GridContainer" type="GridContainer" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer"] layout_mode = 2 columns = 2 -[node name="Name Label" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer"] +[node name="Name Label" type="Label" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer"] layout_mode = 2 text = "Lobby name" -[node name="Player Limit Label" type="Label" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer"] +[node name="Player Limit Label" type="Label" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer"] layout_mode = 2 text = "Player Limit" -[node name="Lobby Name Input" type="LineEdit" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer"] +[node name="Lobby Name Input" type="LineEdit" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer"] layout_mode = 2 size_flags_horizontal = 3 -[node name="Lobby Player Limit Input" type="LineEdit" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer"] +[node name="Lobby Player Limit Input" type="LineEdit" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/GridContainer"] layout_mode = 2 -[node name="HBoxContainer" type="HBoxContainer" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer"] layout_mode = 2 size_flags_horizontal = 4 -[node name="Back Button" type="Button" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/HBoxContainer"] +[node name="Back Button" type="Button" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/HBoxContainer"] layout_mode = 2 text = "Back" -[node name="Host Button" type="Button" parent="Games/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/HBoxContainer"] +[node name="Host Button" type="Button" parent="Games Menu/MarginContainer/HBoxContainer/PanelContainer2/MarginContainer2/VBoxContainer/HBoxContainer"] layout_mode = 2 text = "Host Game" -[node name="Settings" type="PanelContainer" parent="."] +[node name="Settings Menu" type="PanelContainer" parent="."] +unique_name_in_owner = true visible = false custom_minimum_size = Vector2(640, 360) layout_mode = 1 @@ -290,75 +559,76 @@ anchor_right = 0.5 anchor_bottom = 0.5 grow_horizontal = 2 grow_vertical = 2 +script = SubResource("GDScript_xlfi6") -[node name="MarginContainer" type="MarginContainer" parent="Settings"] +[node name="MarginContainer" type="MarginContainer" parent="Settings Menu"] layout_mode = 2 -[node name="Settings VBox" type="VBoxContainer" parent="Settings/MarginContainer"] +[node name="Settings VBox" type="VBoxContainer" parent="Settings Menu/MarginContainer"] layout_mode = 2 size_flags_vertical = 3 -[node name="Player Name" type="HBoxContainer" parent="Settings/MarginContainer/Settings VBox"] +[node name="Player Name" type="HBoxContainer" parent="Settings Menu/MarginContainer/Settings VBox"] layout_mode = 2 -[node name="Player Name Label" type="Label" parent="Settings/MarginContainer/Settings VBox/Player Name"] +[node name="Player Name Label" type="Label" parent="Settings Menu/MarginContainer/Settings VBox/Player Name"] layout_mode = 2 text = "Player Name:" -[node name="Player Name Input" type="LineEdit" parent="Settings/MarginContainer/Settings VBox/Player Name"] +[node name="Player Name Input" type="LineEdit" parent="Settings Menu/MarginContainer/Settings VBox/Player Name"] layout_mode = 2 size_flags_horizontal = 3 text = "Nameless Brawler" clear_button_enabled = true script = ExtResource("1_h5ah4") -[node name="Button" type="Button" parent="Settings/MarginContainer/Settings VBox/Player Name"] +[node name="Button" type="Button" parent="Settings Menu/MarginContainer/Settings VBox/Player Name"] layout_mode = 2 text = "RNG" -[node name="HSeparator" type="HSeparator" parent="Settings/MarginContainer/Settings VBox"] +[node name="HSeparator" type="HSeparator" parent="Settings Menu/MarginContainer/Settings VBox"] layout_mode = 2 -[node name="GridContainer" type="GridContainer" parent="Settings/MarginContainer/Settings VBox"] +[node name="GridContainer" type="GridContainer" parent="Settings Menu/MarginContainer/Settings VBox"] layout_mode = 2 columns = 8 -[node name="Fullscreen Label" type="Label" parent="Settings/MarginContainer/Settings VBox/GridContainer"] +[node name="Fullscreen Label" type="Label" parent="Settings Menu/MarginContainer/Settings VBox/GridContainer"] layout_mode = 2 text = "Fullscreen:" -[node name="Fullscreen CheckButton" type="CheckButton" parent="Settings/MarginContainer/Settings VBox/GridContainer"] +[node name="Fullscreen CheckButton" type="CheckButton" parent="Settings Menu/MarginContainer/Settings VBox/GridContainer"] layout_mode = 2 script = ExtResource("2_llkcl") -[node name="V-Sync Label" type="Label" parent="Settings/MarginContainer/Settings VBox/GridContainer"] +[node name="V-Sync Label" type="Label" parent="Settings Menu/MarginContainer/Settings VBox/GridContainer"] layout_mode = 2 text = "V-Sync:" -[node name="V-Sync CheckButton" type="CheckButton" parent="Settings/MarginContainer/Settings VBox/GridContainer"] +[node name="V-Sync CheckButton" type="CheckButton" parent="Settings Menu/MarginContainer/Settings VBox/GridContainer"] layout_mode = 2 script = ExtResource("3_amefg") -[node name="Confine Mouse Label" type="Label" parent="Settings/MarginContainer/Settings VBox/GridContainer"] +[node name="Confine Mouse Label" type="Label" parent="Settings Menu/MarginContainer/Settings VBox/GridContainer"] layout_mode = 2 text = "Confine mouse:" -[node name="Confine Mouse CheckButton" type="CheckButton" parent="Settings/MarginContainer/Settings VBox/GridContainer"] +[node name="Confine Mouse CheckButton" type="CheckButton" parent="Settings Menu/MarginContainer/Settings VBox/GridContainer"] layout_mode = 2 script = ExtResource("4_s5qgy") -[node name="HSeparator2" type="HSeparator" parent="Settings/MarginContainer/Settings VBox"] +[node name="HSeparator2" type="HSeparator" parent="Settings Menu/MarginContainer/Settings VBox"] layout_mode = 2 -[node name="Volume" type="HBoxContainer" parent="Settings/MarginContainer/Settings VBox"] +[node name="Volume" type="HBoxContainer" parent="Settings Menu/MarginContainer/Settings VBox"] layout_mode = 2 -[node name="Volume Label" type="Label" parent="Settings/MarginContainer/Settings VBox/Volume"] +[node name="Volume Label" type="Label" parent="Settings Menu/MarginContainer/Settings VBox/Volume"] layout_mode = 2 size_flags_horizontal = 3 text = "Volume:" -[node name="Volume Slider" type="HSlider" parent="Settings/MarginContainer/Settings VBox/Volume"] +[node name="Volume Slider" type="HSlider" parent="Settings Menu/MarginContainer/Settings VBox/Volume"] layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 4 @@ -366,15 +636,11 @@ size_flags_stretch_ratio = 3.0 value = 100.0 script = ExtResource("5_5ifsx") -[node name="HBoxContainer" type="HBoxContainer" parent="Settings/MarginContainer/Settings VBox"] +[node name="HBoxContainer" type="HBoxContainer" parent="Settings Menu/MarginContainer/Settings VBox"] layout_mode = 2 size_flags_vertical = 10 alignment = 1 -[node name="Save Button" type="Button" parent="Settings/MarginContainer/Settings VBox/HBoxContainer"] -layout_mode = 2 -text = "Save" - -[node name="Cancel Button" type="Button" parent="Settings/MarginContainer/Settings VBox/HBoxContainer"] +[node name="Confirm Button" type="Button" parent="Settings Menu/MarginContainer/Settings VBox/HBoxContainer"] layout_mode = 2 -text = "Cancel" +text = "Confirm" diff --git a/examples/forest-brawl/ui/menu-theme.tres b/examples/forest-brawl/ui/menu-theme.tres new file mode 100644 index 00000000..3050d9ae --- /dev/null +++ b/examples/forest-brawl/ui/menu-theme.tres @@ -0,0 +1,33 @@ +[gd_resource type="Theme" load_steps=5 format=3 uid="uid://cg8p4yow3i3ly"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wbjcp"] +bg_color = Color(0.25, 0.25, 0.25, 0.752941) +corner_radius_top_left = 2 +corner_radius_top_right = 2 +corner_radius_bottom_right = 2 +corner_radius_bottom_left = 2 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1tayi"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_2kkly"] + +[sub_resource type="SystemFont" id="SystemFont_43mgq"] +generate_mipmaps = true +multichannel_signed_distance_field = true + +[resource] +default_font = SubResource("SystemFont_43mgq") +EmbossLabel/base_type = &"Label" +EmbossLabel/styles/normal = SubResource("StyleBoxFlat_wbjcp") +MainMenuButton/base_type = &"Button" +MainMenuButton/colors/font_color = Color(1, 1, 1, 0.752941) +MainMenuButton/colors/font_focus_color = Color(1, 1, 1, 0.878431) +MainMenuButton/colors/font_hover_color = Color(1, 1, 1, 0.878431) +MainMenuButton/colors/font_hover_pressed_color = Color(1, 1, 1, 1) +MainMenuButton/font_sizes/font_size = 48 +MainMenuButton/styles/focus = SubResource("StyleBoxEmpty_1tayi") +MainMenuButton/styles/pressed = SubResource("StyleBoxEmpty_2kkly") +MarginContainer/constants/margin_bottom = 4 +MarginContainer/constants/margin_left = 4 +MarginContainer/constants/margin_right = 4 +MarginContainer/constants/margin_top = 4 diff --git a/examples/forest-brawl/ui-settings/player-stat-label.tres b/examples/forest-brawl/ui/player-stat-label.tres similarity index 100% rename from examples/forest-brawl/ui-settings/player-stat-label.tres rename to examples/forest-brawl/ui/player-stat-label.tres From a77cbba160f2b3625f967919a9fabeb5a1a3d032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Tue, 21 Oct 2025 23:16:33 +0200 Subject: [PATCH 04/13] connect to nohub and noray --- examples/forest-brawl/forest-brawl.tscn | 7 +- examples/forest-brawl/menu.tscn | 226 ++++++++++++++++++- examples/forest-brawl/ui/die-icon.svg | 1 + examples/forest-brawl/ui/die-icon.svg.import | 37 +++ examples/forest-brawl/ui/gauss-bg.png | Bin 0 -> 283 bytes examples/forest-brawl/ui/gauss-bg.png.import | 34 +++ 6 files changed, 293 insertions(+), 12 deletions(-) create mode 100644 examples/forest-brawl/ui/die-icon.svg create mode 100644 examples/forest-brawl/ui/die-icon.svg.import create mode 100644 examples/forest-brawl/ui/gauss-bg.png create mode 100644 examples/forest-brawl/ui/gauss-bg.png.import diff --git a/examples/forest-brawl/forest-brawl.tscn b/examples/forest-brawl/forest-brawl.tscn index c9f77f39..44510566 100644 --- a/examples/forest-brawl/forest-brawl.tscn +++ b/examples/forest-brawl/forest-brawl.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=20 format=3 uid="uid://cwh2p0qb5872o"] +[gd_scene load_steps=21 format=3 uid="uid://cwh2p0qb5872o"] [ext_resource type="PackedScene" uid="uid://d1544gxqaoptc" path="res://examples/forest-brawl/maps/three-peaks.tscn" id="1_xksrt"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/brawler-spawner.gd" id="5_qv1fx"] @@ -16,6 +16,7 @@ [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd" id="13_ujuuj"] [ext_resource type="PackedScene" uid="uid://ojh5xofoserg" path="res://examples/forest-brawl/scenes/score_screen.tscn" id="14_85lvt"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd" id="14_h1iqv"] +[ext_resource type="PackedScene" uid="uid://dbnx63lgo7288" path="res://examples/forest-brawl/menu.tscn" id="16_3ljtn"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/volume-slider.gd" id="16_6pky3"] [ext_resource type="LabelSettings" uid="uid://b4u1aluftkajy" path="res://examples/forest-brawl/ui/player-stat-label.tres" id="17_48up8"] @@ -78,6 +79,7 @@ grow_vertical = 1 horizontal_alignment = 2 [node name="Network Popup" type="TabContainer" parent="UI"] +visible = false custom_minimum_size = Vector2(320, 240) layout_mode = 1 anchors_preset = 8 @@ -298,6 +300,9 @@ oid_input = NodePath("../Noray/OID Row/OID Value") host_oid_input = NodePath("../Noray/Connect Host Row/Host LineEdit") force_relay_check = NodePath("../Noray/Connect Actions Row/Force Relay Checkbox") +[node name="Menu" parent="UI" instance=ExtResource("16_3ljtn")] +layout_mode = 1 + [node name="Player stats" type="Control" parent="UI" node_paths=PackedStringArray("score_label", "score_manager")] visible = false layout_mode = 1 diff --git a/examples/forest-brawl/menu.tscn b/examples/forest-brawl/menu.tscn index afff9dcb..001d9fa3 100644 --- a/examples/forest-brawl/menu.tscn +++ b/examples/forest-brawl/menu.tscn @@ -1,11 +1,13 @@ -[gd_scene load_steps=12 format=3 uid="uid://dbnx63lgo7288"] +[gd_scene load_steps=17 format=3 uid="uid://dbnx63lgo7288"] [ext_resource type="Texture2D" uid="uid://cdb8di7e1p6h6" path="res://icon.png" id="1_6gbgt"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/random-name-input.gd" id="1_h5ah4"] [ext_resource type="Theme" uid="uid://cg8p4yow3i3ly" path="res://examples/forest-brawl/ui/menu-theme.tres" id="1_xmgyy"] +[ext_resource type="Texture2D" uid="uid://4vyxbqthy3nf" path="res://examples/forest-brawl/ui/gauss-bg.png" id="2_f45cx"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd" id="2_llkcl"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/vsync-checkbutton.gd" id="3_amefg"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd" id="4_s5qgy"] +[ext_resource type="Texture2D" uid="uid://c6bp4x3j4l27k" path="res://examples/forest-brawl/ui/die-icon.svg" id="4_w03d4"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/volume-slider.gd" id="5_5ifsx"] [sub_resource type="GDScript" id="GDScript_jmdql"] @@ -19,6 +21,7 @@ script/source = "extends Control @onready var quick_play_menu := %\"Quick Play Menu\" as Control @onready var games_menu := %\"Games Menu\" as Control +@onready var lan_menu := %\"LAN Menu\" as Control @onready var settings_menu := %\"Settings Menu\" as Control func _ready() -> void: @@ -35,7 +38,7 @@ func _find_game() -> void: _switch_to(games_menu) func _play_lan() -> void: - pass + _switch_to(lan_menu) func _settings() -> void: _switch_to(settings_menu) @@ -85,6 +88,29 @@ func _switch_to(menu: Control) -> void: hide() " +[sub_resource type="Animation" id="Animation_7nw6r"] +resource_name = "spin" +length = 1.5 +loop_mode = 1 +step = 0.0416667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:modulate") +tracks/0/interp = 2 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.75, 1.5), +"transitions": PackedFloat32Array(1, 1, 1), +"update": 0, +"values": [Color(1, 1, 1, 0.501961), Color(1, 1, 1, 1), Color(1, 1, 1, 0.501961)] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_dy8vn"] +_data = { +"spin": SubResource("Animation_7nw6r") +} + [sub_resource type="GDScript" id="GDScript_2payw"] script/source = "extends Control @@ -121,6 +147,9 @@ var presets: Array[Preset] = [ Preset.new(\"localhost\", \"localhost:8890\", \"localhost:9980\") ] +var _noray_address := \"\" +var _nohub_address := \"\" + var _nohub_peer: StreamPeerTCP var _nohub_client: NohubClient var _poll_interval := 2. @@ -151,13 +180,13 @@ func _ready() -> void: func _process(dt: float) -> void: # Update status if _nohub_peer == null: - status_label.text = \"Offline\" + status_label.text = \"Status: Offline\" else: _nohub_peer.poll() match _nohub_peer.get_status(): - StreamPeerTCP.STATUS_CONNECTED: status_label.text = \"Online\" - StreamPeerTCP.STATUS_CONNECTING: status_label.text = \"Connecting...\" - StreamPeerTCP.STATUS_ERROR: status_label.text = \"Error\" + StreamPeerTCP.STATUS_CONNECTED: status_label.text = \"Status: Online\" + StreamPeerTCP.STATUS_CONNECTING: status_label.text = \"Status: Connecting...\" + StreamPeerTCP.STATUS_ERROR: status_label.text = \"Status: Error\" _poll_wait -= dt if _nohub_client: @@ -195,6 +224,7 @@ func _connect() -> void: var err := await Noray.connect_to_host(noray_address[0], noray_address[1]) if err != OK: print(\"Failed to connect to noray: %s\" % [error_string(err)]) + _disconnect() return print(\"Success!\") @@ -210,28 +240,46 @@ func _connect() -> void: break StreamPeerTCP.STATUS_ERROR: print(\"Failed to connect!\") + _disconnect() return await get_tree().process_frame _nohub_peer = peer _nohub_client = NohubClient.new(peer) + # Register with noray + Noray.register_host() + await Noray.on_pid + + err = await Noray.register_remote() + if err != OK: + print(\"Failed to register with noray: %s\" % error_string(err)) + _disconnect() + return + + # Set GameID in nohub var response := await _nohub_client.set_game(_nohub_game_id) if not response.is_success(): print(\"Failed to set game ID! %s\" % [response]) _disconnect() return + # Success + _noray_address = \"%s:%d\" % noray_address + _nohub_address = \"%s:%d\" % nohub_address + func _disconnect() -> void: _render_lobbies([]) if Noray.is_connected_to_host(): Noray.disconnect_from_host() + _noray_address = \"\" if _nohub_peer != null: _nohub_peer.disconnect_from_host() _nohub_peer = null _nohub_client = null + _nohub_address = \"\" func _dock() -> void: if dock_container.visible: @@ -246,7 +294,42 @@ func _back() -> void: _switch_to(main_menu) func _host() -> void: - pass # TODO + if not _nohub_client: + return + + var lobby_name := lobby_name_input.text + var lobby_limit := lobby_player_limit_input.text + + if not lobby_name: + print(\"Lobby name can't be empty!\") + elif not lobby_limit.is_valid_int(): + print(\"Player limit is not a number!\") + elif int(lobby_limit) <= 0: + print(\"Invalid player limit!\") + else: + var player_limit := int(lobby_limit) + var address := \"noray://%s/%s\" % [_noray_address, Noray.oid] + var data := { \"name\": lobby_name, \"player-count\": \"0\", \"player-capacity\": str(player_limit)} + var response := await _nohub_client.create_lobby(address, data) + if not response.is_success(): + print(\"Failed to create lobby!\", response) + else: + print(\"Created lobby!\", response.value()) + _poll_wait = -1. + # TODO: Start game + +func _join(lobby_id: String) -> void: + if not _nohub_client: + return + + var response := await _nohub_client.join_lobby(lobby_id) + if not response.is_success(): + print(\"Failed to join lobby %s: %s\" % [lobby_id, response]) + return + + var address := response.value() + OS.alert(address) + # TODO: Start game # TODO: Deduplicate? func _switch_to(menu: Control) -> void: @@ -270,6 +353,7 @@ func _render_lobbies(lobbies: Array[NohubLobby]) -> void: var join_button := Button.new() join_button.text = \">\" join_button.tooltip_text = \"Join this lobby\" + join_button.pressed.connect(func(): _join(lobby.id)) lobbies_container.add_child(name_label) lobbies_container.add_child(players_label) @@ -286,6 +370,30 @@ func _parse_address(address: String, default_port: int = 0) -> Array: return result " +[sub_resource type="GDScript" id="GDScript_u7g6r"] +script/source = "extends Control + +@onready var main_menu := %\"Main Menu\" as Control + +@onready var host_input := $\"MarginContainer/VBoxContainer/GridContainer/Host Input\" as LineEdit +@onready var port_input := $\"MarginContainer/VBoxContainer/GridContainer/Port Input\" as LineEdit + +@onready var back_button := $\"MarginContainer/VBoxContainer/HBoxContainer/Back Button\" as Button +@onready var connect_button := $\"MarginContainer/VBoxContainer/HBoxContainer/Connect Button\" as Button +@onready var host_button := $\"MarginContainer/VBoxContainer/HBoxContainer/Host Button\" as Button + +func _ready(): + back_button.pressed.connect(_back) + +func _back(): + _switch_to(main_menu) + +# TODO: Deduplicate? +func _switch_to(menu: Control) -> void: + menu.show() + hide() +" + [sub_resource type="GDScript" id="GDScript_xlfi6"] script/source = "extends Control @@ -313,9 +421,19 @@ grow_horizontal = 2 grow_vertical = 2 theme = ExtResource("1_xmgyy") +[node name="TextureRect" type="TextureRect" parent="."] +modulate = Color(0, 0, 0, 0.501961) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("2_f45cx") +stretch_mode = 6 + [node name="Main Menu" type="VBoxContainer" parent="."] unique_name_in_owner = true -visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -374,10 +492,17 @@ text = "Searching..." horizontal_alignment = 1 [node name="Spinner" type="TextureRect" parent="Quick Play Menu"] +modulate = Color(1, 1, 1, 0.501961) layout_mode = 2 texture = ExtResource("1_6gbgt") stretch_mode = 5 +[node name="AnimationPlayer" type="AnimationPlayer" parent="Quick Play Menu/Spinner"] +autoplay = "spin" +libraries = { +"": SubResource("AnimationLibrary_dy8vn") +} + [node name="HBoxContainer" type="HBoxContainer" parent="Quick Play Menu"] layout_mode = 2 alignment = 1 @@ -394,6 +519,7 @@ text = "Host" [node name="Games Menu" type="Control" parent="."] unique_name_in_owner = true +visible = false custom_minimum_size = Vector2(960, 540) layout_mode = 1 anchors_preset = 8 @@ -547,10 +673,79 @@ text = "Back" layout_mode = 2 text = "Host Game" +[node name="LAN Menu" type="PanelContainer" parent="."] +unique_name_in_owner = true +visible = false +custom_minimum_size = Vector2(480, 320) +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -20.0 +offset_top = -20.0 +offset_right = 20.0 +offset_bottom = 20.0 +grow_horizontal = 2 +grow_vertical = 2 +script = SubResource("GDScript_u7g6r") + +[node name="MarginContainer" type="MarginContainer" parent="LAN Menu"] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="LAN Menu/MarginContainer"] +layout_mode = 2 + +[node name="Title Label" type="Label" parent="LAN Menu/MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_type_variation = &"HeaderMedium" +text = "Play on LAN" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="LAN Menu/MarginContainer/VBoxContainer"] +layout_mode = 2 +columns = 2 + +[node name="Host Label" type="Label" parent="LAN Menu/MarginContainer/VBoxContainer/GridContainer"] +layout_mode = 2 +text = "Host: " + +[node name="Host Input" type="LineEdit" parent="LAN Menu/MarginContainer/VBoxContainer/GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "localhost" + +[node name="Port Label" type="Label" parent="LAN Menu/MarginContainer/VBoxContainer/GridContainer"] +layout_mode = 2 +text = "Port: " + +[node name="Port Input" type="LineEdit" parent="LAN Menu/MarginContainer/VBoxContainer/GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "16384" + +[node name="HBoxContainer" type="HBoxContainer" parent="LAN Menu/MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 10 +alignment = 1 + +[node name="Back Button" type="Button" parent="LAN Menu/MarginContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Back" + +[node name="Connect Button" type="Button" parent="LAN Menu/MarginContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Connect" + +[node name="Host Button" type="Button" parent="LAN Menu/MarginContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Host" + [node name="Settings Menu" type="PanelContainer" parent="."] unique_name_in_owner = true visible = false -custom_minimum_size = Vector2(640, 360) +custom_minimum_size = Vector2(480, 320) layout_mode = 1 anchors_preset = 8 anchor_left = 0.5 @@ -568,6 +763,12 @@ layout_mode = 2 layout_mode = 2 size_flags_vertical = 3 +[node name="Title Label" type="Label" parent="Settings Menu/MarginContainer/Settings VBox"] +layout_mode = 2 +theme_type_variation = &"HeaderMedium" +text = "Settings" +horizontal_alignment = 1 + [node name="Player Name" type="HBoxContainer" parent="Settings Menu/MarginContainer/Settings VBox"] layout_mode = 2 @@ -582,9 +783,12 @@ text = "Nameless Brawler" clear_button_enabled = true script = ExtResource("1_h5ah4") -[node name="Button" type="Button" parent="Settings Menu/MarginContainer/Settings VBox/Player Name"] +[node name="Randomize Button" type="Button" parent="Settings Menu/MarginContainer/Settings VBox/Player Name"] +custom_minimum_size = Vector2(32, 0) layout_mode = 2 -text = "RNG" +icon = ExtResource("4_w03d4") +icon_alignment = 1 +expand_icon = true [node name="HSeparator" type="HSeparator" parent="Settings Menu/MarginContainer/Settings VBox"] layout_mode = 2 diff --git a/examples/forest-brawl/ui/die-icon.svg b/examples/forest-brawl/ui/die-icon.svg new file mode 100644 index 00000000..58d150ed --- /dev/null +++ b/examples/forest-brawl/ui/die-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/forest-brawl/ui/die-icon.svg.import b/examples/forest-brawl/ui/die-icon.svg.import new file mode 100644 index 00000000..997b9fe3 --- /dev/null +++ b/examples/forest-brawl/ui/die-icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c6bp4x3j4l27k" +path="res://.godot/imported/die-icon.svg-b75822ea2b6919dba23b8da30e58af37.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://examples/forest-brawl/ui/die-icon.svg" +dest_files=["res://.godot/imported/die-icon.svg-b75822ea2b6919dba23b8da30e58af37.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=4.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/examples/forest-brawl/ui/gauss-bg.png b/examples/forest-brawl/ui/gauss-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..173855335c30ce544c60e97e491fc14ff5952894 GIT binary patch literal 283 zcmV+$0p$LPP)|Dd>>fBdHX1tsb$GVb z2_Pb>q-wVp*F1H&$#;t})%(VS;u`19u+yE1voR$iZUrLZDk-~F&9I-^nEQcRcaT;Y h<^D(N1zOFkuW$dbYhvyy_%#3k002ovPDHLkV1gZ Date: Tue, 21 Oct 2025 23:39:17 +0200 Subject: [PATCH 05/13] lan play --- examples/forest-brawl/menu.tscn | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/examples/forest-brawl/menu.tscn b/examples/forest-brawl/menu.tscn index 001d9fa3..966b61f3 100644 --- a/examples/forest-brawl/menu.tscn +++ b/examples/forest-brawl/menu.tscn @@ -384,14 +384,56 @@ script/source = "extends Control func _ready(): back_button.pressed.connect(_back) + connect_button.pressed.connect(_connect) + host_button.pressed.connect(_host) func _back(): _switch_to(main_menu) +func _connect(): + var host := host_input.text + var port = _get_port() + + var peer := ENetMultiplayerPeer.new() + var err := peer.create_client(host, port) + if err != OK: + push_error(\"Failed to start client: %s\" % [error_string(err)]) + return + + multiplayer.multiplayer_peer = peer + + while peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTING: + peer.poll() + await get_tree().process_frame + + if peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED: + push_error(\"Failed to connect!\") + return + + get_parent_control().hide() # Success + +func _host(): + var port := _get_port() + + var peer := ENetMultiplayerPeer.new() + var err := peer.create_server(port) + if err != OK: + push_error(\"Failed to start server: %s\" % [error_string(err)]) + return + + multiplayer.multiplayer_peer = peer + get_parent_control().hide() # Success + # TODO: Deduplicate? func _switch_to(menu: Control) -> void: menu.show() hide() + +func _get_port() -> int: + if not port_input.text.is_valid_int(): + return 16384 + else: + return int(port_input.text) " [sub_resource type="GDScript" id="GDScript_xlfi6"] From 01d3d11a00bd939820838d34b06549501464cded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Wed, 22 Oct 2025 12:37:48 +0200 Subject: [PATCH 06/13] connect noray --- .../forest-brawl/forest-brawl-connector.gd | 180 +++++++++++++++ .../forest-brawl-noray-connector.gd | 127 ++++++++++ examples/forest-brawl/forest-brawl.tscn | 6 +- examples/forest-brawl/menu.tscn | 216 +++++++----------- 4 files changed, 400 insertions(+), 129 deletions(-) create mode 100644 examples/forest-brawl/forest-brawl-connector.gd create mode 100644 examples/forest-brawl/forest-brawl-noray-connector.gd diff --git a/examples/forest-brawl/forest-brawl-connector.gd b/examples/forest-brawl/forest-brawl-connector.gd new file mode 100644 index 00000000..08cfe0a1 --- /dev/null +++ b/examples/forest-brawl/forest-brawl-connector.gd @@ -0,0 +1,180 @@ +extends Node +class_name ForestBrawlConnector + +class ServiceHosts: + var name: String + var noray_address: String + var nohub_address: String + + func _init(p_name: String, p_noray_address: String, p_nohub_address: String): + name = p_name + noray_address = p_noray_address + nohub_address = p_nohub_address + +const GAME_ID := "WK6koYfZ7cEMjcsba3ovxQF1lM9XjkWh" + +static var _instance: ForestBrawlConnector +static var known_service_hosts: Array[ServiceHosts] = [ + ServiceHosts.new("foxssake.studio", "foxssake.studio:8890", "foxssake.studio:12980"), + ServiceHosts.new("localhost", "localhost:8890", "localhost:9980") +] + +var _noray_connector: ForestBrawlNorayConnector + +var _noray_address := "" +var _nohub_address := "" + +var _nohub_peer: StreamPeerTCP +var _nohub_client: NohubClient + +static func _static_init(): + known_service_hosts.make_read_only() + +static func nohub() -> NohubClient: + if not _instance: return null + return _instance._nohub_client + +static func noray_address() -> String: + if not _instance: return "" + return _instance._noray_address + +static func nohub_address() -> String: + if not _instance: return "" + return _instance._nohub_address + +static func connect_to_service_hosts(services: ServiceHosts) -> Error: + return await connect_to_services(services.noray_address, services.nohub_address) + +static func connect_to_any_service_host() -> Error: + # TODO: Find one based on ping + return await connect_to_service_hosts(known_service_hosts[0]) + +static func connect_to_services(p_noray_address: String, p_nohub_address: String) -> Error: + assert(_instance, "ForestBrawlConnector instance missing from Scene Tree!") + return await _instance._connect_to_services(p_noray_address, p_nohub_address) + +static func disconnect_from_services() -> void: + assert(_instance, "ForestBrawlConnector instance missing from Scene Tree!") + _instance._disconnect_from_services() + +static func is_connected_to_services() -> bool: + if not _instance: return false + return _instance._is_connected_to_services() + +static func join_noray(oid: String) -> Error: + assert(_instance, "ForestBrawlConnector instance missing from Scene Tree!") + return _instance._join_noray(oid) + +static func host_noray() -> Error: + assert(_instance, "ForestBrawlConnector instance missing from Scene Tree!") + return await _instance._host_noray() + +func _connect_to_services(p_noray_address: String, p_nohub_address: String) -> Error: + _disconnect_from_services() + + var noray_address = _parse_address(p_noray_address, 8890) + var nohub_address = _parse_address(p_nohub_address, 12980) + + # Connect to noray + print("Connecting to noray at %s:%d..." % [noray_address[0], noray_address[1]]) + var err := await Noray.connect_to_host(noray_address[0], noray_address[1]) + if err != OK: + print("Failed to connect to noray: %s" % [error_string(err)]) + _disconnect_from_services() + return err + print("Success!") + + # Connect to nohub + print("Connecting to nohub at %s:%d..." % [noray_address[0], noray_address[1]]) + var peer := StreamPeerTCP.new() + peer.connect_to_host(nohub_address[0], nohub_address[1]) + while true: + peer.poll() + match peer.get_status(): + StreamPeerTCP.STATUS_CONNECTED: + print("Success!") + break + StreamPeerTCP.STATUS_ERROR: + print("Failed to connect!") + _disconnect_from_services() + return ERR_CONNECTION_ERROR + await get_tree().process_frame + + _nohub_peer = peer + _nohub_client = NohubClient.new(peer) + + # Register with noray + print("Registering host with noray... ") + Noray.register_host() + await Noray.on_pid + print("Success!") + + print("Registering remote with noray... ") + err = await Noray.register_remote() + if err != OK: + print("Failed registering remote address: %s" % error_string(err)) + _disconnect_from_services() + return ERR_CANT_ACQUIRE_RESOURCE + print("Success!") + + # Set GameID in nohub + print("Setting game ID with nohub... ") + await get_tree().process_frame + var response := await _nohub_client.set_game(GAME_ID) + if not response.is_success(): + print("Failed to set game ID! %s" % [response]) + _disconnect_from_services() + return ERR_QUERY_FAILED + print("Success!") + + # Success + _noray_address = "%s:%d" % noray_address + _nohub_address = "%s:%d" % nohub_address + + return OK + +func _disconnect_from_services() -> void: + if Noray.is_connected_to_host(): + Noray.disconnect_from_host() + _noray_address = "" + + if _nohub_peer != null: + _nohub_peer.disconnect_from_host() + _nohub_peer = null + _nohub_client = null + _nohub_address = "" + +func _is_connected_to_services() -> bool: + return Noray.is_connected_to_host() and _nohub_peer != null and _nohub_peer.get_status() == StreamPeerTCP.STATUS_CONNECTED + +func _join_noray(oid: String) -> Error: + return _noray_connector.join(oid) + +func _host_noray() -> Error: + return await _noray_connector.host() + +func _ready(): + _instance = self + _noray_connector = ForestBrawlNorayConnector.new() + add_child(_noray_connector) + +func _process(_dt) -> void: + if _nohub_peer: + var err := _nohub_peer.poll() + if err != OK: + print("Failed polling nohub: ", error_string(err)) + _disconnect_from_services() + + if _nohub_client: + # TODO(trimsock): Return poll result, so we don't need to poll the peer separately + _nohub_client.poll() + +func _parse_address(address: String, default_port: int = 0) -> Array: + var result = ["", default_port] + if address.contains(":"): + var idx := address.rfind(":") + result[0] = address.substr(0, idx) + result[1] = int(address.substr(idx + 1)) + else: + result[0] = address + return result diff --git a/examples/forest-brawl/forest-brawl-noray-connector.gd b/examples/forest-brawl/forest-brawl-noray-connector.gd new file mode 100644 index 00000000..289e8ea9 --- /dev/null +++ b/examples/forest-brawl/forest-brawl-noray-connector.gd @@ -0,0 +1,127 @@ +extends Node +class_name ForestBrawlNorayConnector + +var _is_host := false +var _is_client := false +var _target_oid := "" + +func _ready(): + Noray.on_connect_nat.connect(_handle_connect_nat) + Noray.on_connect_relay.connect(_handle_connect_relay) + +func host() -> Error: + if Noray.local_port <= 0: + return ERR_UNCONFIGURED + + # Start host + var err = OK + var port = Noray.local_port + print("Starting host on port %s" % port) + + var peer = ENetMultiplayerPeer.new() + err = peer.create_server(port) + if err != OK: + print("Failed to listen on port %s: %s" % [port, error_string(err)]) + return err + + get_tree().get_multiplayer().multiplayer_peer = peer + print("Listening on port %s" % port) + + # Wait for server to start + while peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTING: + await get_tree().process_frame + + if peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED: + OS.alert("Failed to start server!") + return FAILED + + get_tree().get_multiplayer().server_relay = true + + _is_host = true + _is_client = false + + return OK + +func join(oid: String, force_relay: bool = false) -> Error: + _is_host = false + _is_client = true + _target_oid = oid + + if force_relay: + return Noray.connect_relay(oid) + else: + return Noray.connect_nat(oid) + +func _handle_connect_nat(address: String, port: int) -> Error: + var err = await _handle_connect(address, port) + + # If client failed to connect over NAT, try again over relay + if err != OK and not _is_host: + print("NAT connect failed with reason %s, retrying with relay" % error_string(err)) + Noray.connect_relay(_target_oid) + err = OK + + return err + +func _handle_connect_relay(address: String, port: int) -> Error: + return await _handle_connect(address, port) + +func _handle_connect(address: String, port: int) -> Error: + if not Noray.local_port: + return ERR_UNCONFIGURED + + var err = OK + + if not _is_host and not _is_client: + push_warning("Refusing connection, not running as client nor host") + err = ERR_UNAVAILABLE + + if _is_client: + var udp = PacketPeerUDP.new() + udp.bind(Noray.local_port) + udp.set_dest_address(address, port) + + print("Attempting handshake with %s:%s" % [address, port]) + err = await PacketHandshake.over_packet_peer(udp) + udp.close() + + if err != OK: + if err == ERR_BUSY: + print("Handshake to %s:%s succeeded partially, attempting connection anyway" % [address, port]) + else: + print("Handshake to %s:%s failed: %s" % [address, port, error_string(err)]) + return err + else: + print("Handshake to %s:%s succeeded" % [address, port]) + + # Connect + var peer = ENetMultiplayerPeer.new() + err = peer.create_client(address, port, 0, 0, 0, Noray.local_port) + if err != OK: + print("Failed to create client: %s" % error_string(err)) + return err + + get_tree().get_multiplayer().multiplayer_peer = peer + + # Wait for connection to succeed + await Async.condition( + func(): return peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTING + ) + + if peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED: + print("Failed to connect to %s:%s with status %s" % [address, port, peer.get_connection_status()]) + get_tree().get_multiplayer().multiplayer_peer = null + return ERR_CANT_CONNECT + + if _is_host: + # We should already have the connection configured, only thing to do is a handshake + var peer = get_tree().get_multiplayer().multiplayer_peer as ENetMultiplayerPeer + + err = await PacketHandshake.over_enet_peer(peer, address, port) + + if err != OK: + print("Handshake to %s:%s failed: %s" % [address, port, error_string(err)]) + return err + print("Handshake to %s:%s concluded" % [address, port]) + + return err diff --git a/examples/forest-brawl/forest-brawl.tscn b/examples/forest-brawl/forest-brawl.tscn index 44510566..a4dbe6c3 100644 --- a/examples/forest-brawl/forest-brawl.tscn +++ b/examples/forest-brawl/forest-brawl.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=21 format=3 uid="uid://cwh2p0qb5872o"] +[gd_scene load_steps=22 format=3 uid="uid://cwh2p0qb5872o"] [ext_resource type="PackedScene" uid="uid://d1544gxqaoptc" path="res://examples/forest-brawl/maps/three-peaks.tscn" id="1_xksrt"] +[ext_resource type="Script" path="res://examples/forest-brawl/forest-brawl-connector.gd" id="2_wafqi"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/brawler-spawner.gd" id="5_qv1fx"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/following-camera.gd" id="5_yxhn7"] [ext_resource type="PackedScene" uid="uid://wi4owat0bml3" path="res://examples/forest-brawl/scenes/brawler.tscn" id="7_tcy3g"] @@ -29,6 +30,9 @@ font_size = 64 [node name="Network" type="Node" parent="."] +[node name="ForestBrawlConnector" type="Node" parent="Network"] +script = ExtResource("2_wafqi") + [node name="Brawler Spawner" type="Node" parent="Network" node_paths=PackedStringArray("spawn_root", "camera", "joining_screen", "name_input")] unique_name_in_owner = true script = ExtResource("5_qv1fx") diff --git a/examples/forest-brawl/menu.tscn b/examples/forest-brawl/menu.tscn index 966b61f3..0d5ae45a 100644 --- a/examples/forest-brawl/menu.tscn +++ b/examples/forest-brawl/menu.tscn @@ -1,7 +1,6 @@ [gd_scene load_steps=17 format=3 uid="uid://dbnx63lgo7288"] [ext_resource type="Texture2D" uid="uid://cdb8di7e1p6h6" path="res://icon.png" id="1_6gbgt"] -[ext_resource type="Script" path="res://examples/forest-brawl/scripts/random-name-input.gd" id="1_h5ah4"] [ext_resource type="Theme" uid="uid://cg8p4yow3i3ly" path="res://examples/forest-brawl/ui/menu-theme.tres" id="1_xmgyy"] [ext_resource type="Texture2D" uid="uid://4vyxbqthy3nf" path="res://examples/forest-brawl/ui/gauss-bg.png" id="2_f45cx"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd" id="2_llkcl"] @@ -10,6 +9,19 @@ [ext_resource type="Texture2D" uid="uid://c6bp4x3j4l27k" path="res://examples/forest-brawl/ui/die-icon.svg" id="4_w03d4"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/volume-slider.gd" id="5_5ifsx"] +[sub_resource type="GDScript" id="GDScript_hv8dj"] +script/source = "extends Control + +func _ready(): + # Hide when game starts + NetworkEvents.on_client_start.connect(func(__): hide()) + NetworkEvents.on_server_start.connect(func(): hide()) + + # Show when game ends + NetworkEvents.on_client_stop.connect(func(__): show()) + NetworkEvents.on_server_stop.connect(func(): show()) +" + [sub_resource type="GDScript" id="GDScript_jmdql"] script/source = "extends Control @@ -114,16 +126,6 @@ _data = { [sub_resource type="GDScript" id="GDScript_2payw"] script/source = "extends Control -class Preset: - var name: String - var noray_address: String - var nohub_address: String - - func _init(p_name: String, p_noray_address: String, p_nohub_address: String): - name = p_name - noray_address = p_noray_address - nohub_address = p_nohub_address - @onready var presets_option := $\"MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/Presets Option\" as OptionButton @onready var noray_input := $\"MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/noray Input\" as LineEdit @onready var nohub_input := $\"MarginContainer/HBoxContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/nohub Input\" as LineEdit @@ -142,27 +144,17 @@ class Preset: @onready var main_menu := %\"Main Menu\" as Control -var presets: Array[Preset] = [ - Preset.new(\"foxssake.studio\", \"foxssake.studio:8890\", \"foxssake.studio:12980\"), - Preset.new(\"localhost\", \"localhost:8890\", \"localhost:9980\") -] - -var _noray_address := \"\" -var _nohub_address := \"\" - -var _nohub_peer: StreamPeerTCP -var _nohub_client: NohubClient var _poll_interval := 2. var _poll_wait := 0. -var _nohub_game_id := \"WK6koYfZ7cEMjcsba3ovxQF1lM9XjkWh\" +var _is_hosting := false func _ready() -> void: # TODO: Deduplicate? visibility_changed.connect(func(): - if visible: _execute() + if is_visible_in_tree(): _execute() else: _cancel() - ) + , CONNECT_DEFERRED) presets_option.item_selected.connect(_select_preset) connect_button.pressed.connect(_connect) @@ -172,114 +164,52 @@ func _ready() -> void: # Populate presets presets_option.clear() - for preset in presets: - presets_option.add_item(preset.name) + for hosts in ForestBrawlConnector.known_service_hosts: + presets_option.add_item(hosts.name) presets_option.select(0) _select_preset(0) func _process(dt: float) -> void: - # Update status - if _nohub_peer == null: - status_label.text = \"Status: Offline\" - else: - _nohub_peer.poll() - match _nohub_peer.get_status(): - StreamPeerTCP.STATUS_CONNECTED: status_label.text = \"Status: Online\" - StreamPeerTCP.STATUS_CONNECTING: status_label.text = \"Status: Connecting...\" - StreamPeerTCP.STATUS_ERROR: status_label.text = \"Status: Error\" - + # Poll lobbies _poll_wait -= dt - if _nohub_client: - _nohub_client.poll() - if _poll_wait < 0.: - print(\"Listing lobbies...\") - _poll_wait = _poll_interval - var response := await _nohub_client.list_lobbies() - if not response.is_success(): - print(\"Failed listing lobbies: %s\" % [response]) - else: - print(\"Found lobbies: %s\" % [response.value()]) - _render_lobbies(response.value()) + if _poll_wait < 0.0 and ForestBrawlConnector.is_connected_to_services(): + print(\"Listing lobbies...\") + _poll_wait = _poll_interval + var response := await ForestBrawlConnector.nohub().list_lobbies() + if not response.is_success(): + print(\"Failed listing lobbies: %s\" % [response]) + else: + print(\"Found lobbies: %s\" % [response.value()]) + _render_lobbies(response.value()) func _execute() -> void: - _connect.call_deferred() + _connect() func _cancel() -> void: - _disconnect.call_deferred() + _disconnect() _render_lobbies([]) func _select_preset(idx: int) -> void: - var preset := presets[idx] + var preset := ForestBrawlConnector.known_service_hosts[idx] noray_input.text = preset.noray_address nohub_input.text = preset.nohub_address func _connect() -> void: _disconnect() - var noray_address = _parse_address(noray_input.text, 8890) - var nohub_address = _parse_address(nohub_input.text, 12980) - - # Connect to noray - print(\"Connecting to noray at %s:%d...\" % [noray_address[0], noray_address[1]]) - var err := await Noray.connect_to_host(noray_address[0], noray_address[1]) - if err != OK: - print(\"Failed to connect to noray: %s\" % [error_string(err)]) - _disconnect() - return - print(\"Success!\") - - # Connect to nohub - print(\"Connecting to nohub at %s:%d...\" % [noray_address[0], noray_address[1]]) - var peer := StreamPeerTCP.new() - peer.connect_to_host(nohub_address[0], nohub_address[1]) - while true: - peer.poll() - match peer.get_status(): - StreamPeerTCP.STATUS_CONNECTED: - print(\"Success!\") - break - StreamPeerTCP.STATUS_ERROR: - print(\"Failed to connect!\") - _disconnect() - return - await get_tree().process_frame - - _nohub_peer = peer - _nohub_client = NohubClient.new(peer) - - # Register with noray - Noray.register_host() - await Noray.on_pid - - err = await Noray.register_remote() + status_label.text = \"Status: Connecting...\" + var err := await ForestBrawlConnector.connect_to_services(noray_input.text, nohub_input.text) if err != OK: - print(\"Failed to register with noray: %s\" % error_string(err)) - _disconnect() - return - - # Set GameID in nohub - var response := await _nohub_client.set_game(_nohub_game_id) - if not response.is_success(): - print(\"Failed to set game ID! %s\" % [response]) + print(\"Failed to connect to services: \", error_string(err)) _disconnect() - return - - # Success - _noray_address = \"%s:%d\" % noray_address - _nohub_address = \"%s:%d\" % nohub_address + else: + status_label.text = \"Status: Online\" func _disconnect() -> void: - _render_lobbies([]) - - if Noray.is_connected_to_host(): - Noray.disconnect_from_host() - _noray_address = \"\" - - if _nohub_peer != null: - _nohub_peer.disconnect_from_host() - _nohub_peer = null - _nohub_client = null - _nohub_address = \"\" + if not _is_hosting: + ForestBrawlConnector.disconnect_from_services() + status_label.text = \"Status: Offline\" + _is_hosting = false func _dock() -> void: if dock_container.visible: @@ -294,11 +224,14 @@ func _back() -> void: _switch_to(main_menu) func _host() -> void: - if not _nohub_client: + if not ForestBrawlConnector.is_connected_to_services(): return var lobby_name := lobby_name_input.text var lobby_limit := lobby_player_limit_input.text + + var noray_address := ForestBrawlConnector.noray_address() + var nohub := ForestBrawlConnector.nohub() if not lobby_name: print(\"Lobby name can't be empty!\") @@ -308,28 +241,43 @@ func _host() -> void: print(\"Invalid player limit!\") else: var player_limit := int(lobby_limit) - var address := \"noray://%s/%s\" % [_noray_address, Noray.oid] + var address := \"noray://%s/%s\" % [noray_address, Noray.oid] var data := { \"name\": lobby_name, \"player-count\": \"0\", \"player-capacity\": str(player_limit)} - var response := await _nohub_client.create_lobby(address, data) + var response := await nohub.create_lobby(address, data) if not response.is_success(): print(\"Failed to create lobby!\", response) else: print(\"Created lobby!\", response.value()) _poll_wait = -1. - # TODO: Start game + + var err := await ForestBrawlConnector.host_noray() + if err != OK: + prints(\"Failed to host game:\", error_string(err)) + return + + # Success! + _is_hosting = true func _join(lobby_id: String) -> void: - if not _nohub_client: + if not ForestBrawlConnector.is_connected_to_services(): return - var response := await _nohub_client.join_lobby(lobby_id) + print(\"Attempting to join lobby #%s\" % [lobby_id]) + var response := await ForestBrawlConnector.nohub().join_lobby(lobby_id) if not response.is_success(): print(\"Failed to join lobby %s: %s\" % [lobby_id, response]) return var address := response.value() - OS.alert(address) - # TODO: Start game + print(\"Received address: %s\" % address) + var uri := _parse_uri(address) + if uri.is_empty() or uri[\"protocol\"] != \"noray\": + prints(\"Invalid address!\", address, uri) + return + print(\"Parsed OID: %s\" % [uri[\"path\"]]) + + # TODO: Connect to noray instance if it's on a different one + ForestBrawlConnector.join_noray(uri[\"path\"]) # TODO: Deduplicate? func _switch_to(menu: Control) -> void: @@ -359,15 +307,18 @@ func _render_lobbies(lobbies: Array[NohubLobby]) -> void: lobbies_container.add_child(players_label) lobbies_container.add_child(join_button) -func _parse_address(address: String, default_port: int = 0) -> Array: - var result = [\"\", default_port] - if address.contains(\":\"): - var idx := address.rfind(\":\") - result[0] = address.substr(0, idx) - result[1] = int(address.substr(idx + 1)) - else: - result[0] = address - return result +func _parse_uri(uri: String) -> Dictionary: + var pattern := RegEx.create_from_string(\"([a-zA-Z0-9]+)://([^/:]+):?([0-9]+)?/(.*)\") + var hit := pattern.search(uri) + if not hit: return {} + + return { + \"uri\": uri, + \"protocol\": hit.strings[1], + \"host\": hit.strings[2], + \"port\": hit.strings[3], + \"path\": hit.strings[4] + } " [sub_resource type="GDScript" id="GDScript_u7g6r"] @@ -442,8 +393,17 @@ script/source = "extends Control @onready var confirm_button := $\"MarginContainer/Settings VBox/HBoxContainer/Confirm Button\" as Button @onready var main_menu := %\"Main Menu\" as Control +@onready var player_name_input := $\"MarginContainer/Settings VBox/Player Name/Player Name Input\" as LineEdit +@onready var randomize_button := $\"MarginContainer/Settings VBox/Player Name/Randomize Button\" as Button + func _ready() -> void: confirm_button.pressed.connect(_confirm) + randomize_button.pressed.connect(_randomize_name) + + _randomize_name() + +func _randomize_name(): + player_name_input.text = NameProvider.name() func _confirm() -> void: _switch_to(main_menu) @@ -462,6 +422,7 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 theme = ExtResource("1_xmgyy") +script = SubResource("GDScript_hv8dj") [node name="TextureRect" type="TextureRect" parent="."] modulate = Color(0, 0, 0, 0.501961) @@ -823,7 +784,6 @@ layout_mode = 2 size_flags_horizontal = 3 text = "Nameless Brawler" clear_button_enabled = true -script = ExtResource("1_h5ah4") [node name="Randomize Button" type="Button" parent="Settings Menu/MarginContainer/Settings VBox/Player Name"] custom_minimum_size = Vector2(32, 0) From 7462397fa43cb58524c24667e05b45686db2db8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Wed, 22 Oct 2025 13:24:34 +0200 Subject: [PATCH 07/13] quick play --- .../forest-brawl/forest-brawl-connector.gd | 30 +++++ examples/forest-brawl/menu.tscn | 108 ++++++++++++++---- 2 files changed, 113 insertions(+), 25 deletions(-) diff --git a/examples/forest-brawl/forest-brawl-connector.gd b/examples/forest-brawl/forest-brawl-connector.gd index 08cfe0a1..2b32c751 100644 --- a/examples/forest-brawl/forest-brawl-connector.gd +++ b/examples/forest-brawl/forest-brawl-connector.gd @@ -61,6 +61,10 @@ static func is_connected_to_services() -> bool: if not _instance: return false return _instance._is_connected_to_services() +static func join(address: String) -> Error: + assert(_instance, "ForestBrawlConnector instance missing from Scene Tree!") + return _instance._join(address) + static func join_noray(oid: String) -> Error: assert(_instance, "ForestBrawlConnector instance missing from Scene Tree!") return _instance._join_noray(oid) @@ -147,6 +151,19 @@ func _disconnect_from_services() -> void: func _is_connected_to_services() -> bool: return Noray.is_connected_to_host() and _nohub_peer != null and _nohub_peer.get_status() == StreamPeerTCP.STATUS_CONNECTED +func _join(address: String) -> Error: + var uri := _parse_uri(address) + if uri.is_empty(): + return ERR_PARSE_ERROR + + if uri["protocol"] == "noray": + # TODO: Support different hosts + var oid := uri["path"] as String + return _join_noray(oid) + + print("Unknown schema: %s" % [uri["protocol"]]) + return ERR_UNAVAILABLE + func _join_noray(oid: String) -> Error: return _noray_connector.join(oid) @@ -178,3 +195,16 @@ func _parse_address(address: String, default_port: int = 0) -> Array: else: result[0] = address return result + +func _parse_uri(uri: String) -> Dictionary: + var pattern := RegEx.create_from_string("([a-zA-Z0-9]+)://([^/:]+):?([0-9]+)?/(.*)") + var hit := pattern.search(uri) + if not hit: return {} + + return { + "uri": uri, + "protocol": hit.strings[1], + "host": hit.strings[2], + "port": hit.strings[3], + "path": hit.strings[4] + } diff --git a/examples/forest-brawl/menu.tscn b/examples/forest-brawl/menu.tscn index 0d5ae45a..65aa8324 100644 --- a/examples/forest-brawl/menu.tscn +++ b/examples/forest-brawl/menu.tscn @@ -20,6 +20,13 @@ func _ready(): # Show when game ends NetworkEvents.on_client_stop.connect(func(__): show()) NetworkEvents.on_server_stop.connect(func(): show()) + + # Make sure the main menu is visible by default + for child in get_children(): + child.hide() + + for i in range(2): + get_child(i).show() " [sub_resource type="GDScript" id="GDScript_jmdql"] @@ -73,9 +80,12 @@ script/source = "extends Control @onready var main_menu := %\"Main Menu\" as Control +func is_active() -> bool: + return is_visible_in_tree() + func _ready() -> void: visibility_changed.connect(func(): - if visible: _execute() + if is_visible_in_tree(): _execute() else: _cancel() ) @@ -83,7 +93,53 @@ func _ready() -> void: host_button.pressed.connect(_host) func _execute() -> void: - pass # TODO + label.text = \"Connecting to services...\" + var err := await ForestBrawlConnector.connect_to_any_service_host() + if err != OK: + label.text = \"Connection failed: %s\" % [error_string(err)] + return + + label.text = \"Looking for games...\" + var expanded_search := false + var search_time := 0.0 + var lobby: NohubLobby + + while is_active(): + await get_tree().create_timer(1.0).timeout + search_time += 1.0 + + var response := await ForestBrawlConnector.nohub().list_lobbies() + if not response.is_success(): + label.text = \"nohub error: %s\" % [response.error().message] + + # Only consider quick-play lobbies + var lobbies := response.value()\\ + .filter(func (it: NohubLobby): + return it.data.get(\"quick-play\", \"\") == \"enabled\" or expanded_search + ) + + if not lobbies.is_empty(): + # TODO: More advanced strategies? + lobby = lobbies.pick_random() + break + + if search_time > 5.0 and not expanded_search: + label.text = \"Expanding search...\" + expanded_search = true + + if not is_active(): + return + + label.text = \"Joining...\" + var response := await ForestBrawlConnector.nohub().join_lobby(lobby.id) + if not response.is_success(): + label.text = \"nohub error: %s\" % [response.error().message] + + var address := response.value() + print(\"Joining address: %s\" % [address]) + err = ForestBrawlConnector.join(address) + if err != OK: + label.text = \"Couldn't join %s: %s\" % [address, error_string(err)] func _cancel() -> void: pass # TODO @@ -92,7 +148,29 @@ func _back() -> void: _switch_to(main_menu) func _host() -> void: - pass # TODO + label.text = \"Creating lobby...\" + + # Create lobby + var lobby_name := \"Quick Play #%x\" % [randi_range(0x10000000, 0xFFFFFFFF)] + var player_capacity := 8 + var address := \"noray://%s/%s\" % [ForestBrawlConnector.noray_address(), Noray.oid] + # TODO(nohub.gd): Stringify data values + var data := { + \"name\": lobby_name, + \"player-count\": \"0\", + \"player-capacity\": str(player_capacity), + \"quick-play\": \"enabled\" + } + + var response := await ForestBrawlConnector.nohub().create_lobby(address, data) + if not response.is_success(): + label.text = \"Lobby fail: %s\" % [response.error().message] + + # Start game + label.text = \"Starting...\" + var err := await ForestBrawlConnector.host_noray() + if err != OK: + label.text = \"Fail: %s\" % [error_string(err)] # TODO: Deduplicate? func _switch_to(menu: Control) -> void: @@ -270,14 +348,7 @@ func _join(lobby_id: String) -> void: var address := response.value() print(\"Received address: %s\" % address) - var uri := _parse_uri(address) - if uri.is_empty() or uri[\"protocol\"] != \"noray\": - prints(\"Invalid address!\", address, uri) - return - print(\"Parsed OID: %s\" % [uri[\"path\"]]) - - # TODO: Connect to noray instance if it's on a different one - ForestBrawlConnector.join_noray(uri[\"path\"]) + ForestBrawlConnector.join(address) # TODO: Deduplicate? func _switch_to(menu: Control) -> void: @@ -306,19 +377,6 @@ func _render_lobbies(lobbies: Array[NohubLobby]) -> void: lobbies_container.add_child(name_label) lobbies_container.add_child(players_label) lobbies_container.add_child(join_button) - -func _parse_uri(uri: String) -> Dictionary: - var pattern := RegEx.create_from_string(\"([a-zA-Z0-9]+)://([^/:]+):?([0-9]+)?/(.*)\") - var hit := pattern.search(uri) - if not hit: return {} - - return { - \"uri\": uri, - \"protocol\": hit.strings[1], - \"host\": hit.strings[2], - \"port\": hit.strings[3], - \"path\": hit.strings[4] - } " [sub_resource type="GDScript" id="GDScript_u7g6r"] @@ -437,6 +495,7 @@ stretch_mode = 6 [node name="Main Menu" type="VBoxContainer" parent="."] unique_name_in_owner = true +visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -478,7 +537,6 @@ flat = true [node name="Quick Play Menu" type="VBoxContainer" parent="."] unique_name_in_owner = true -visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 From 6ef50a827cd3b049a6628b3cb2c241e8b9f254ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Thu, 23 Oct 2025 11:28:09 +0200 Subject: [PATCH 08/13] track player count --- .../forest-brawl/forest-brawl-connector.gd | 93 +++++-- .../forest-brawl-noray-connector.gd | 32 ++- examples/forest-brawl/forest-brawl.tscn | 243 +----------------- examples/forest-brawl/menu.tscn | 48 ++-- .../forest-brawl/scripts/brawler-spawner.gd | 2 +- 5 files changed, 123 insertions(+), 295 deletions(-) diff --git a/examples/forest-brawl/forest-brawl-connector.gd b/examples/forest-brawl/forest-brawl-connector.gd index 2b32c751..a36d0917 100644 --- a/examples/forest-brawl/forest-brawl-connector.gd +++ b/examples/forest-brawl/forest-brawl-connector.gd @@ -13,12 +13,14 @@ class ServiceHosts: const GAME_ID := "WK6koYfZ7cEMjcsba3ovxQF1lM9XjkWh" -static var _instance: ForestBrawlConnector static var known_service_hosts: Array[ServiceHosts] = [ ServiceHosts.new("foxssake.studio", "foxssake.studio:8890", "foxssake.studio:12980"), ServiceHosts.new("localhost", "localhost:8890", "localhost:9980") ] +static var _instance: ForestBrawlConnector +static var _logger := _NetfoxLogger.new("forest-brawl", "ForestBrawlConnector") + var _noray_connector: ForestBrawlNorayConnector var _noray_address := "" @@ -27,6 +29,8 @@ var _nohub_address := "" var _nohub_peer: StreamPeerTCP var _nohub_client: NohubClient +var _hosted_lobby: NohubLobby + static func _static_init(): known_service_hosts.make_read_only() @@ -69,10 +73,22 @@ static func join_noray(oid: String) -> Error: assert(_instance, "ForestBrawlConnector instance missing from Scene Tree!") return _instance._join_noray(oid) +static func host_lobby(name: String, address: String, max_players: int = 8) -> NohubResult.Lobby: + assert(_instance, "ForestBrawlConnector instance missing from Scene Tree!") + return await _instance._host_lobby(name, address, max_players) + +static func host_quick_play(address: String, max_players: int = 8) -> NohubResult.Lobby: + assert(_instance, "ForestBrawlConnector instance missing from Scene Tree!") + return await _instance._host_quick_play(address, max_players) + static func host_noray() -> Error: assert(_instance, "ForestBrawlConnector instance missing from Scene Tree!") return await _instance._host_noray() +static func update_player_count(player_count: int) -> void: + assert(_instance, "ForestBrawlConnector instance missing from Scene Tree!") + await _instance._update_player_count(player_count) + func _connect_to_services(p_noray_address: String, p_nohub_address: String) -> Error: _disconnect_from_services() @@ -80,26 +96,26 @@ func _connect_to_services(p_noray_address: String, p_nohub_address: String) -> E var nohub_address = _parse_address(p_nohub_address, 12980) # Connect to noray - print("Connecting to noray at %s:%d..." % [noray_address[0], noray_address[1]]) + _logger.info("Connecting to noray at %s:%d...", [noray_address[0], noray_address[1]]) var err := await Noray.connect_to_host(noray_address[0], noray_address[1]) if err != OK: - print("Failed to connect to noray: %s" % [error_string(err)]) + _logger.info("Failed to connect to noray: %s" % [error_string(err)]) _disconnect_from_services() return err - print("Success!") + _logger.info("Successfully connected to noray!") # Connect to nohub - print("Connecting to nohub at %s:%d..." % [noray_address[0], noray_address[1]]) + _logger.info("Connecting to nohub at %s:%d...", [noray_address[0], noray_address[1]]) var peer := StreamPeerTCP.new() peer.connect_to_host(nohub_address[0], nohub_address[1]) while true: peer.poll() match peer.get_status(): StreamPeerTCP.STATUS_CONNECTED: - print("Success!") + _logger.info("Successfully connected to nohub!") break StreamPeerTCP.STATUS_ERROR: - print("Failed to connect!") + _logger.info("Failed to connect to nohub!") _disconnect_from_services() return ERR_CONNECTION_ERROR await get_tree().process_frame @@ -108,28 +124,28 @@ func _connect_to_services(p_noray_address: String, p_nohub_address: String) -> E _nohub_client = NohubClient.new(peer) # Register with noray - print("Registering host with noray... ") + _logger.info("Registering host with noray... ") Noray.register_host() await Noray.on_pid - print("Success!") + _logger.info("Success!") - print("Registering remote with noray... ") + _logger.info("Registering remote with noray... ") err = await Noray.register_remote() if err != OK: - print("Failed registering remote address: %s" % error_string(err)) + _logger.info("Failed registering remote address: %s" % error_string(err)) _disconnect_from_services() return ERR_CANT_ACQUIRE_RESOURCE - print("Success!") + _logger.info("Success!") # Set GameID in nohub - print("Setting game ID with nohub... ") + _logger.info("Setting game ID with nohub... ") await get_tree().process_frame var response := await _nohub_client.set_game(GAME_ID) if not response.is_success(): - print("Failed to set game ID! %s" % [response]) + _logger.info("Failed to set game ID! %s" % [response]) _disconnect_from_services() return ERR_QUERY_FAILED - print("Success!") + _logger.info("Success!") # Success _noray_address = "%s:%d" % noray_address @@ -138,6 +154,8 @@ func _connect_to_services(p_noray_address: String, p_nohub_address: String) -> E return OK func _disconnect_from_services() -> void: + _hosted_lobby = null + if Noray.is_connected_to_host(): Noray.disconnect_from_host() _noray_address = "" @@ -154,6 +172,7 @@ func _is_connected_to_services() -> bool: func _join(address: String) -> Error: var uri := _parse_uri(address) if uri.is_empty(): + _logger.info("Failed to parse URI: %s", [address]) return ERR_PARSE_ERROR if uri["protocol"] == "noray": @@ -161,25 +180,65 @@ func _join(address: String) -> Error: var oid := uri["path"] as String return _join_noray(oid) - print("Unknown schema: %s" % [uri["protocol"]]) + _logger.info("Unknown schema: %s" % [uri["protocol"]]) return ERR_UNAVAILABLE func _join_noray(oid: String) -> Error: return _noray_connector.join(oid) +func _host_lobby(name: String, address: String, max_players: int = 8, extra_data: Dictionary = {}) -> NohubResult.Lobby: + if not _nohub_client: + return NohubResult.of_error("NotConnectedError", "No nohub client present!") + + # TODO(nohub.gd): Stringify data values + var base_data := { "name": name, "player-count": "0", "player-capacity": str(max_players) } + var data := extra_data.duplicate() + data.merge(base_data, true) + + var response := await _nohub_client.create_lobby(address, data) + if response.is_success(): + _hosted_lobby = response.value() + return response + +func _host_quick_play(address: String, max_players: int = 8) -> NohubResult.Lobby: + var name := "Quick Play #%x" % [randi_range(0x10000000, 0xFFFFFFFF)] + return await _host_lobby(name, address, max_players, { "quick-play": "enabled" }) + func _host_noray() -> Error: return await _noray_connector.host() +func _update_player_count(player_count: int) -> void: + if not _nohub_client: + return + if not _hosted_lobby: + return + + _hosted_lobby.data["player-count"] = str(player_count) + await _nohub_client.set_lobby_data(_hosted_lobby.id, _hosted_lobby.data) + +func _report_player_count() -> void: + if multiplayer.is_server(): + _update_player_count(multiplayer.get_peers().size() + 1) + func _ready(): _instance = self _noray_connector = ForestBrawlNorayConnector.new() add_child(_noray_connector) + NetworkEvents.on_peer_join.connect(func(__): _report_player_count()) + NetworkEvents.on_peer_leave.connect(func(__): _report_player_count()) + NetworkEvents.on_server_start.connect(func(): _report_player_count()) + NetworkEvents.on_server_stop.connect(func(): + if _hosted_lobby and _nohub_client: + await _nohub_client.delete_lobby(_hosted_lobby.id) + _hosted_lobby = null + ) + func _process(_dt) -> void: if _nohub_peer: var err := _nohub_peer.poll() if err != OK: - print("Failed polling nohub: ", error_string(err)) + _logger.info("Failed polling nohub: %s", [error_string(err)]) _disconnect_from_services() if _nohub_client: diff --git a/examples/forest-brawl/forest-brawl-noray-connector.gd b/examples/forest-brawl/forest-brawl-noray-connector.gd index 289e8ea9..c3001c1b 100644 --- a/examples/forest-brawl/forest-brawl-noray-connector.gd +++ b/examples/forest-brawl/forest-brawl-noray-connector.gd @@ -5,6 +5,8 @@ var _is_host := false var _is_client := false var _target_oid := "" +static var _logger := _NetfoxLogger.new("forest-brawl", "ForestBrawlNorayConnector") + func _ready(): Noray.on_connect_nat.connect(_handle_connect_nat) Noray.on_connect_relay.connect(_handle_connect_relay) @@ -16,16 +18,16 @@ func host() -> Error: # Start host var err = OK var port = Noray.local_port - print("Starting host on port %s" % port) + _logger.info("Starting host on port %s" % port) var peer = ENetMultiplayerPeer.new() err = peer.create_server(port) if err != OK: - print("Failed to listen on port %s: %s" % [port, error_string(err)]) + _logger.info("Failed to listen on port %s: %s" % [port, error_string(err)]) return err get_tree().get_multiplayer().multiplayer_peer = peer - print("Listening on port %s" % port) + _logger.info("Listening on port %s" % port) # Wait for server to start while peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTING: @@ -48,22 +50,26 @@ func join(oid: String, force_relay: bool = false) -> Error: _target_oid = oid if force_relay: + _logger.info("Connecting over relay to %s", [oid]) return Noray.connect_relay(oid) else: + _logger.info("Connecting over NAT to %s", [oid]) return Noray.connect_nat(oid) func _handle_connect_nat(address: String, port: int) -> Error: + _logger.info("Received NAT connect command to %s:%d", [address, port]) var err = await _handle_connect(address, port) # If client failed to connect over NAT, try again over relay if err != OK and not _is_host: - print("NAT connect failed with reason %s, retrying with relay" % error_string(err)) + _logger.info("NAT connect failed with reason %s, retrying with relay to %s", [error_string(err), _target_oid]) Noray.connect_relay(_target_oid) err = OK return err func _handle_connect_relay(address: String, port: int) -> Error: + _logger.info("Received relay connect command to %s:%d", [address, port]) return await _handle_connect(address, port) func _handle_connect(address: String, port: int) -> Error: @@ -73,7 +79,7 @@ func _handle_connect(address: String, port: int) -> Error: var err = OK if not _is_host and not _is_client: - push_warning("Refusing connection, not running as client nor host") + _logger.info("Refusing connection, not running as client nor host") err = ERR_UNAVAILABLE if _is_client: @@ -81,24 +87,24 @@ func _handle_connect(address: String, port: int) -> Error: udp.bind(Noray.local_port) udp.set_dest_address(address, port) - print("Attempting handshake with %s:%s" % [address, port]) + _logger.info("Attempting handshake with %s:%s" % [address, port]) err = await PacketHandshake.over_packet_peer(udp) udp.close() if err != OK: if err == ERR_BUSY: - print("Handshake to %s:%s succeeded partially, attempting connection anyway" % [address, port]) + _logger.info("Handshake to %s:%s succeeded partially, attempting connection anyway" % [address, port]) else: - print("Handshake to %s:%s failed: %s" % [address, port, error_string(err)]) + _logger.info("Handshake to %s:%s failed: %s" % [address, port, error_string(err)]) return err else: - print("Handshake to %s:%s succeeded" % [address, port]) + _logger.info("Handshake to %s:%s succeeded" % [address, port]) # Connect var peer = ENetMultiplayerPeer.new() err = peer.create_client(address, port, 0, 0, 0, Noray.local_port) if err != OK: - print("Failed to create client: %s" % error_string(err)) + _logger.info("Failed to create client: %s" % error_string(err)) return err get_tree().get_multiplayer().multiplayer_peer = peer @@ -109,7 +115,7 @@ func _handle_connect(address: String, port: int) -> Error: ) if peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED: - print("Failed to connect to %s:%s with status %s" % [address, port, peer.get_connection_status()]) + _logger.info("Failed to connect to %s:%s with status %s" % [address, port, peer.get_connection_status()]) get_tree().get_multiplayer().multiplayer_peer = null return ERR_CANT_CONNECT @@ -120,8 +126,8 @@ func _handle_connect(address: String, port: int) -> Error: err = await PacketHandshake.over_enet_peer(peer, address, port) if err != OK: - print("Handshake to %s:%s failed: %s" % [address, port, error_string(err)]) + _logger.info("Handshake to %s:%s failed: %s" % [address, port, error_string(err)]) return err - print("Handshake to %s:%s concluded" % [address, port]) + _logger.info("Handshake to %s:%s concluded" % [address, port]) return err diff --git a/examples/forest-brawl/forest-brawl.tscn b/examples/forest-brawl/forest-brawl.tscn index a4dbe6c3..d9e7b99e 100644 --- a/examples/forest-brawl/forest-brawl.tscn +++ b/examples/forest-brawl/forest-brawl.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=22 format=3 uid="uid://cwh2p0qb5872o"] +[gd_scene load_steps=15 format=3 uid="uid://cwh2p0qb5872o"] [ext_resource type="PackedScene" uid="uid://d1544gxqaoptc" path="res://examples/forest-brawl/maps/three-peaks.tscn" id="1_xksrt"] [ext_resource type="Script" path="res://examples/forest-brawl/forest-brawl-connector.gd" id="2_wafqi"] @@ -8,17 +8,10 @@ [ext_resource type="PackedScene" uid="uid://bpf1jdr255nr0" path="res://examples/shared/ui/time-display.tscn" id="9_d2tot"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/score-manager.gd" id="9_vxjwh"] [ext_resource type="Script" path="res://addons/netfox/tick-interpolator.gd" id="10_ld676"] -[ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/vsync-checkbutton.gd" id="11_4x74a"] -[ext_resource type="Script" path="res://examples/forest-brawl/scripts/random-name-input.gd" id="11_cf8pu"] [ext_resource type="PackedScene" uid="uid://b1vadi3ma8uiq" path="res://examples/forest-brawl/scenes/brawler-crown.tscn" id="11_eeeag"] -[ext_resource type="Script" path="res://examples/shared/scripts/noray-bootstrapper.gd" id="11_vpdh0"] [ext_resource type="Script" path="res://examples/forest-brawl/scripts/player-stats-display.gd" id="12_5kocp"] -[ext_resource type="Script" path="res://examples/shared/scripts/lan-bootstrapper.gd" id="12_gjc7i"] -[ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd" id="13_ujuuj"] [ext_resource type="PackedScene" uid="uid://ojh5xofoserg" path="res://examples/forest-brawl/scenes/score_screen.tscn" id="14_85lvt"] -[ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd" id="14_h1iqv"] [ext_resource type="PackedScene" uid="uid://dbnx63lgo7288" path="res://examples/forest-brawl/menu.tscn" id="16_3ljtn"] -[ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/volume-slider.gd" id="16_6pky3"] [ext_resource type="LabelSettings" uid="uid://b4u1aluftkajy" path="res://examples/forest-brawl/ui/player-stat-label.tres" id="17_48up8"] [sub_resource type="LabelSettings" id="LabelSettings_l686d"] @@ -33,14 +26,13 @@ font_size = 64 [node name="ForestBrawlConnector" type="Node" parent="Network"] script = ExtResource("2_wafqi") -[node name="Brawler Spawner" type="Node" parent="Network" node_paths=PackedStringArray("spawn_root", "camera", "joining_screen", "name_input")] +[node name="Brawler Spawner" type="Node" parent="Network" node_paths=PackedStringArray("spawn_root", "camera", "joining_screen")] unique_name_in_owner = true script = ExtResource("5_qv1fx") player_scene = ExtResource("7_tcy3g") spawn_root = NodePath("../../Players") camera = NodePath("../../Camera3D") joining_screen = NodePath("../../UI/Joining Screen") -name_input = NodePath("../../UI/Network Popup/Settings/Player Name/Player Name Input") [node name="Players" type="Node" parent="."] @@ -82,228 +74,6 @@ grow_horizontal = 0 grow_vertical = 1 horizontal_alignment = 2 -[node name="Network Popup" type="TabContainer" parent="UI"] -visible = false -custom_minimum_size = Vector2(320, 240) -layout_mode = 1 -anchors_preset = 8 -anchor_left = 0.5 -anchor_top = 0.5 -anchor_right = 0.5 -anchor_bottom = 0.5 -offset_left = -125.5 -offset_top = -105.0 -offset_right = 125.5 -offset_bottom = 105.0 -grow_horizontal = 2 -grow_vertical = 2 -use_hidden_tabs_for_min_size = true - -[node name="Settings" type="VBoxContainer" parent="UI/Network Popup"] -layout_mode = 2 -size_flags_vertical = 3 - -[node name="Player Name" type="HBoxContainer" parent="UI/Network Popup/Settings"] -layout_mode = 2 - -[node name="Player Name Label" type="Label" parent="UI/Network Popup/Settings/Player Name"] -layout_mode = 2 -text = "Player Name:" - -[node name="Player Name Input" type="LineEdit" parent="UI/Network Popup/Settings/Player Name"] -layout_mode = 2 -size_flags_horizontal = 3 -text = "Nameless Brawler" -clear_button_enabled = true -script = ExtResource("11_cf8pu") - -[node name="GridContainer" type="GridContainer" parent="UI/Network Popup/Settings"] -layout_mode = 2 -columns = 2 - -[node name="Fullscreen" type="HBoxContainer" parent="UI/Network Popup/Settings/GridContainer"] -layout_mode = 2 - -[node name="Fullscreen Label" type="Label" parent="UI/Network Popup/Settings/GridContainer/Fullscreen"] -layout_mode = 2 -text = "Fullscreen:" - -[node name="Fullscreen CheckButton" type="CheckButton" parent="UI/Network Popup/Settings/GridContainer/Fullscreen"] -layout_mode = 2 -script = ExtResource("14_h1iqv") - -[node name="V-Sync" type="HBoxContainer" parent="UI/Network Popup/Settings/GridContainer"] -layout_mode = 2 - -[node name="V-Sync Label" type="Label" parent="UI/Network Popup/Settings/GridContainer/V-Sync"] -layout_mode = 2 -text = "V-Sync:" - -[node name="V-Sync CheckButton" type="CheckButton" parent="UI/Network Popup/Settings/GridContainer/V-Sync"] -layout_mode = 2 -script = ExtResource("11_4x74a") - -[node name="Confine mouse" type="HBoxContainer" parent="UI/Network Popup/Settings/GridContainer"] -layout_mode = 2 - -[node name="Confine Mouse Label" type="Label" parent="UI/Network Popup/Settings/GridContainer/Confine mouse"] -layout_mode = 2 -text = "Confine mouse:" - -[node name="Confine Mouse CheckButton" type="CheckButton" parent="UI/Network Popup/Settings/GridContainer/Confine mouse"] -layout_mode = 2 -script = ExtResource("13_ujuuj") - -[node name="Volume" type="HBoxContainer" parent="UI/Network Popup/Settings"] -layout_mode = 2 - -[node name="Volume Label" type="Label" parent="UI/Network Popup/Settings/Volume"] -layout_mode = 2 -text = "Volume:" - -[node name="Volume Slider" type="HSlider" parent="UI/Network Popup/Settings/Volume"] -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 4 -value = 100.0 -script = ExtResource("16_6pky3") - -[node name="LAN" type="VBoxContainer" parent="UI/Network Popup"] -visible = false -layout_mode = 2 - -[node name="Address Row" type="HBoxContainer" parent="UI/Network Popup/LAN"] -layout_mode = 2 -size_flags_vertical = 2 - -[node name="Address Label" type="Label" parent="UI/Network Popup/LAN/Address Row"] -layout_mode = 2 -text = "Address:" - -[node name="Address LineEdit" type="LineEdit" parent="UI/Network Popup/LAN/Address Row"] -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 0 -text = "localhost" - -[node name="Port Label" type="Label" parent="UI/Network Popup/LAN/Address Row"] -layout_mode = 2 -text = "Port:" - -[node name="Port LineEdit" type="LineEdit" parent="UI/Network Popup/LAN/Address Row"] -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 0 -text = "16384" - -[node name="Actions Row" type="HBoxContainer" parent="UI/Network Popup/LAN"] -layout_mode = 2 -size_flags_horizontal = 4 - -[node name="Host Only Button" type="Button" parent="UI/Network Popup/LAN/Actions Row"] -layout_mode = 2 -size_flags_horizontal = 4 -text = "Host Only" - -[node name="Host Button" type="Button" parent="UI/Network Popup/LAN/Actions Row"] -layout_mode = 2 -size_flags_horizontal = 4 -text = "Host" - -[node name="Join Button" type="Button" parent="UI/Network Popup/LAN/Actions Row"] -layout_mode = 2 -size_flags_horizontal = 4 -text = "Join" - -[node name="Noray" type="VBoxContainer" parent="UI/Network Popup"] -visible = false -layout_mode = 2 - -[node name="Noray Address Row" type="HBoxContainer" parent="UI/Network Popup/Noray"] -layout_mode = 2 - -[node name="Address Label" type="Label" parent="UI/Network Popup/Noray/Noray Address Row"] -layout_mode = 2 -text = "noray host:" - -[node name="Address LineEdit" type="LineEdit" parent="UI/Network Popup/Noray/Noray Address Row"] -layout_mode = 2 -size_flags_horizontal = 3 -text = "tomfol.io:8890" -placeholder_text = "noray.example.com:8890" - -[node name="OID Row" type="HBoxContainer" parent="UI/Network Popup/Noray"] -layout_mode = 2 - -[node name="OID Label" type="Label" parent="UI/Network Popup/Noray/OID Row"] -layout_mode = 2 -text = "Open ID: " - -[node name="OID Value" type="LineEdit" parent="UI/Network Popup/Noray/OID Row"] -layout_mode = 2 -size_flags_horizontal = 3 -text = "123456789" -editable = false - -[node name="Noray Actions Row" type="HBoxContainer" parent="UI/Network Popup/Noray"] -layout_mode = 2 - -[node name="Connect Button" type="Button" parent="UI/Network Popup/Noray/Noray Actions Row"] -layout_mode = 2 -text = "Connect" - -[node name="Disconnect Button" type="Button" parent="UI/Network Popup/Noray/Noray Actions Row"] -layout_mode = 2 -text = "Disconnect" - -[node name="HSeparator" type="HSeparator" parent="UI/Network Popup/Noray"] -layout_mode = 2 - -[node name="Connect Host Row" type="HBoxContainer" parent="UI/Network Popup/Noray"] -layout_mode = 2 - -[node name="Host Label" type="Label" parent="UI/Network Popup/Noray/Connect Host Row"] -layout_mode = 2 -text = "Target Host: " - -[node name="Host LineEdit" type="LineEdit" parent="UI/Network Popup/Noray/Connect Host Row"] -layout_mode = 2 -size_flags_horizontal = 3 -placeholder_text = "Host OID" - -[node name="Connect Actions Row" type="HBoxContainer" parent="UI/Network Popup/Noray"] -layout_mode = 2 - -[node name="Host Only Button" type="Button" parent="UI/Network Popup/Noray/Connect Actions Row"] -layout_mode = 2 -text = "Host Only" - -[node name="Host Button" type="Button" parent="UI/Network Popup/Noray/Connect Actions Row"] -layout_mode = 2 -text = "Host" - -[node name="Join Button" type="Button" parent="UI/Network Popup/Noray/Connect Actions Row"] -layout_mode = 2 -text = "Join" - -[node name="Force Relay Checkbox" type="CheckBox" parent="UI/Network Popup/Noray/Connect Actions Row"] -layout_mode = 2 -text = "Force Relay" - -[node name="LAN Bootstrapper" type="Node" parent="UI/Network Popup" node_paths=PackedStringArray("connect_ui", "address_input", "port_input")] -script = ExtResource("12_gjc7i") -connect_ui = NodePath("..") -address_input = NodePath("../LAN/Address Row/Address LineEdit") -port_input = NodePath("../LAN/Address Row/Port LineEdit") - -[node name="Noray Bootstrapper" type="Node" parent="UI/Network Popup" node_paths=PackedStringArray("connect_ui", "noray_address_input", "oid_input", "host_oid_input", "force_relay_check")] -script = ExtResource("11_vpdh0") -connect_ui = NodePath("..") -noray_address_input = NodePath("../Noray/Noray Address Row/Address LineEdit") -oid_input = NodePath("../Noray/OID Row/OID Value") -host_oid_input = NodePath("../Noray/Connect Host Row/Host LineEdit") -force_relay_check = NodePath("../Noray/Connect Actions Row/Force Relay Checkbox") - [node name="Menu" parent="UI" instance=ExtResource("16_3ljtn")] layout_mode = 1 @@ -372,12 +142,3 @@ vertical_alignment = 1 [node name="Score Screen" parent="UI" instance=ExtResource("14_85lvt")] visible = false layout_mode = 1 - -[connection signal="pressed" from="UI/Network Popup/LAN/Actions Row/Host Only Button" to="UI/Network Popup/LAN Bootstrapper" method="host_only"] -[connection signal="pressed" from="UI/Network Popup/LAN/Actions Row/Host Button" to="UI/Network Popup/LAN Bootstrapper" method="host"] -[connection signal="pressed" from="UI/Network Popup/LAN/Actions Row/Join Button" to="UI/Network Popup/LAN Bootstrapper" method="join"] -[connection signal="pressed" from="UI/Network Popup/Noray/Noray Actions Row/Connect Button" to="UI/Network Popup/Noray Bootstrapper" method="connect_to_noray"] -[connection signal="pressed" from="UI/Network Popup/Noray/Noray Actions Row/Disconnect Button" to="UI/Network Popup/Noray Bootstrapper" method="disconnect_from_noray"] -[connection signal="pressed" from="UI/Network Popup/Noray/Connect Actions Row/Host Only Button" to="UI/Network Popup/Noray Bootstrapper" method="host_only"] -[connection signal="pressed" from="UI/Network Popup/Noray/Connect Actions Row/Host Button" to="UI/Network Popup/Noray Bootstrapper" method="host"] -[connection signal="pressed" from="UI/Network Popup/Noray/Connect Actions Row/Join Button" to="UI/Network Popup/Noray Bootstrapper" method="join"] diff --git a/examples/forest-brawl/menu.tscn b/examples/forest-brawl/menu.tscn index 65aa8324..cf5e9560 100644 --- a/examples/forest-brawl/menu.tscn +++ b/examples/forest-brawl/menu.tscn @@ -142,7 +142,7 @@ func _execute() -> void: label.text = \"Couldn't join %s: %s\" % [address, error_string(err)] func _cancel() -> void: - pass # TODO + pass func _back() -> void: _switch_to(main_menu) @@ -154,15 +154,8 @@ func _host() -> void: var lobby_name := \"Quick Play #%x\" % [randi_range(0x10000000, 0xFFFFFFFF)] var player_capacity := 8 var address := \"noray://%s/%s\" % [ForestBrawlConnector.noray_address(), Noray.oid] - # TODO(nohub.gd): Stringify data values - var data := { - \"name\": lobby_name, - \"player-count\": \"0\", - \"player-capacity\": str(player_capacity), - \"quick-play\": \"enabled\" - } - var response := await ForestBrawlConnector.nohub().create_lobby(address, data) + var response := await ForestBrawlConnector.host_quick_play(address, player_capacity) if not response.is_success(): label.text = \"Lobby fail: %s\" % [response.error().message] @@ -227,6 +220,9 @@ var _poll_wait := 0. var _is_hosting := false +func is_active() -> bool: + return is_visible_in_tree() + func _ready() -> void: # TODO: Deduplicate? visibility_changed.connect(func(): @@ -248,6 +244,9 @@ func _ready() -> void: _select_preset(0) func _process(dt: float) -> void: + if not is_active(): + return + # Poll lobbies _poll_wait -= dt if _poll_wait < 0.0 and ForestBrawlConnector.is_connected_to_services(): @@ -309,25 +308,28 @@ func _host() -> void: var lobby_limit := lobby_player_limit_input.text var noray_address := ForestBrawlConnector.noray_address() - var nohub := ForestBrawlConnector.nohub() if not lobby_name: print(\"Lobby name can't be empty!\") - elif not lobby_limit.is_valid_int(): + return + if not lobby_limit.is_valid_int(): print(\"Player limit is not a number!\") - elif int(lobby_limit) <= 0: + return + if int(lobby_limit) <= 0: print(\"Invalid player limit!\") + return + + var player_limit := int(lobby_limit) + var address := \"noray://%s/%s\" % [noray_address, Noray.oid] + + var response := await ForestBrawlConnector.host_lobby(lobby_name, address, player_limit) + if not response.is_success(): + print(\"Failed to create lobby! \", response) + return else: - var player_limit := int(lobby_limit) - var address := \"noray://%s/%s\" % [noray_address, Noray.oid] - var data := { \"name\": lobby_name, \"player-count\": \"0\", \"player-capacity\": str(player_limit)} - var response := await nohub.create_lobby(address, data) - if not response.is_success(): - print(\"Failed to create lobby!\", response) - else: - print(\"Created lobby!\", response.value()) - _poll_wait = -1. - + print(\"Created lobby! \", response.value()) + _poll_wait = -1. + var err := await ForestBrawlConnector.host_noray() if err != OK: prints(\"Failed to host game:\", error_string(err)) @@ -495,7 +497,6 @@ stretch_mode = 6 [node name="Main Menu" type="VBoxContainer" parent="."] unique_name_in_owner = true -visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -537,6 +538,7 @@ flat = true [node name="Quick Play Menu" type="VBoxContainer" parent="."] unique_name_in_owner = true +visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 diff --git a/examples/forest-brawl/scripts/brawler-spawner.gd b/examples/forest-brawl/scripts/brawler-spawner.gd index 37f37160..d1d22973 100644 --- a/examples/forest-brawl/scripts/brawler-spawner.gd +++ b/examples/forest-brawl/scripts/brawler-spawner.gd @@ -82,7 +82,7 @@ func _spawn(id: int) -> BrawlerController: GameEvents.on_own_brawler_spawn.emit(avatar) # Submit name - var player_name = name_input.text + var player_name = NameProvider.name() # TODO: Decouple from UI reference print("Submitting player name " + player_name) _submit_name.rpc(player_name) From 9cf6f91be1b0209b6ee71681e971c563c454e8b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Thu, 23 Oct 2025 12:12:13 +0200 Subject: [PATCH 09/13] rewrite settings --- .../forest-brawl/forest-brawl-settings.gd | 60 +++++++++++++ examples/forest-brawl/menu.tscn | 88 ++++++++++++++++--- .../forest-brawl/scripts/brawler-spawner.gd | 2 +- 3 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 examples/forest-brawl/forest-brawl-settings.gd diff --git a/examples/forest-brawl/forest-brawl-settings.gd b/examples/forest-brawl/forest-brawl-settings.gd new file mode 100644 index 00000000..24478d51 --- /dev/null +++ b/examples/forest-brawl/forest-brawl-settings.gd @@ -0,0 +1,60 @@ +extends RefCounted +class_name ForestBrawlSettings + +const DEFAULT_PATH = "user://settings.json" + +var player_name: String = NameProvider.name() +var full_screen: bool = false +var vsync: bool = true +var confine_mouse: bool = false +var master_volume: float = 1. + +static var _active: ForestBrawlSettings + +func to_dictionary() -> Dictionary: + return { + "player_name": player_name, + "full_screen": full_screen, + "vsync": vsync, + "confine_mouse": confine_mouse, + "master_volume": master_volume + } + +func serialize() -> String: + return JSON.stringify(to_dictionary(), " ") + +func save(path: String = DEFAULT_PATH) -> void: + var file := FileAccess.open(path, FileAccess.WRITE) + file.store_string(serialize()) + file.close() + +static func from_dictionary(data: Dictionary) -> ForestBrawlSettings: + var result := ForestBrawlSettings.new() + + result.player_name = data.get("player_name", result.player_name) + result.full_screen = data.get("full_screen", result.full_screen) + result.vsync = data.get("vsync", result.vsync) + result.confine_mouse = data.get("confine_mouse", result.confine_mouse) + result.master_volume = data.get("master_volume", result.master_volume) + + return result + +static func load(path: String = DEFAULT_PATH) -> ForestBrawlSettings: + if not FileAccess.file_exists(path): + return ForestBrawlSettings.new() + + var text := FileAccess.get_file_as_string(path) + var data = JSON.parse_string(text) + + if typeof(data) == TYPE_DICTIONARY: + return ForestBrawlSettings.from_dictionary(data as Dictionary) + else: + return ForestBrawlSettings.new() + +static func set_active(settings: ForestBrawlSettings) -> void: + _active = settings + +static func get_active() -> ForestBrawlSettings: + if not _active: + _active = ForestBrawlSettings.load() + return _active diff --git a/examples/forest-brawl/menu.tscn b/examples/forest-brawl/menu.tscn index cf5e9560..6b8bb079 100644 --- a/examples/forest-brawl/menu.tscn +++ b/examples/forest-brawl/menu.tscn @@ -1,13 +1,9 @@ -[gd_scene load_steps=17 format=3 uid="uid://dbnx63lgo7288"] +[gd_scene load_steps=13 format=3 uid="uid://dbnx63lgo7288"] [ext_resource type="Texture2D" uid="uid://cdb8di7e1p6h6" path="res://icon.png" id="1_6gbgt"] [ext_resource type="Theme" uid="uid://cg8p4yow3i3ly" path="res://examples/forest-brawl/ui/menu-theme.tres" id="1_xmgyy"] [ext_resource type="Texture2D" uid="uid://4vyxbqthy3nf" path="res://examples/forest-brawl/ui/gauss-bg.png" id="2_f45cx"] -[ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd" id="2_llkcl"] -[ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/vsync-checkbutton.gd" id="3_amefg"] -[ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd" id="4_s5qgy"] [ext_resource type="Texture2D" uid="uid://c6bp4x3j4l27k" path="res://examples/forest-brawl/ui/die-icon.svg" id="4_w03d4"] -[ext_resource type="Script" path="res://examples/forest-brawl/scripts/settings/volume-slider.gd" id="5_5ifsx"] [sub_resource type="GDScript" id="GDScript_hv8dj"] script/source = "extends Control @@ -455,12 +451,39 @@ script/source = "extends Control @onready var player_name_input := $\"MarginContainer/Settings VBox/Player Name/Player Name Input\" as LineEdit @onready var randomize_button := $\"MarginContainer/Settings VBox/Player Name/Randomize Button\" as Button +@onready var fullscreen_toggle := $\"MarginContainer/Settings VBox/GridContainer/Fullscreen CheckButton\" as CheckButton +@onready var vsync_toggle := $\"MarginContainer/Settings VBox/GridContainer/V-Sync CheckButton\" as CheckButton +@onready var confine_mouse_toggle := $\"MarginContainer/Settings VBox/GridContainer/Confine Mouse CheckButton\" as CheckButton +@onready var volume_slider := $\"MarginContainer/Settings VBox/Volume/Volume Slider\" as HSlider + +var _settings: ForestBrawlSettings func _ready() -> void: + visibility_changed.connect(func(): + if is_visible_in_tree(): _execute() + else: _cancel() + ) + + _settings = ForestBrawlSettings.load() + _apply_settings(_settings) + + player_name_input.text_changed.connect(func(__): _on_change()) + fullscreen_toggle.toggled.connect(func(__): _on_change()) + vsync_toggle.toggled.connect(func(__): _on_change()) + confine_mouse_toggle.toggled.connect(func(__): _on_change()) + volume_slider.changed.connect(func(__): _on_change()) + confirm_button.pressed.connect(_confirm) randomize_button.pressed.connect(_randomize_name) - _randomize_name() +func _execute() -> void: + _settings = ForestBrawlSettings.load() + _render_settings(_settings) + +func _cancel() -> void: + if _settings: + _settings.save() + ForestBrawlSettings.set_active(_settings) func _randomize_name(): player_name_input.text = NameProvider.name() @@ -468,6 +491,53 @@ func _randomize_name(): func _confirm() -> void: _switch_to(main_menu) +func _on_change() -> void: + _settings = _read_settings() + _apply_settings(_settings) + +func _render_settings(settings: ForestBrawlSettings) -> void: + player_name_input.text = settings.player_name + fullscreen_toggle.set_pressed_no_signal(settings.full_screen) + vsync_toggle.set_pressed_no_signal(settings.vsync) + confine_mouse_toggle.set_pressed_no_signal(settings.confine_mouse) + volume_slider.set_value_no_signal(lerpf(volume_slider.min_value, volume_slider.max_value, settings.master_volume)) + +func _read_settings() -> ForestBrawlSettings: + var settings := ForestBrawlSettings.new() + + settings.full_screen = fullscreen_toggle.button_pressed + settings.vsync = vsync_toggle.button_pressed + settings.confine_mouse = confine_mouse_toggle.button_pressed + settings.master_volume = inverse_lerp(volume_slider.min_value, volume_slider.max_value, volume_slider.value) + + return settings + +func _apply_settings(settings: ForestBrawlSettings) -> void: + # Full screen + if settings.full_screen: + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN) + else: + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + + # V-sync + if settings.vsync: + DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ADAPTIVE) + else: + DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED) + + # Confine mouse + if settings.confine_mouse: + DisplayServer.mouse_set_mode(DisplayServer.MOUSE_MODE_CONFINED) + else: + DisplayServer.mouse_set_mode(DisplayServer.MOUSE_MODE_VISIBLE) + + # Volume + var volume = lerp(-60, 0, settings.master_volume) + var mute = volume < -59.5 + + AudioServer.set_bus_volume_db(0, volume) + AudioServer.set_bus_mute(0, mute) + # TODO: Deduplicate? func _switch_to(menu: Control) -> void: menu.show() @@ -497,6 +567,7 @@ stretch_mode = 6 [node name="Main Menu" type="VBoxContainer" parent="."] unique_name_in_owner = true +visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -807,7 +878,6 @@ text = "Host" [node name="Settings Menu" type="PanelContainer" parent="."] unique_name_in_owner = true -visible = false custom_minimum_size = Vector2(480, 320) layout_mode = 1 anchors_preset = 8 @@ -865,7 +935,6 @@ text = "Fullscreen:" [node name="Fullscreen CheckButton" type="CheckButton" parent="Settings Menu/MarginContainer/Settings VBox/GridContainer"] layout_mode = 2 -script = ExtResource("2_llkcl") [node name="V-Sync Label" type="Label" parent="Settings Menu/MarginContainer/Settings VBox/GridContainer"] layout_mode = 2 @@ -873,7 +942,6 @@ text = "V-Sync:" [node name="V-Sync CheckButton" type="CheckButton" parent="Settings Menu/MarginContainer/Settings VBox/GridContainer"] layout_mode = 2 -script = ExtResource("3_amefg") [node name="Confine Mouse Label" type="Label" parent="Settings Menu/MarginContainer/Settings VBox/GridContainer"] layout_mode = 2 @@ -881,7 +949,6 @@ text = "Confine mouse:" [node name="Confine Mouse CheckButton" type="CheckButton" parent="Settings Menu/MarginContainer/Settings VBox/GridContainer"] layout_mode = 2 -script = ExtResource("4_s5qgy") [node name="HSeparator2" type="HSeparator" parent="Settings Menu/MarginContainer/Settings VBox"] layout_mode = 2 @@ -900,7 +967,6 @@ size_flags_horizontal = 3 size_flags_vertical = 4 size_flags_stretch_ratio = 3.0 value = 100.0 -script = ExtResource("5_5ifsx") [node name="HBoxContainer" type="HBoxContainer" parent="Settings Menu/MarginContainer/Settings VBox"] layout_mode = 2 diff --git a/examples/forest-brawl/scripts/brawler-spawner.gd b/examples/forest-brawl/scripts/brawler-spawner.gd index d1d22973..c9591105 100644 --- a/examples/forest-brawl/scripts/brawler-spawner.gd +++ b/examples/forest-brawl/scripts/brawler-spawner.gd @@ -82,7 +82,7 @@ func _spawn(id: int) -> BrawlerController: GameEvents.on_own_brawler_spawn.emit(avatar) # Submit name - var player_name = NameProvider.name() # TODO: Decouple from UI reference + var player_name = ForestBrawlSettings.get_active().player_name print("Submitting player name " + player_name) _submit_name.rpc(player_name) From f516d4b47fc43ed4f28a1eb810c1224ed5afae56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Thu, 23 Oct 2025 12:12:24 +0200 Subject: [PATCH 10/13] remove unused settings scripts --- .../scripts/settings/confine-mouse-checkbutton.gd | 7 ------- .../scripts/settings/confine-mouse-checkbutton.gd.uid | 1 - .../scripts/settings/fullscreen-checkbutton.gd | 7 ------- .../scripts/settings/fullscreen-checkbutton.gd.uid | 1 - examples/forest-brawl/scripts/settings/volume-slider.gd | 9 --------- .../forest-brawl/scripts/settings/volume-slider.gd.uid | 1 - .../forest-brawl/scripts/settings/vsync-checkbutton.gd | 7 ------- .../scripts/settings/vsync-checkbutton.gd.uid | 1 - 8 files changed, 34 deletions(-) delete mode 100644 examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd delete mode 100644 examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd.uid delete mode 100644 examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd delete mode 100644 examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd.uid delete mode 100644 examples/forest-brawl/scripts/settings/volume-slider.gd delete mode 100644 examples/forest-brawl/scripts/settings/volume-slider.gd.uid delete mode 100644 examples/forest-brawl/scripts/settings/vsync-checkbutton.gd delete mode 100644 examples/forest-brawl/scripts/settings/vsync-checkbutton.gd.uid diff --git a/examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd b/examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd deleted file mode 100644 index cf9c24b0..00000000 --- a/examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd +++ /dev/null @@ -1,7 +0,0 @@ -extends CheckButton - -func _toggled(toggle): - if toggle: - DisplayServer.mouse_set_mode(DisplayServer.MOUSE_MODE_CONFINED) - else: - DisplayServer.mouse_set_mode(DisplayServer.MOUSE_MODE_VISIBLE) diff --git a/examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd.uid b/examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd.uid deleted file mode 100644 index d969b2ff..00000000 --- a/examples/forest-brawl/scripts/settings/confine-mouse-checkbutton.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://nvr3jr0bviin diff --git a/examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd b/examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd deleted file mode 100644 index dc3e588d..00000000 --- a/examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd +++ /dev/null @@ -1,7 +0,0 @@ -extends CheckButton - -func _toggled(toggle): - if toggle: - DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN) - else: - DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) diff --git a/examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd.uid b/examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd.uid deleted file mode 100644 index 13209d26..00000000 --- a/examples/forest-brawl/scripts/settings/fullscreen-checkbutton.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://doidsx4hyb4gd diff --git a/examples/forest-brawl/scripts/settings/volume-slider.gd b/examples/forest-brawl/scripts/settings/volume-slider.gd deleted file mode 100644 index 4731d25c..00000000 --- a/examples/forest-brawl/scripts/settings/volume-slider.gd +++ /dev/null @@ -1,9 +0,0 @@ -extends HSlider - -func _value_changed(new_value): - var f = new_value / 100.0 - var volume = lerp(-60, 0, f) - var mute = f < 0.01 - - AudioServer.set_bus_volume_db(0, volume) - AudioServer.set_bus_mute(0, mute) diff --git a/examples/forest-brawl/scripts/settings/volume-slider.gd.uid b/examples/forest-brawl/scripts/settings/volume-slider.gd.uid deleted file mode 100644 index b3cd9292..00000000 --- a/examples/forest-brawl/scripts/settings/volume-slider.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ghfyw0kak8vn diff --git a/examples/forest-brawl/scripts/settings/vsync-checkbutton.gd b/examples/forest-brawl/scripts/settings/vsync-checkbutton.gd deleted file mode 100644 index ded00238..00000000 --- a/examples/forest-brawl/scripts/settings/vsync-checkbutton.gd +++ /dev/null @@ -1,7 +0,0 @@ -extends CheckButton - -func _toggled(toggle): - if toggle: - DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ADAPTIVE) - else: - DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED) diff --git a/examples/forest-brawl/scripts/settings/vsync-checkbutton.gd.uid b/examples/forest-brawl/scripts/settings/vsync-checkbutton.gd.uid deleted file mode 100644 index fc4f7efa..00000000 --- a/examples/forest-brawl/scripts/settings/vsync-checkbutton.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://gyiplrcda6mg From e1ca5168a8ae87adff7555f7b116e63653ccb578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Thu, 23 Oct 2025 12:30:37 +0200 Subject: [PATCH 11/13] more settings --- .../forest-brawl/forest-brawl-connector.gd | 2 +- .../forest-brawl/forest-brawl-settings.gd | 6 +++++ examples/forest-brawl/menu.tscn | 24 +++++++++++++++++++ .../forest-brawl/scripts/brawler-spawner.gd | 3 ++- .../forest-brawl/scripts/name-provider.gd | 2 +- 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/examples/forest-brawl/forest-brawl-connector.gd b/examples/forest-brawl/forest-brawl-connector.gd index a36d0917..044efea8 100644 --- a/examples/forest-brawl/forest-brawl-connector.gd +++ b/examples/forest-brawl/forest-brawl-connector.gd @@ -184,7 +184,7 @@ func _join(address: String) -> Error: return ERR_UNAVAILABLE func _join_noray(oid: String) -> Error: - return _noray_connector.join(oid) + return _noray_connector.join(oid, ForestBrawlSettings.get_active().force_relay) func _host_lobby(name: String, address: String, max_players: int = 8, extra_data: Dictionary = {}) -> NohubResult.Lobby: if not _nohub_client: diff --git a/examples/forest-brawl/forest-brawl-settings.gd b/examples/forest-brawl/forest-brawl-settings.gd index 24478d51..634da27a 100644 --- a/examples/forest-brawl/forest-brawl-settings.gd +++ b/examples/forest-brawl/forest-brawl-settings.gd @@ -4,6 +4,8 @@ class_name ForestBrawlSettings const DEFAULT_PATH = "user://settings.json" var player_name: String = NameProvider.name() +var randomize_name: bool = false +var force_relay: bool = false var full_screen: bool = false var vsync: bool = true var confine_mouse: bool = false @@ -14,6 +16,8 @@ static var _active: ForestBrawlSettings func to_dictionary() -> Dictionary: return { "player_name": player_name, + "randomize_name": randomize_name, + "force_relay": force_relay, "full_screen": full_screen, "vsync": vsync, "confine_mouse": confine_mouse, @@ -32,6 +36,8 @@ static func from_dictionary(data: Dictionary) -> ForestBrawlSettings: var result := ForestBrawlSettings.new() result.player_name = data.get("player_name", result.player_name) + result.randomize_name = data.get("randomize_name", result.randomize_name) + result.force_relay = data.get("force_relay", result.force_relay) result.full_screen = data.get("full_screen", result.full_screen) result.vsync = data.get("vsync", result.vsync) result.confine_mouse = data.get("confine_mouse", result.confine_mouse) diff --git a/examples/forest-brawl/menu.tscn b/examples/forest-brawl/menu.tscn index 6b8bb079..19a94485 100644 --- a/examples/forest-brawl/menu.tscn +++ b/examples/forest-brawl/menu.tscn @@ -451,6 +451,8 @@ script/source = "extends Control @onready var player_name_input := $\"MarginContainer/Settings VBox/Player Name/Player Name Input\" as LineEdit @onready var randomize_button := $\"MarginContainer/Settings VBox/Player Name/Randomize Button\" as Button +@onready var always_randomize_checkbox := $\"MarginContainer/Settings VBox/Always Randomize CheckBox\" as CheckBox +@onready var force_relay_checkbox := $\"MarginContainer/Settings VBox/Force relay CheckBox\" @onready var fullscreen_toggle := $\"MarginContainer/Settings VBox/GridContainer/Fullscreen CheckButton\" as CheckButton @onready var vsync_toggle := $\"MarginContainer/Settings VBox/GridContainer/V-Sync CheckButton\" as CheckButton @onready var confine_mouse_toggle := $\"MarginContainer/Settings VBox/GridContainer/Confine Mouse CheckButton\" as CheckButton @@ -468,6 +470,8 @@ func _ready() -> void: _apply_settings(_settings) player_name_input.text_changed.connect(func(__): _on_change()) + always_randomize_checkbox.toggled.connect(func(__): _on_change()) + force_relay_checkbox.toggled.connect(func(__): _on_change()) fullscreen_toggle.toggled.connect(func(__): _on_change()) vsync_toggle.toggled.connect(func(__): _on_change()) confine_mouse_toggle.toggled.connect(func(__): _on_change()) @@ -484,6 +488,7 @@ func _cancel() -> void: if _settings: _settings.save() ForestBrawlSettings.set_active(_settings) + print(\"Saved settings: %s\" % _settings.serialize()) func _randomize_name(): player_name_input.text = NameProvider.name() @@ -497,6 +502,8 @@ func _on_change() -> void: func _render_settings(settings: ForestBrawlSettings) -> void: player_name_input.text = settings.player_name + always_randomize_checkbox.set_pressed_no_signal(settings.randomize_name) + force_relay_checkbox.set_pressed_no_signal(settings.force_relay) fullscreen_toggle.set_pressed_no_signal(settings.full_screen) vsync_toggle.set_pressed_no_signal(settings.vsync) confine_mouse_toggle.set_pressed_no_signal(settings.confine_mouse) @@ -505,6 +512,9 @@ func _render_settings(settings: ForestBrawlSettings) -> void: func _read_settings() -> ForestBrawlSettings: var settings := ForestBrawlSettings.new() + settings.player_name = player_name_input.text + settings.randomize_name = always_randomize_checkbox.button_pressed + settings.force_relay = force_relay_checkbox.button_pressed settings.full_screen = fullscreen_toggle.button_pressed settings.vsync = vsync_toggle.button_pressed settings.confine_mouse = confine_mouse_toggle.button_pressed @@ -513,6 +523,9 @@ func _read_settings() -> ForestBrawlSettings: return settings func _apply_settings(settings: ForestBrawlSettings) -> void: + # Randomize name + player_name_input.editable = not settings.randomize_name + # Full screen if settings.full_screen: DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN) @@ -918,11 +931,21 @@ clear_button_enabled = true [node name="Randomize Button" type="Button" parent="Settings Menu/MarginContainer/Settings VBox/Player Name"] custom_minimum_size = Vector2(32, 0) layout_mode = 2 +tooltip_text = "Generate a random name" icon = ExtResource("4_w03d4") icon_alignment = 1 expand_icon = true +[node name="Always Randomize CheckBox" type="CheckBox" parent="Settings Menu/MarginContainer/Settings VBox"] +layout_mode = 2 +text = "Always randomize name" + +[node name="Force relay CheckBox" type="CheckBox" parent="Settings Menu/MarginContainer/Settings VBox"] +layout_mode = 2 +text = "Force relay" + [node name="HSeparator" type="HSeparator" parent="Settings Menu/MarginContainer/Settings VBox"] +visible = false layout_mode = 2 [node name="GridContainer" type="GridContainer" parent="Settings Menu/MarginContainer/Settings VBox"] @@ -951,6 +974,7 @@ text = "Confine mouse:" layout_mode = 2 [node name="HSeparator2" type="HSeparator" parent="Settings Menu/MarginContainer/Settings VBox"] +visible = false layout_mode = 2 [node name="Volume" type="HBoxContainer" parent="Settings Menu/MarginContainer/Settings VBox"] diff --git a/examples/forest-brawl/scripts/brawler-spawner.gd b/examples/forest-brawl/scripts/brawler-spawner.gd index c9591105..fbb32576 100644 --- a/examples/forest-brawl/scripts/brawler-spawner.gd +++ b/examples/forest-brawl/scripts/brawler-spawner.gd @@ -82,7 +82,8 @@ func _spawn(id: int) -> BrawlerController: GameEvents.on_own_brawler_spawn.emit(avatar) # Submit name - var player_name = ForestBrawlSettings.get_active().player_name + var settings := ForestBrawlSettings.get_active() + var player_name = NameProvider.name() if settings.randomize_name else settings.player_name print("Submitting player name " + player_name) _submit_name.rpc(player_name) diff --git a/examples/forest-brawl/scripts/name-provider.gd b/examples/forest-brawl/scripts/name-provider.gd index 17f28eba..85dc9381 100644 --- a/examples/forest-brawl/scripts/name-provider.gd +++ b/examples/forest-brawl/scripts/name-provider.gd @@ -7,7 +7,7 @@ static var _animals: PackedStringArray static func _pick_random(from: PackedStringArray) -> String: return from[randi_range(0, from.size()-1)] -static func name(): +static func name() -> String: return ("%s %s" % [ NameProvider._pick_random(NameProvider._adjectives), NameProvider._pick_random(NameProvider._animals) From bfd4314f3da2e39a5920f8cc3cc7ebc3381c48a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Thu, 23 Oct 2025 14:07:38 +0200 Subject: [PATCH 12/13] uids --- addons/nohub.gd/lobby.gd.uid | 1 + addons/nohub.gd/nohub.gd.uid | 1 + addons/nohub.gd/nohub_client.gd.uid | 1 + addons/nohub.gd/result.gd.uid | 1 + examples/forest-brawl/forest-brawl-connector.gd.uid | 1 + examples/forest-brawl/forest-brawl-noray-connector.gd.uid | 1 + examples/forest-brawl/forest-brawl-settings.gd.uid | 1 + 7 files changed, 7 insertions(+) create mode 100644 addons/nohub.gd/lobby.gd.uid create mode 100644 addons/nohub.gd/nohub.gd.uid create mode 100644 addons/nohub.gd/nohub_client.gd.uid create mode 100644 addons/nohub.gd/result.gd.uid create mode 100644 examples/forest-brawl/forest-brawl-connector.gd.uid create mode 100644 examples/forest-brawl/forest-brawl-noray-connector.gd.uid create mode 100644 examples/forest-brawl/forest-brawl-settings.gd.uid diff --git a/addons/nohub.gd/lobby.gd.uid b/addons/nohub.gd/lobby.gd.uid new file mode 100644 index 00000000..f7c7159f --- /dev/null +++ b/addons/nohub.gd/lobby.gd.uid @@ -0,0 +1 @@ +uid://darwb07a50hht diff --git a/addons/nohub.gd/nohub.gd.uid b/addons/nohub.gd/nohub.gd.uid new file mode 100644 index 00000000..f5e1d8af --- /dev/null +++ b/addons/nohub.gd/nohub.gd.uid @@ -0,0 +1 @@ +uid://c4r2pqwp1rtml diff --git a/addons/nohub.gd/nohub_client.gd.uid b/addons/nohub.gd/nohub_client.gd.uid new file mode 100644 index 00000000..1e3acb0c --- /dev/null +++ b/addons/nohub.gd/nohub_client.gd.uid @@ -0,0 +1 @@ +uid://b5po2uj4gudcc diff --git a/addons/nohub.gd/result.gd.uid b/addons/nohub.gd/result.gd.uid new file mode 100644 index 00000000..a3955c78 --- /dev/null +++ b/addons/nohub.gd/result.gd.uid @@ -0,0 +1 @@ +uid://cv4r0rosao3v2 diff --git a/examples/forest-brawl/forest-brawl-connector.gd.uid b/examples/forest-brawl/forest-brawl-connector.gd.uid new file mode 100644 index 00000000..b5f6a8d8 --- /dev/null +++ b/examples/forest-brawl/forest-brawl-connector.gd.uid @@ -0,0 +1 @@ +uid://clnp3t5017c1h diff --git a/examples/forest-brawl/forest-brawl-noray-connector.gd.uid b/examples/forest-brawl/forest-brawl-noray-connector.gd.uid new file mode 100644 index 00000000..36250585 --- /dev/null +++ b/examples/forest-brawl/forest-brawl-noray-connector.gd.uid @@ -0,0 +1 @@ +uid://chjoyedqttcl6 diff --git a/examples/forest-brawl/forest-brawl-settings.gd.uid b/examples/forest-brawl/forest-brawl-settings.gd.uid new file mode 100644 index 00000000..7ee5364a --- /dev/null +++ b/examples/forest-brawl/forest-brawl-settings.gd.uid @@ -0,0 +1 @@ +uid://cvvregmiprsqt From 3cdbd40fe4b62fd787624e19d2a970a8854bbd02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Thu, 23 Oct 2025 19:14:32 +0200 Subject: [PATCH 13/13] UI sounds --- examples/forest-brawl/menu.tscn | 46 +++++++++++++++++- examples/forest-brawl/sounds/attribution.md | 27 ++++++++++ .../forest-brawl/sounds/click/click_001.ogg | Bin 0 -> 4876 bytes .../sounds/click/click_001.ogg.import | 19 ++++++++ .../forest-brawl/sounds/click/click_002.ogg | Bin 0 -> 4275 bytes .../sounds/click/click_002.ogg.import | 19 ++++++++ .../forest-brawl/sounds/click/click_003.ogg | Bin 0 -> 4371 bytes .../sounds/click/click_003.ogg.import | 19 ++++++++ .../forest-brawl/sounds/click/click_004.ogg | Bin 0 -> 4486 bytes .../sounds/click/click_004.ogg.import | 19 ++++++++ .../forest-brawl/sounds/click/click_005.ogg | Bin 0 -> 4410 bytes .../sounds/click/click_005.ogg.import | 19 ++++++++ .../forest-brawl/sounds/switch/switch1.ogg | Bin 0 -> 6104 bytes .../sounds/switch/switch1.ogg.import | 19 ++++++++ .../forest-brawl/sounds/switch/switch2.ogg | Bin 0 -> 6042 bytes .../sounds/switch/switch2.ogg.import | 19 ++++++++ .../forest-brawl/sounds/switch/switch3.ogg | Bin 0 -> 6270 bytes .../sounds/switch/switch3.ogg.import | 19 ++++++++ .../forest-brawl/sounds/switch/switch4.ogg | Bin 0 -> 6477 bytes .../sounds/switch/switch4.ogg.import | 19 ++++++++ .../forest-brawl/sounds/switch/switch5.ogg | Bin 0 -> 6181 bytes .../sounds/switch/switch5.ogg.import | 19 ++++++++ .../forest-brawl/sounds/switch/switch6.ogg | Bin 0 -> 6610 bytes .../sounds/switch/switch6.ogg.import | 19 ++++++++ .../forest-brawl/sounds/switch/switch7.ogg | Bin 0 -> 5707 bytes .../sounds/switch/switch7.ogg.import | 19 ++++++++ .../forest-brawl/sounds/switch/switch8.ogg | Bin 0 -> 6253 bytes .../sounds/switch/switch8.ogg.import | 19 ++++++++ 28 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 examples/forest-brawl/sounds/click/click_001.ogg create mode 100644 examples/forest-brawl/sounds/click/click_001.ogg.import create mode 100644 examples/forest-brawl/sounds/click/click_002.ogg create mode 100644 examples/forest-brawl/sounds/click/click_002.ogg.import create mode 100644 examples/forest-brawl/sounds/click/click_003.ogg create mode 100644 examples/forest-brawl/sounds/click/click_003.ogg.import create mode 100644 examples/forest-brawl/sounds/click/click_004.ogg create mode 100644 examples/forest-brawl/sounds/click/click_004.ogg.import create mode 100644 examples/forest-brawl/sounds/click/click_005.ogg create mode 100644 examples/forest-brawl/sounds/click/click_005.ogg.import create mode 100644 examples/forest-brawl/sounds/switch/switch1.ogg create mode 100644 examples/forest-brawl/sounds/switch/switch1.ogg.import create mode 100644 examples/forest-brawl/sounds/switch/switch2.ogg create mode 100644 examples/forest-brawl/sounds/switch/switch2.ogg.import create mode 100644 examples/forest-brawl/sounds/switch/switch3.ogg create mode 100644 examples/forest-brawl/sounds/switch/switch3.ogg.import create mode 100644 examples/forest-brawl/sounds/switch/switch4.ogg create mode 100644 examples/forest-brawl/sounds/switch/switch4.ogg.import create mode 100644 examples/forest-brawl/sounds/switch/switch5.ogg create mode 100644 examples/forest-brawl/sounds/switch/switch5.ogg.import create mode 100644 examples/forest-brawl/sounds/switch/switch6.ogg create mode 100644 examples/forest-brawl/sounds/switch/switch6.ogg.import create mode 100644 examples/forest-brawl/sounds/switch/switch7.ogg create mode 100644 examples/forest-brawl/sounds/switch/switch7.ogg.import create mode 100644 examples/forest-brawl/sounds/switch/switch8.ogg create mode 100644 examples/forest-brawl/sounds/switch/switch8.ogg.import diff --git a/examples/forest-brawl/menu.tscn b/examples/forest-brawl/menu.tscn index 19a94485..44cb1b50 100644 --- a/examples/forest-brawl/menu.tscn +++ b/examples/forest-brawl/menu.tscn @@ -1,13 +1,31 @@ -[gd_scene load_steps=13 format=3 uid="uid://dbnx63lgo7288"] +[gd_scene load_steps=26 format=3 uid="uid://dbnx63lgo7288"] [ext_resource type="Texture2D" uid="uid://cdb8di7e1p6h6" path="res://icon.png" id="1_6gbgt"] [ext_resource type="Theme" uid="uid://cg8p4yow3i3ly" path="res://examples/forest-brawl/ui/menu-theme.tres" id="1_xmgyy"] [ext_resource type="Texture2D" uid="uid://4vyxbqthy3nf" path="res://examples/forest-brawl/ui/gauss-bg.png" id="2_f45cx"] +[ext_resource type="AudioStream" uid="uid://b1tjkexft6b6p" path="res://examples/forest-brawl/sounds/switch/switch1.ogg" id="2_fg4qs"] +[ext_resource type="AudioStream" uid="uid://cr7ui6x21ywtf" path="res://examples/forest-brawl/sounds/switch/switch2.ogg" id="3_x70pt"] +[ext_resource type="AudioStream" uid="uid://b175yiuxb4out" path="res://examples/forest-brawl/sounds/switch/switch3.ogg" id="4_gji41"] [ext_resource type="Texture2D" uid="uid://c6bp4x3j4l27k" path="res://examples/forest-brawl/ui/die-icon.svg" id="4_w03d4"] +[ext_resource type="AudioStream" uid="uid://br4qdiu4g7uuc" path="res://examples/forest-brawl/sounds/switch/switch4.ogg" id="5_6auhi"] +[ext_resource type="AudioStream" uid="uid://ct4mldhp1j0p1" path="res://examples/forest-brawl/sounds/switch/switch5.ogg" id="6_47cu0"] +[ext_resource type="AudioStream" uid="uid://b51qiut02m5hq" path="res://examples/forest-brawl/sounds/switch/switch6.ogg" id="7_g4pxc"] +[ext_resource type="AudioStream" uid="uid://cg2pl8kegpluj" path="res://examples/forest-brawl/sounds/switch/switch7.ogg" id="8_si12f"] +[ext_resource type="AudioStream" uid="uid://cwpvdoq22ewxj" path="res://examples/forest-brawl/sounds/switch/switch8.ogg" id="9_j72q5"] +[ext_resource type="AudioStream" uid="uid://vopvgimi40ci" path="res://examples/forest-brawl/sounds/click/click_001.ogg" id="10_xypl4"] +[ext_resource type="AudioStream" uid="uid://d4fnb7fq15ud7" path="res://examples/forest-brawl/sounds/click/click_002.ogg" id="11_6mefx"] +[ext_resource type="AudioStream" uid="uid://xm1rrumqubqo" path="res://examples/forest-brawl/sounds/click/click_003.ogg" id="12_880g4"] +[ext_resource type="AudioStream" uid="uid://pya5p1mgjj0r" path="res://examples/forest-brawl/sounds/click/click_004.ogg" id="13_iojt7"] +[ext_resource type="AudioStream" uid="uid://dgxn16sobxnj6" path="res://examples/forest-brawl/sounds/click/click_005.ogg" id="14_uoh15"] [sub_resource type="GDScript" id="GDScript_hv8dj"] script/source = "extends Control +@onready var audio_player := %\"UI Audio\" as AudioStreamPlayer + +@export var toggle_sounds: Array[AudioStream] = [] +@export var click_sounds: Array[AudioStream] = [] + func _ready(): # Hide when game starts NetworkEvents.on_client_start.connect(func(__): hide()) @@ -19,10 +37,29 @@ func _ready(): # Make sure the main menu is visible by default for child in get_children(): - child.hide() + if child is Control: + child.hide() for i in range(2): get_child(i).show() + + # Play a sound when something is toggled + for control in find_children(\"*\", \"Control\"): + if control is CheckButton or control is CheckBox: + var toggle := control as Button + toggle.toggled.connect(func(__): _play_random_sfx(toggle_sounds, -4)) + elif control is Button: + var button := control as Button + button.pressed.connect(func(): _play_random_sfx(click_sounds, +8)) + +func _play_random_sfx(pool: Array[AudioStream], gain_db: float = 0.0) -> void: + if pool.is_empty(): + return + + var sfx := pool.pick_random() as AudioStream + audio_player.stream = sfx + audio_player.volume_db = gain_db + audio_player.play() " [sub_resource type="GDScript" id="GDScript_jmdql"] @@ -566,6 +603,8 @@ grow_horizontal = 2 grow_vertical = 2 theme = ExtResource("1_xmgyy") script = SubResource("GDScript_hv8dj") +toggle_sounds = Array[AudioStream]([ExtResource("2_fg4qs"), ExtResource("3_x70pt"), ExtResource("4_gji41"), ExtResource("5_6auhi"), ExtResource("6_47cu0"), ExtResource("7_g4pxc"), ExtResource("8_si12f"), ExtResource("9_j72q5")]) +click_sounds = Array[AudioStream]([ExtResource("10_xypl4"), ExtResource("11_6mefx"), ExtResource("12_880g4"), ExtResource("13_iojt7"), ExtResource("14_uoh15")]) [node name="TextureRect" type="TextureRect" parent="."] modulate = Color(0, 0, 0, 0.501961) @@ -1000,3 +1039,6 @@ alignment = 1 [node name="Confirm Button" type="Button" parent="Settings Menu/MarginContainer/Settings VBox/HBoxContainer"] layout_mode = 2 text = "Confirm" + +[node name="UI Audio" type="AudioStreamPlayer" parent="."] +unique_name_in_owner = true diff --git a/examples/forest-brawl/sounds/attribution.md b/examples/forest-brawl/sounds/attribution.md index 30a0121c..8b8f743d 100644 --- a/examples/forest-brawl/sounds/attribution.md +++ b/examples/forest-brawl/sounds/attribution.md @@ -109,6 +109,31 @@ Files: *"Slide Whistle, Descending, A.wav"* by [InspectorJ], under the [Attribution 4.0] license. No changes were made to the original sound effect. +## Switch sounds + +[Source](https://kenney.nl/assets/ui-audio) + +Played in menus when toggling items. + +Files: +* `switch/*` + +*UI Audio* by [Kenney], under the [Creative Commons CC0] license. No changes +were made to the original sound effect. + +## Click sounds + +[Source](https://kenney.nl/assets/interface-sounds) + +Played in menus when pressing button. + +Files: +* `click/*` + +*Interface Sounds* by [Kenney], under the [Creative Commons CC0] license. No +changes were made to the original sound effect. + + [studiomandragore]: https://freesound.org/people/studiomandragore/ [qubodup]: https://freesound.org/people/qubodup/ [Breviceps]: https://freesound.org/people/Breviceps/sounds/452998/ @@ -117,8 +142,10 @@ Files: [oganesson]: https://freesound.org/people/oganesson/ [silversatyr]: https://freesound.org/people/silversatyr/ [InspectorJ]: https://freesound.org/people/InspectorJ/ +[Kenney]: https://kenney.nl/ [Creative Commons 0]: https://creativecommons.org/publicdomain/zero/1.0/ [Attribution 3.0]: https://creativecommons.org/licenses/by/3.0/ [Attribution 4.0]: https://creativecommons.org/licenses/by/4.0/ +[Creative Commons CC0]: https://creativecommons.org/publicdomain/zero/1.0/ diff --git a/examples/forest-brawl/sounds/click/click_001.ogg b/examples/forest-brawl/sounds/click/click_001.ogg new file mode 100644 index 0000000000000000000000000000000000000000..7ca77c7186126ce177640aad0c7ccc4c59bf4446 GIT binary patch literal 4876 zcmai230PBC)4q{4h%`X3K_lM

Pq@U{Hf%0a?t2h$I9gl!}3fY*7=UQiWm(2#OKm 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 V2iw099()>e{rl^IH{Y3<_#g2vx_|%x literal 0 HcmV?d00001 diff --git a/examples/forest-brawl/sounds/click/click_002.ogg.import b/examples/forest-brawl/sounds/click/click_002.ogg.import new file mode 100644 index 00000000..793aabe9 --- /dev/null +++ b/examples/forest-brawl/sounds/click/click_002.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://d4fnb7fq15ud7" +path="res://.godot/imported/click_002.ogg-cafda1b0619c2c53ecaa27cf77305839.oggvorbisstr" + +[deps] + +source_file="res://examples/forest-brawl/sounds/click/click_002.ogg" +dest_files=["res://.godot/imported/click_002.ogg-cafda1b0619c2c53ecaa27cf77305839.oggvorbisstr"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/examples/forest-brawl/sounds/click/click_003.ogg b/examples/forest-brawl/sounds/click/click_003.ogg new file mode 100644 index 0000000000000000000000000000000000000000..62de5e45638cfd2a568406ff86f25ff98e2738e1 GIT binary patch literal 4371 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 Q?kr0GsQUATpT6t-za7Z#;Q#;t literal 0 HcmV?d00001 diff --git a/examples/forest-brawl/sounds/click/click_003.ogg.import b/examples/forest-brawl/sounds/click/click_003.ogg.import new file mode 100644 index 00000000..af8826a0 --- /dev/null +++ b/examples/forest-brawl/sounds/click/click_003.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://xm1rrumqubqo" +path="res://.godot/imported/click_003.ogg-556b570cff6493f7c19189b38898afa7.oggvorbisstr" + +[deps] + +source_file="res://examples/forest-brawl/sounds/click/click_003.ogg" +dest_files=["res://.godot/imported/click_003.ogg-556b570cff6493f7c19189b38898afa7.oggvorbisstr"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/examples/forest-brawl/sounds/click/click_004.ogg b/examples/forest-brawl/sounds/click/click_004.ogg new file mode 100644 index 0000000000000000000000000000000000000000..d569cdaf6c248f012125023c301a758caf88f133 GIT binary patch literal 4486 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 literal 0 HcmV?d00001 diff --git a/examples/forest-brawl/sounds/switch/switch4.ogg.import b/examples/forest-brawl/sounds/switch/switch4.ogg.import new file mode 100644 index 00000000..96619c68 --- /dev/null +++ b/examples/forest-brawl/sounds/switch/switch4.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://br4qdiu4g7uuc" +path="res://.godot/imported/switch4.ogg-a2eceb97efa2d775762d9d0263baba6a.oggvorbisstr" + +[deps] + +source_file="res://examples/forest-brawl/sounds/switch/switch4.ogg" +dest_files=["res://.godot/imported/switch4.ogg-a2eceb97efa2d775762d9d0263baba6a.oggvorbisstr"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/examples/forest-brawl/sounds/switch/switch5.ogg b/examples/forest-brawl/sounds/switch/switch5.ogg new file mode 100644 index 0000000000000000000000000000000000000000..754facf2059cba32a9a45df63a72ba116d3708ff GIT binary patch literal 6181 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 Luo+e6fJOcTNA{;Q literal 0 HcmV?d00001 diff --git a/examples/forest-brawl/sounds/switch/switch5.ogg.import b/examples/forest-brawl/sounds/switch/switch5.ogg.import new file mode 100644 index 00000000..b88551a0 --- /dev/null +++ b/examples/forest-brawl/sounds/switch/switch5.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://ct4mldhp1j0p1" +path="res://.godot/imported/switch5.ogg-fd5ab6ef619a24ff825fdf15c9bca7f8.oggvorbisstr" + +[deps] + +source_file="res://examples/forest-brawl/sounds/switch/switch5.ogg" +dest_files=["res://.godot/imported/switch5.ogg-fd5ab6ef619a24ff825fdf15c9bca7f8.oggvorbisstr"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/examples/forest-brawl/sounds/switch/switch6.ogg b/examples/forest-brawl/sounds/switch/switch6.ogg new file mode 100644 index 0000000000000000000000000000000000000000..6dabf0b7fb30f05f2d1db5f98c8a9fdd8f2507f2 GIT binary patch literal 6610 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 literal 0 HcmV?d00001 diff --git a/examples/forest-brawl/sounds/switch/switch7.ogg.import b/examples/forest-brawl/sounds/switch/switch7.ogg.import new file mode 100644 index 00000000..6c157a64 --- /dev/null +++ b/examples/forest-brawl/sounds/switch/switch7.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://cg2pl8kegpluj" +path="res://.godot/imported/switch7.ogg-1c5b5746aba87a2346e2fec3244e374b.oggvorbisstr" + +[deps] + +source_file="res://examples/forest-brawl/sounds/switch/switch7.ogg" +dest_files=["res://.godot/imported/switch7.ogg-1c5b5746aba87a2346e2fec3244e374b.oggvorbisstr"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/examples/forest-brawl/sounds/switch/switch8.ogg b/examples/forest-brawl/sounds/switch/switch8.ogg new file mode 100644 index 0000000000000000000000000000000000000000..55b0159e6ff9b188ad05b00afa8a4232aeede73c GIT binary patch literal 6253 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 literal 0 HcmV?d00001 diff --git a/examples/forest-brawl/sounds/switch/switch8.ogg.import b/examples/forest-brawl/sounds/switch/switch8.ogg.import new file mode 100644 index 00000000..8e169cfa --- /dev/null +++ b/examples/forest-brawl/sounds/switch/switch8.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://cwpvdoq22ewxj" +path="res://.godot/imported/switch8.ogg-43a19b1ac90684c51b78e9a5449d6564.oggvorbisstr" + +[deps] + +source_file="res://examples/forest-brawl/sounds/switch/switch8.ogg" +dest_files=["res://.godot/imported/switch8.ogg-43a19b1ac90684c51b78e9a5449d6564.oggvorbisstr"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4