From 3747debcf27b4127795052dc2c9832cad5d218c6 Mon Sep 17 00:00:00 2001 From: "Cyril B." <53737317+Cykyrios@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:36:34 +0200 Subject: [PATCH] Refactor InSim button handling --- .../godot_insim/demo/buttons/demo_buttons.gd | 131 +++-- .../godot_insim/src/buttons/insim_button.gd | 83 --- .../src/buttons/insim_button/insim_button.gd | 108 ++++ .../{ => insim_button}/insim_button.gd.uid | 0 .../insim_button/insim_multi_button.gd | 113 ++++ .../insim_button/insim_multi_button.gd.uid | 1 + .../buttons/insim_button/insim_solo_button.gd | 78 +++ .../insim_button/insim_solo_button.gd.uid | 1 + .../src/buttons/insim_button/ucid_mapping.gd | 35 ++ .../buttons/insim_button/ucid_mapping.gd.uid | 1 + .../src/buttons/insim_button_dictionary.gd | 14 - .../buttons/insim_button_dictionary.gd.uid | 1 - .../src/buttons/insim_button_manager.gd | 532 ++++++++++++++++++ ...ons.gd.uid => insim_button_manager.gd.uid} | 0 .../godot_insim/src/buttons/insim_buttons.gd | 527 ----------------- addons/godot_insim/src/insim/insim.gd | 268 ++++----- .../test/buttons/test_insim_buttons.gd | 200 +++++-- 17 files changed, 1205 insertions(+), 888 deletions(-) delete mode 100644 addons/godot_insim/src/buttons/insim_button.gd create mode 100644 addons/godot_insim/src/buttons/insim_button/insim_button.gd rename addons/godot_insim/src/buttons/{ => insim_button}/insim_button.gd.uid (100%) create mode 100644 addons/godot_insim/src/buttons/insim_button/insim_multi_button.gd create mode 100644 addons/godot_insim/src/buttons/insim_button/insim_multi_button.gd.uid create mode 100644 addons/godot_insim/src/buttons/insim_button/insim_solo_button.gd create mode 100644 addons/godot_insim/src/buttons/insim_button/insim_solo_button.gd.uid create mode 100644 addons/godot_insim/src/buttons/insim_button/ucid_mapping.gd create mode 100644 addons/godot_insim/src/buttons/insim_button/ucid_mapping.gd.uid delete mode 100644 addons/godot_insim/src/buttons/insim_button_dictionary.gd delete mode 100644 addons/godot_insim/src/buttons/insim_button_dictionary.gd.uid create mode 100644 addons/godot_insim/src/buttons/insim_button_manager.gd rename addons/godot_insim/src/buttons/{insim_buttons.gd.uid => insim_button_manager.gd.uid} (100%) delete mode 100644 addons/godot_insim/src/buttons/insim_buttons.gd diff --git a/addons/godot_insim/demo/buttons/demo_buttons.gd b/addons/godot_insim/demo/buttons/demo_buttons.gd index da222c6..80d918f 100644 --- a/addons/godot_insim/demo/buttons/demo_buttons.gd +++ b/addons/godot_insim/demo/buttons/demo_buttons.gd @@ -42,9 +42,10 @@ func _ready() -> void: await insim.connected if insim.is_host(): insim.send_message("Host InSim started") + insim.button_manager.id_range = Vector2i(50, 80) show_manual_buttons() - show_auto_buttons() show_global_button() + show_auto_buttons() update_auto_buttons() @@ -54,11 +55,10 @@ func _exit_tree() -> void: func show_auto_buttons(for_ucid := -1) -> void: - insim.buttons.id_range = Vector2i(50, 80) var ucid_array: Array[int] = [] if for_ucid > -1: ucid_array.append(for_ucid) - insim.add_button( + insim.add_multi_button( ucid_array, Vector2i(auto_buttons_left, auto_buttons_top), Vector2i(2 * auto_button_width, 9 * auto_button_height), @@ -66,7 +66,7 @@ func show_auto_buttons(for_ucid := -1) -> void: "", "auto/background", ) - insim.add_button( + insim.add_multi_button( ucid_array, Vector2i(auto_buttons_left, auto_buttons_top), Vector2i(2 * auto_button_width, roundi(1.5 * auto_button_height)), @@ -74,17 +74,17 @@ func show_auto_buttons(for_ucid := -1) -> void: "Auto Buttons", "auto/title", ) - insim.add_button( + insim.add_multi_button( ucid_array, Vector2i(auto_buttons_left, auto_buttons_top + 2 * auto_button_height), Vector2i(2 * auto_button_width, auto_button_height), 0, "Custom clickID range: %d-%d" % [ - insim.buttons.id_range.x, insim.buttons.id_range.y + insim.button_manager.id_range.x, insim.button_manager.id_range.y ], "auto/range", ) - insim.add_button( + insim.add_multi_button( ucid_array, Vector2i(auto_buttons_left, auto_buttons_top + 3 * auto_button_height), Vector2i(2 * auto_button_width, auto_button_height), @@ -92,11 +92,16 @@ func show_auto_buttons(for_ucid := -1) -> void: "test", "auto/test", ) - var button := insim.get_button_by_name("auto/test", for_ucid) + var button := insim.get_button_by_name( + insim.connections.keys()[0] as int, "auto/test" + ) as InSimMultiButton if button: - button.text = "ID=%d, name=%s" % [button.click_id, button.name] - insim.send_packet(button.get_btn_packet(true)) - insim.add_button( + var test_callable := func(ucid: int) -> String: + var mapping := button.get_ucid_mapping(ucid) + var click_id_string := str(mapping.click_id) if mapping else "###" + return "ID=%s, name=%s" % [click_id_string, button.name] + insim.update_multi_button(button, test_callable) + insim.add_multi_button( ucid_array, Vector2i(auto_buttons_left + 2, auto_buttons_top + 4 * auto_button_height), Vector2i(2 * auto_button_width - 4, auto_button_height), @@ -104,7 +109,7 @@ func show_auto_buttons(for_ucid := -1) -> void: func(ucid: int) -> String: return insim.get_connection_nickname(ucid), "auto/player_name", ) - insim.add_button( + insim.add_multi_button( ucid_array, Vector2i(auto_buttons_left + 2, auto_buttons_top + 5 * auto_button_height), Vector2i(2 * auto_button_width - 4, auto_button_height), @@ -112,7 +117,7 @@ func show_auto_buttons(for_ucid := -1) -> void: "Time", "auto/clock", ) - insim.add_button( + insim.add_multi_button( ucid_array, Vector2i(auto_buttons_left + 2, auto_buttons_top + 7 * auto_button_height), # 6 is blink Vector2i(2 * auto_button_width - 4, auto_button_height), @@ -123,8 +128,8 @@ func show_auto_buttons(for_ucid := -1) -> void: func show_global_button() -> void: - insim.add_button( - [InSim.UCID_ALL], + insim.add_solo_button( + InSim.UCID_ALL, Vector2i(50, 1), Vector2i(50, 5), InSim.ButtonStyle.ISB_DARK | InSim.ButtonStyle.ISB_CLICK, @@ -235,45 +240,51 @@ func show_manual_buttons() -> void: func toggle_menu(for_ucid: int) -> void: if menu_open.has(for_ucid): menu_open.erase(for_ucid) - insim.delete_buttons_by_prefix([for_ucid], "menu/") - insim.update_button_text(insim.get_button_by_name("auto/menu", for_ucid), "Show menu") + insim.delete_buttons(insim.get_buttons_by_prefix(for_ucid, "menu/")) + insim.update_multi_button( + insim.get_button_by_name(for_ucid, "auto/menu") as InSimMultiButton, + "Show menu", + ) return menu_open.append(for_ucid) - insim.update_button_text(insim.get_button_by_name("auto/menu", for_ucid), "Close menu") - insim.add_button( - [for_ucid], + insim.update_multi_button( + insim.get_button_by_name(for_ucid, "auto/menu") as InSimMultiButton, + "Close menu", + ) + insim.add_solo_button( + for_ucid, Vector2i(menu_buttons_left, menu_buttons_top), Vector2i(2 * menu_button_width, 5 * menu_button_height), InSim.ButtonStyle.ISB_LIGHT, "", "menu/background", ) - insim.add_button( - [for_ucid], + insim.add_solo_button( + for_ucid, Vector2i(menu_buttons_left + 2, menu_buttons_top), Vector2i(2 * menu_button_width - 4, 2 * menu_button_height), InSim.ButtonColor.ISB_TITLE, "Menu", "menu/title", ) - insim.add_button( - [for_ucid], + insim.add_solo_button( + for_ucid, Vector2i(menu_buttons_left + 2 * menu_button_width - 4, menu_buttons_top), Vector2i(4, menu_button_height), InSim.ButtonStyle.ISB_DARK | InSim.ButtonStyle.ISB_CLICK, LFSText.get_color_code(LFSText.ColorCode.RED) + "X", "menu/close", ) - insim.add_button( - [for_ucid], + insim.add_solo_button( + for_ucid, Vector2i(menu_buttons_left + 2, menu_buttons_top + 2 * menu_button_height), Vector2i(2 * menu_button_width - 4, menu_button_height), InSim.ButtonStyle.ISB_DARK | InSim.ButtonStyle.ISB_CLICK, "Item 1", "menu/item_1", ) - insim.add_button( - [for_ucid], + insim.add_solo_button( + for_ucid, Vector2i(menu_buttons_left + 2, menu_buttons_top + 3 * menu_button_height), Vector2i(2 * menu_button_width - 4, menu_button_height), InSim.ButtonStyle.ISB_DARK | InSim.ButtonStyle.ISB_CLICK, @@ -289,15 +300,15 @@ func update_auto_buttons() -> void: var loop := 0 while true: await get_tree().create_timer(1).timeout - var buttons := insim.get_global_button_by_name("auto/clock") - if not buttons.is_empty(): - insim.update_global_button_text( - buttons[0].click_id, - Time.get_time_string_from_system(true), + var button := insim.get_button_by_name(0, "auto/clock") as InSimMultiButton + if button: + insim.update_multi_button( + button, + Time.get_time_string_from_system(), ) loop += 1 if loop % 2: - insim.add_button( + insim.add_multi_button( [], Vector2i(auto_buttons_left + 2, auto_buttons_top + 6 * auto_button_height), Vector2i(2 * auto_button_width - 4, auto_button_height), @@ -305,36 +316,40 @@ func update_auto_buttons() -> void: "", "auto/blink", ) - var click_id := insim.buttons.get_global_button_id_from_name("auto/blink") - if click_id != -1: - insim.update_global_button_text(click_id, "ID=%d, name=%s" % [ - click_id, "auto/blink" - ]) + button = insim.get_button_by_name(0, "auto/blink") + if button: + var get_click_id := func(ucid: int) -> String: + var mapping := button.get_ucid_mapping(ucid) + var click_id_string := str(mapping.click_id) if mapping else "###" + return "ID=%s, name=%s" % [click_id_string, "auto/blink"] + insim.update_multi_button(button, get_click_id) else: - insim.delete_global_button_by_name("auto/blink") + insim.delete_button(insim.get_button_by_name(0, "auto/blink")) func update_last_clicker_button(clicker_ucid: int) -> void: - var button := insim.get_button_by_name("global/last_clicker", InSim.UCID_ALL) + var button := insim.get_button_by_name(InSim.UCID_ALL, "global/last_clicker") as InSimSoloButton if not button: return - insim.update_button_text( + insim.update_solo_button( button, "Last clicker: %s" % [insim.get_connection_nickname(clicker_ucid)] ) func update_player_name_button(ucid: int) -> void: - var button := insim.get_button_by_name("auto/player_name", ucid) + var button := insim.get_button_by_name(ucid, "auto/player_name") as InSimMultiButton if button and button.name == "auto/player_name": - button.text = ( - "ID=%d, name=%s" % [ - button.click_id, - button.name, - ] if button.text == insim.get_connection_nickname(button.ucid) - else insim.get_connection_nickname(button.ucid) - ) - insim.send_packet(button.get_btn_packet(true)) + var get_text := func(for_ucid: int) -> String: + var mapping := button.get_ucid_mapping(for_ucid) + return ( + "ID=%d, name=%s" % [ + mapping.click_id, + button.name, + ] if mapping.text == insim.get_connection_nickname(for_ucid) + else insim.get_connection_nickname(for_ucid) + ) + insim.update_multi_button(button, get_text) func _on_bfn_received(packet: InSimBFNPacket) -> void: @@ -362,14 +377,14 @@ func _on_bfn_received(packet: InSimBFNPacket) -> void: func _on_btc_received(packet: InSimBTCPacket) -> void: - # Note that buttons created manually are not tracked in InSimButtons; you can either - # add them manually (insim.buttons[ucid] = InSimButtonDictionary.new(), then - # add a manually created InSimButton to it), or keep track of manual buttons yourself. - # For click_id == 10 below, we check packet.click_id instead of button.click_id for - # that reason. - var button := insim.get_button_by_id(packet.click_id, packet.ucid) + # Note that buttons created manually are not tracked by [InSimButtonManager]; you can + # either add them manually (insim.button_manager[ucid] = InSimButtonDictionary.new(), + # then add a manually created InSimButton to it), or keep track of manual buttons + # yourself. For click_id == 10 below, we check packet.click_id instead of button.click_id + # for that reason. + var button := insim.get_button_by_id(packet.ucid, packet.click_id) if not button: - button = insim.get_button_by_id(packet.click_id, InSim.UCID_ALL) + button = insim.get_button_by_id(InSim.UCID_ALL, packet.click_id) var flags := packet.click_flags var message := "You clicked %s (%s)." % [ "the ^2Click^8 button in the Manual Buttons category" if packet.click_id == 10 diff --git a/addons/godot_insim/src/buttons/insim_button.gd b/addons/godot_insim/src/buttons/insim_button.gd deleted file mode 100644 index 7f856f0..0000000 --- a/addons/godot_insim/src/buttons/insim_button.gd +++ /dev/null @@ -1,83 +0,0 @@ -class_name InSimButton -extends RefCounted -## InSim Button -## -## An object representing an InSim button, mostly mirroring [InSimBTNPacket]. Allows querying -## existing buttons by creating and storing the object after receiving an [InSimBTNPacket]. -## Any change made should be followed by sending a new [InSimBTNPacket] to update the actual -## InSim button, which can be done by calling [method get_btn_packet]. - -## If set in [member inst], the button will display everywhere - this is typically best avoided. -const INST_ALWAYS_ON := 0x80 - -## A custom identifier, which can be used to keep track of, update or delete the button. -var name := "" -## The request ID that sent the [InSimBTNPacket] to create this button. -var reqi := 0 -## The list of unique connection IDs to display the button for (0: local, 255: everyone). -## If [code]0[/code] or [code]255[/code] are present, they should be the only value. -var ucid := 0 -## The button's click ID. -var click_id := 0: - set(value): - click_id = clampi(value, 0, InSimButtons.MAX_BUTTONS - 1) -## Some flags and mostly used internally by InSim. -var inst := 0 -## Style flags for this button, see [enum InSim.ButtonStyle] and [enum InSim.ButtonColor]. -var style := 0 -## Number of characters that can be typed in - note that color codes, code page changes, and -## multibyte characters consume "invisible" characters. -var type_in := 0 -## The button's position in LFS coordinates. -var position := Vector2i.ZERO -## The button's size in LFS coordinates. -var size := Vector2i.ZERO -## The text displayed by this button. -var text := "" -## The caption for this button (used as description for [member type_in]). -var caption := "" - - -## Creates and returns a new [InSimButton]. -static func create( - b_ucid: int, b_click_id: int, b_inst: int, b_style: int, b_position: Vector2i, - b_size: Vector2i, b_text: String, b_name := "", b_type_in := 0, b_caption := "" -) -> InSimButton: - var button := InSimButton.new() - button.ucid = b_ucid - button.click_id = b_click_id - button.inst = b_inst - button.style = b_style - button.type_in = b_type_in - button.position = b_position - button.size = b_size - button.text = b_text - button.name = b_name - button.caption = b_caption - return button - - -## Returns an [InSimBTNPacket] corresponding to this [InSimButton], disregarding its position -## and size if [param ignore_position_and_size] is [code]true[/code]. This is useful to update -## an existing button without having to recreate an [InSimBTNPacket] from scratch after -## modifying the button. -func get_btn_packet(ignore_position_and_size := false) -> InSimBTNPacket: - var packet := InSimBTNPacket.create( - ucid, - click_id, - inst, - style, - type_in, - position.x, - position.y, - size.x, - size.y, - text, - caption - ) - if ignore_position_and_size: - packet.left = 0 - packet.top = 0 - packet.width = 0 - packet.height = 0 - return packet diff --git a/addons/godot_insim/src/buttons/insim_button/insim_button.gd b/addons/godot_insim/src/buttons/insim_button/insim_button.gd new file mode 100644 index 0000000..70aafbf --- /dev/null +++ b/addons/godot_insim/src/buttons/insim_button/insim_button.gd @@ -0,0 +1,108 @@ +class_name InSimButton +extends RefCounted +## InSim Button base class +## +## An object representing an InSim button, mostly mirroring [InSimBTNPacket]. Allows querying +## existing buttons by creating and storing the object after receiving an [InSimBTNPacket]. +## Any change made should be followed by sending a new [InSimBTNPacket] to update the actual +## InSim button, which can be done by calling [method get_btn_packet].[br] +## [b]Note:[/b] This class is not intended to be used directly, you should use [InSimSoloButton] +## and [InSimMultiButton] instead when interacting with [InSimButtonManager]. + +## If set in [member inst], the button will display everywhere - this is typically best avoided. +const INST_ALWAYS_ON := 0x80 + +## A custom identifier, which can be used to keep track of, update or delete the button. +var name := "" +## The request ID that sent the [InSimBTNPacket] to create this button. +var reqi := 0 +## Some flags and mostly used internally by InSim. +var inst := 0 +## Style flags for this button, see [enum InSim.ButtonStyle] and [enum InSim.ButtonColor]. +var style := 0 +## The button's position in LFS coordinates. +var position := Vector2i.ZERO +## The button's size in LFS coordinates. +var size := Vector2i.ZERO + + +## Virtual function to override. Returns an [InSimBTNPacket] representing this button +## for the given [param for_ucid]. +@warning_ignore("unused_parameter") +func _get_btn_packet(for_ucid: int) -> InSimBTNPacket: + return InSimBTNPacket.new() + + +## Virtual function to override. Returns the button's caption. See [method get_caption] +## for more details. +@warning_ignore("unused_parameter") +func _get_caption(for_ucid: int) -> String: + return "" + + +## Virtual function to override. Returns the button's clickID. See [method get_click_id] +## for more details. +@warning_ignore("unused_parameter") +func _get_click_id(for_ucid: int) -> int: + return -1 + + +## Virtual function to override. Returns the button's text. See [method get_text] +## for more details. +@warning_ignore("unused_parameter") +func _get_text(for_ucid: int) -> String: + return "" + + +## Virtual function to override. Returns the button's type_in. See [method get_type_in] +## for more details. +@warning_ignore("unused_parameter") +func _get_type_in(for_ucid: int) -> int: + return -1 + + +## Returns an [InSimBTNPacket] corresponding to this [InSimButton] for the given [param for_ucid], +## if relevant. If [param ignore_position_and_size] is [code]true[/code], the button's position +## and size are set to zero; this is useful to update an existing button without having to +## recreate an [InSimBTNPacket] from scratch after modifying the button.[br] +## [b]Note:[/b] This function is mainly intended for internal use by the [InSimButtonManager]. +func get_btn_packet(for_ucid: int, ignore_position_and_size := false) -> InSimBTNPacket: + var packet := _get_btn_packet(for_ucid) + if ignore_position_and_size: + packet.left = 0 + packet.top = 0 + packet.width = 0 + packet.height = 0 + return packet + + +## Returns the button's clickID. If the button can have multiple values, return +## the one corresponding to the given [param for_ucid]. See [method _get_click_id] +## for implementation.[br] +## [b]Note:[/b] The clickID is only implemented in child classes. +func get_click_id(for_ucid: int) -> int: + return _get_click_id(for_ucid) + + +## Returns the button's caption. If the button can have multiple values, return +## the one corresponding to the given [param for_ucid]. See [method _get_caption] +## for implementation.[br] +## [b]Note:[/b] The caption is only implemented in child classes. +func get_caption(for_ucid: int) -> String: + return _get_caption(for_ucid) + + +## Returns the button's text. If the button can have multiple values, return +## the one corresponding to the given [param for_ucid]. See [method _get_text] +## for implementation.[br] +## [b]Note:[/b] The text is only implemented in child classes. +func get_text(for_ucid: int) -> String: + return _get_text(for_ucid) + + +## Returns the button's type_in. If the button can have multiple values, return +## the one corresponding to the given [param for_ucid]. See [method _get_type_in] +## for implementation.[br] +## [b]Note:[/b] The type_in is only implemented in child classes. +func get_type_in(for_ucid: int) -> int: + return _get_type_in(for_ucid) diff --git a/addons/godot_insim/src/buttons/insim_button.gd.uid b/addons/godot_insim/src/buttons/insim_button/insim_button.gd.uid similarity index 100% rename from addons/godot_insim/src/buttons/insim_button.gd.uid rename to addons/godot_insim/src/buttons/insim_button/insim_button.gd.uid diff --git a/addons/godot_insim/src/buttons/insim_button/insim_multi_button.gd b/addons/godot_insim/src/buttons/insim_button/insim_multi_button.gd new file mode 100644 index 0000000..4d7acdc --- /dev/null +++ b/addons/godot_insim/src/buttons/insim_button/insim_multi_button.gd @@ -0,0 +1,113 @@ +class_name InSimMultiButton +extends InSimButton +## InSim Button assigned to multiple UCIDs +## +## The [InSimMultiButton] represents a common button sent to multiple players, with text +## that can be customized for each player. It can be sent to a specific list of players, +## or to everyone as a global button. + +## A dictionary of UCID mappings, i.e. all UCIDs for which to display the button, +## and the data to display for each of them. [constant InSim.UCID_ALL] cannot be +## a valid key. +var ucid_mappings: Dictionary[int, UCIDMapping] = {} + + +func _init(b_position: Vector2i, b_size: Vector2i, b_style := 0, b_name := "", b_inst := 0) -> void: + position = b_position + size = b_size + style = b_style + name = b_name + inst = b_inst + + +func _get_btn_packet(for_ucid: int) -> InSimBTNPacket: + var mapping := get_ucid_mapping(for_ucid) + if not mapping: + return null + var packet := InSimBTNPacket.create( + for_ucid, + mapping.click_id, + inst, + style, + mapping.type_in, + position.x, + position.y, + size.x, + size.y, + mapping.text, + mapping.caption, + ) + return packet + + +func _get_caption(for_ucid: int) -> String: + var mapping := get_ucid_mapping(for_ucid) + if not mapping: + return "" + return mapping.caption + + +func _get_click_id(for_ucid: int) -> int: + var mapping := get_ucid_mapping(for_ucid) + if not mapping: + return -1 + return mapping.click_id + + +func _get_text(for_ucid: int) -> String: + var mapping := get_ucid_mapping(for_ucid) + if not mapping: + return "" + return mapping.text + + +func _get_type_in(for_ucid: int) -> int: + var mapping := get_ucid_mapping(for_ucid) + if not mapping: + return -1 + return mapping.type_in + + +## Add a [UCIDMapping] to [member ucid_mappings] with the given parameters. +func add_ucid_mapping( + for_ucid: int, click_id: int, text: String, caption := "", type_in := 0 +) -> void: + ucid_mappings[for_ucid] = UCIDMapping.create(for_ucid, click_id, type_in, text, caption) + + +## Deletes all UCID mappings for this button. +func clear_ucid_mappings() -> void: + ucid_mappings.clear() + + +## Returns the [UCIDMapping] associated with the given [param for_ucid]. +func get_ucid_mapping(for_ucid: int) -> UCIDMapping: + return ucid_mappings[for_ucid] if ucid_mappings.has(for_ucid) else null + + +## Removes the [UCIDMapping] associated with the given [param for_ucid], if it exists. +func remove_ucid_mapping(for_ucid: int) -> void: + var _existed := ucid_mappings.erase(for_ucid) + + +## Updates the data contained in the [UCIDMapping] asociated with the given [param for_ucid]. +## Fails if [param for_ucid] is not a valid UCID for this button. +func update_ucid_mapping( + for_ucid: int, text: String, caption := "", type_in := 0, click_id := -1 +) -> void: + var mapping := get_ucid_mapping(for_ucid) + if not mapping: + push_error("Cannot update MultiButton mapping: UCID %d not found" % [for_ucid]) + return + if ( + text != mapping.text + or caption != mapping.caption + or type_in != mapping.type_in + ): + mapping.dirty_flag = true + mapping.type_in = type_in + mapping.text = text + mapping.caption = caption + if click_id != -1 and click_id != mapping.click_id: + mapping.click_id = click_id + mapping.dirty_flag = true diff --git a/addons/godot_insim/src/buttons/insim_button/insim_multi_button.gd.uid b/addons/godot_insim/src/buttons/insim_button/insim_multi_button.gd.uid new file mode 100644 index 0000000..7fc0747 --- /dev/null +++ b/addons/godot_insim/src/buttons/insim_button/insim_multi_button.gd.uid @@ -0,0 +1 @@ +uid://1i7tqj7qjqwx diff --git a/addons/godot_insim/src/buttons/insim_button/insim_solo_button.gd b/addons/godot_insim/src/buttons/insim_button/insim_solo_button.gd new file mode 100644 index 0000000..572581b --- /dev/null +++ b/addons/godot_insim/src/buttons/insim_button/insim_solo_button.gd @@ -0,0 +1,78 @@ +class_name InSimSoloButton +extends InSimButton +## InSim Button assigned to a single UCID +## +## The [InSimSoloButton] represents a single button: one UCID, one clickID. If the button's +## UCID is [constant InSim.UCID_ALL], this button technically represents more than +## one button, but is still considered as a single "broadcast" button. + +## The unique connection ID to display the button for (0: local/host, 255: everyone). +var ucid := 0 +## The button's clickID. +var click_id := 0: + set(value): + click_id = clampi(value, 0, InSimButtonManager.MAX_BUTTONS - 1) +## Number of characters that can be typed in.[br] +## [b]Note:[/b] Color codes, codepage change codes, and multibyte characters all count +## toward that number (e.g. typing a Japanese character will add [code]^J[/code] and +## 2 more bytes (for a multibyte character) to make up one character, so your first +## Japanese character would actually cost 4 characters (bytes), and subsequent multibyte +## characters would cost an additional 2). +var type_in := 0 +## The text displayed by this button. +var text := "" +## The caption for this button (used as description for [member type_in]). +var caption := "" + + +func _init( + b_ucid: int, b_click_id: int, b_inst: int, b_style: int, b_position: Vector2i, + b_size: Vector2i, b_text: String, b_name := "", b_type_in := 0, b_caption := "" +) -> void: + ucid = b_ucid + click_id = b_click_id + inst = b_inst + style = b_style + type_in = b_type_in + position = b_position + size = b_size + text = b_text + name = b_name + caption = b_caption + + +@warning_ignore("unused_parameter") +func _get_btn_packet(for_ucid: int) -> InSimBTNPacket: + return InSimBTNPacket.create( + ucid, + click_id, + inst, + style, + type_in, + position.x, + position.y, + size.x, + size.y, + text, + caption, + ) + + +@warning_ignore("unused_parameter") +func _get_caption(for_ucid: int) -> String: + return caption + + +@warning_ignore("unused_parameter") +func _get_click_id(for_ucid: int) -> int: + return click_id + + +@warning_ignore("unused_parameter") +func _get_text(for_ucid: int) -> String: + return text + + +@warning_ignore("unused_parameter") +func _get_type_in(for_ucid: int) -> int: + return type_in diff --git a/addons/godot_insim/src/buttons/insim_button/insim_solo_button.gd.uid b/addons/godot_insim/src/buttons/insim_button/insim_solo_button.gd.uid new file mode 100644 index 0000000..2dbab03 --- /dev/null +++ b/addons/godot_insim/src/buttons/insim_button/insim_solo_button.gd.uid @@ -0,0 +1 @@ +uid://d304bhsa0kyeq diff --git a/addons/godot_insim/src/buttons/insim_button/ucid_mapping.gd b/addons/godot_insim/src/buttons/insim_button/ucid_mapping.gd new file mode 100644 index 0000000..6913cfb --- /dev/null +++ b/addons/godot_insim/src/buttons/insim_button/ucid_mapping.gd @@ -0,0 +1,35 @@ +class_name UCIDMapping +extends RefCounted +## UCID mapping for [InSimMultiButton] +## +## This class contains button data associated with a UCID, for use with [InSimMultiButton], +## which can hold references to multiple UCIDs and the corresponding data with such mappings. + +## The UCID this mapping is for. +var ucid := 0 +## The click ID for the button associated to this [member ucid]. +var click_id := 0 +## The number of characters that can be typed in for the button associated to +## this [member ucid]. Note that color codes, code page changes, and multibyte +## characters consume "invisible" characters. +var type_in := 0 +## The text displayed by the button associated to this [member ucid]. +var text := "" +## The caption for the button associated to this [member ucid]. +var caption := "" +## A dirty flag that is used to decide whether the button associated to this +## [member ucid] needs to be sent again. +var dirty_flag := true + + +## Returns a new [UCIDMapping] created from the data passed in the given parameters. +static func create( + m_ucid: int, m_click_id: int, m_type_in: int, m_text: String, m_caption: String +) -> UCIDMapping: + var mapping := UCIDMapping.new() + mapping.ucid = m_ucid + mapping.click_id = m_click_id + mapping.type_in = m_type_in + mapping.text = m_text + mapping.caption = m_caption + return mapping diff --git a/addons/godot_insim/src/buttons/insim_button/ucid_mapping.gd.uid b/addons/godot_insim/src/buttons/insim_button/ucid_mapping.gd.uid new file mode 100644 index 0000000..cfb972f --- /dev/null +++ b/addons/godot_insim/src/buttons/insim_button/ucid_mapping.gd.uid @@ -0,0 +1 @@ +uid://1882dmu7ljmi diff --git a/addons/godot_insim/src/buttons/insim_button_dictionary.gd b/addons/godot_insim/src/buttons/insim_button_dictionary.gd deleted file mode 100644 index a84b61c..0000000 --- a/addons/godot_insim/src/buttons/insim_button_dictionary.gd +++ /dev/null @@ -1,14 +0,0 @@ -class_name InSimButtonDictionary -extends RefCounted -## InSimButton dictionary -## -## A [Dictionary] of [InSimButton] objects. - -## A [Dictionary] of [InSimButton] objects, with keys corresponding to the buttons' click IDs. -var buttons: Dictionary[int, InSimButton] = {} - - -## Returns [code]true[/code] if [member buttons] has the [param id] key, otherwise -## returns [code]false[/code]. -func has_id(id: int) -> bool: - return true if buttons.has(id) else false diff --git a/addons/godot_insim/src/buttons/insim_button_dictionary.gd.uid b/addons/godot_insim/src/buttons/insim_button_dictionary.gd.uid deleted file mode 100644 index bdd53d2..0000000 --- a/addons/godot_insim/src/buttons/insim_button_dictionary.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://6mdvyir0fgwi diff --git a/addons/godot_insim/src/buttons/insim_button_manager.gd b/addons/godot_insim/src/buttons/insim_button_manager.gd new file mode 100644 index 0000000..764fbcb --- /dev/null +++ b/addons/godot_insim/src/buttons/insim_button_manager.gd @@ -0,0 +1,532 @@ +class_name InSimButtonManager +extends RefCounted +## InSimButton manager +## +## This class keeps track of existing [InSimButton]s and allows manipulating buttons. +## An instance is included in the [InSim] object. + +## The maximum number of buttons across all InSim apps. +const MAX_BUTTONS := 240 + +## A reference to the parent [InSim] instance, used to fetch [member InSim.connections] +## for per-player button management. +var insim: InSim = null + +## The range of clickIDs this instance can use. This range should remain as small +## as possible to allow other instances or InSim apps to manage their own range of +## buttons, while still allowing all modules to allocate clickIDs freely. +var id_range := Vector2i(0, MAX_BUTTONS - 1): + set(value): + id_range = Vector2i( + clampi(value.x, 0, MAX_BUTTONS - 1), + clampi(value.y, value.x, MAX_BUTTONS - 1) + ) +## A dictionary mapping all button clickIDs for every UCID, updating every time a button +## is added, updated, or deleted through the provided methods.[br] +## [b]Warning:[/b] While you can read the current mappings by reading this property, +## you should never modify values, or you may break the button tracking. +var id_map: Dictionary[int, Array] = {} +## The list of all buttons that were created using [method add_solo_button] and +## [method add_multi_button]; calling [method delete_button] and [method delete_buttons] +## removes buttons accordingly.[br] +## [b]Warning:[/b] While you can get all buttons by reading this property, you should +## never modify values, or you may break the button tracking. +var buttons: Array[InSimButton] = [] +## The list of UCIDs corresponding to players who have disabled InSim buttons (by +## pressing [kbd]Shift + I[/kbd]); adding and updating buttons should be disabled +## for those, with the exception of [member _broadcast_buttons], which by nature +## are sent to everyone. +var disabled_ucids: Array[int] = [] + +## The list of current broadcast buttons, i.e. [InSimSoloButton] buttons that were +## created with a UCID of [constant InSim.UCID_ALL]; all buttons in this list are +## sent again when a player connects to the server. +var _broadcast_buttons: Array[InSimSoloButton] = [] +## The list of current global buttons, i.e. [InSimMultiButton] buttons that were +## created with an empty UCID array, or were declared global by calling +## [method set_global_multi_button]; those are sent to all connected UCIDs as individual +## packets. When a player connects, they receive all currently existing global buttons, +## and their UCID is added to the buttons' mappings; conversely, when a player +## disconnects, their UCID is removed from all buttons. +var _global_buttons: Array[InSimMultiButton] = [] + + +func _init(insim_instance: InSim) -> void: + insim = insim_instance + + +## Creates an [InSimMultiButton] from the given parameters, and returns an array of +## [InSimBTNPacket]s corresponding to the created buttons. See [method add_solo_button] +## for more details on general use and parameters. You need to pass an array of +## [param ucids], all of which will have a mapping to their own clickID, text, +## caption, and type_in values. For this reason, instead of the usual [String] and +## [int], you can pass a [Callable] to [param text], [param caption], and [param type_in]; +## those should take a single UCID [int] as parameter, and return a [String] ([param text] +## and [param caption]) or an [int] ([param type_in]). This will allow the [InSimMultiButton] +## to generate packets with values tailored to each UCID, while using a single [InSimButton] +## internally, for easier button management. See the example code in +## [method InSim.add_multi_button]. +func add_multi_button( + ucids: Array[int], position: Vector2i, size: Vector2i, style: int, text: Variant, + button_name := "", type_in: Variant = 0, caption: Variant = "", show_everywhere := false, +) -> Array[InSimBTNPacket]: + var inst := InSimButton.INST_ALWAYS_ON if show_everywhere else 0 + var button := InSimMultiButton.new(position, size, style, button_name, inst) + buttons.append(button) + ucids.erase(InSim.UCID_ALL) + if ucids.is_empty(): + set_global_multi_button(button, true) + ucids = insim.connections.keys() + var packets: Array[InSimBTNPacket] = [] + for ucid in ucids: + var click_id := _get_free_click_id(ucid) + if click_id < 0: + push_error("Cannot add clickID to UCID %d: no clickID available." % [ucid]) + continue + _apply_multi_button_input(button, ucid, click_id, text, caption, type_in) + if ucid in disabled_ucids: + continue + _add_id_mapping(ucid, click_id) + if button.get_ucid_mapping(ucid).dirty_flag: + packets.append(button.get_btn_packet(ucid)) + return packets + + +## Creates an [InSimSoloButton] from the given parameters, which is added to the +## [member buttons] [Array]. Button creation can fail if there is no clickID available +## in the [member id_range] for the given UCID (when assigning [constant InSim.UCID_ALL], +## there must be a clickID globally available, taking all UCIDs into account). +## Returns an [InSimBTNPacket] corresponding to the created button if successful, +## or returns [code]null[/code] otherwise.[br] +## [param position] and [param size] expect coordinates on the 200x200 canvas LFS +## uses to render buttons on. If [param show_everywhere] is [code]true[/code], the +## button will not be hidden as it usually would in some situations, like pressing +## [code]T[/code] to open the chat input box, or in the garage view; this also means +## you should use it sparingly.[br] +## [b]Tip:[/b] Use the [param button_name] parameter to organize your buttons! You can +## easily create and manage complex button hierarchies by including categories in your +## button names, like [code]menu/sub_menu/title[/code]; when you need to delete an +## entire category, you can call [method delete_buttons] and pass the list returned +## by [method get_buttons_by_prefix]. +func add_solo_button( + ucid: int, position: Vector2i, size: Vector2i, style: int, text: String, + button_name := "", type_in := 0, caption := "", show_everywhere := false, +) -> InSimBTNPacket: + var inst := InSimButton.INST_ALWAYS_ON if show_everywhere else 0 + var click_id := _get_free_click_id(ucid) + if click_id < 0: + return null + var button := InSimSoloButton.new( + ucid, click_id, inst, style, position, size, text, button_name, type_in, caption + ) + buttons.append(button) + _add_id_mapping(ucid, click_id) + if ucid == InSim.UCID_ALL: + _broadcast_buttons.append(button) + return button.get_btn_packet(ucid) + + +## Deletes the given [param button] and returns an array of [InSimBFNPacket]s to be sent +## by [InSim]. You can get a reference to the [param button] by calling one of +## [method get_button_by_id] or [method get_button_by_name], or by getting a button from +## [method get_buttons_by_id_range], [method get_buttons_by_prefix], or +## [method get_buttons_by_regex]. You can also pass specific [param ucids] that should +## have their buttons deleted; this will only affect the [param button] if it is an +## [InSimMultiButton], in which case only the selected UCID mappings will be removed, +## and the button itself will only be deleted when all mappings are removed. +func delete_button(button: InSimButton, ucids: Array[int] = []) -> Array[InSimBFNPacket]: + if not button: + push_error("Cannot delete null button.") + return [] + var packets: Array[InSimBFNPacket] = [] + if button is InSimSoloButton: + var solo_button := button as InSimSoloButton + packets.append(InSimBFNPacket.create( + InSim.ButtonFunction.BFN_DEL_BTN, + solo_button.ucid, + solo_button.click_id, + 0, + )) + _remove_id_mapping(solo_button.ucid, solo_button.click_id) + if _broadcast_buttons.has(solo_button): + _broadcast_buttons.erase(solo_button) + buttons.erase(button) + button = null + elif button is InSimMultiButton: + var multi_button := button as InSimMultiButton + var ucids_to_remove: Array[int] = ucids.duplicate() + if ucids_to_remove.is_empty(): + ucids_to_remove = multi_button.ucid_mappings.keys().duplicate() + for ucid in ucids_to_remove: + var mapping := multi_button.get_ucid_mapping(ucid) + packets.append(InSimBFNPacket.create( + InSim.ButtonFunction.BFN_DEL_BTN, + ucid, + mapping.click_id, + 0, + )) + multi_button.remove_ucid_mapping(ucid) + _remove_id_mapping(ucid, mapping.click_id) + if multi_button.ucid_mappings.is_empty(): + if _global_buttons.has(multi_button): + _global_buttons.erase(multi_button) + buttons.erase(button) + button = null + packets = _compact_bfn_packets(packets) + return packets + + +## Deletes an array of buttons [param to_delete], with optional filtering using [param ucids], +## and returns an array of [InSimBFNPacket]s to be sent by [InSim]. All considerations from +## [method delete_button] apply here as well to each button [param to_delete]. +func delete_buttons(to_delete: Array[InSimButton], ucids: Array[int] = []) -> Array[InSimBFNPacket]: + var packets: Array[InSimBFNPacket] = [] + for button in to_delete: + if not button: + push_error("Cannot delete null button.") + continue + packets.append_array(delete_button(button, ucids)) + packets = _compact_bfn_packets(packets) + return packets + + +## Disables button updates by adding [param ucid] to [member disabled_ucids]. Packets +## targetting [param ucid] will not be sent anymore until [method enable_buttons_for_ucid] +## is called.[br] +## [b]Note:[/b] This doesn't prevent broadcast packets ([constant InSim.UCID_ALL]) +## from being sent. +func disable_buttons_for_ucid(ucid: int) -> void: + disabled_ucids.erase(ucid) + disabled_ucids.append(ucid) + + +## Allows buttons to be sent to the given [param ucid] again, by removing it from +## [member disabled_ucids]. If button mappings were not removed, all remembered buttons +## are immediately sent again. See [method disable_buttons_for_ucid] for the opposite effect. +func enable_buttons_for_ucid(ucid: int) -> void: + disabled_ucids.erase(ucid) + for button in get_broadcast_buttons(): + insim.send_packet(button.get_btn_packet(InSim.UCID_ALL)) + if not id_map.has(ucid): + # This situation should only occur if all remembered buttons were multi-buttons + # that got deleted in the meantime. + return + for click_id in id_map[ucid] as Array[int]: + var button := get_button_by_id(ucid, click_id) + if button: + insim.send_packet(button.get_btn_packet(ucid)) + + +## Removes all buttons mappings for the given [param ucid]. Mainly intended for [InSim] +## to "forget" players leaving the server.[br] +## [b]Note:[/b] This doesn't clear buttons for the player with the given [param ucid], +## it simply removes internal mapppings. +func forget_buttons_for_ucid(ucid: int) -> void: + var button_count := buttons.size() + for i in button_count: + var idx := button_count - 1 - i + var button := buttons[idx] + if button is InSimSoloButton: + button = null + buttons.remove_at(idx) + elif button is InSimMultiButton: + (button as InSimMultiButton).remove_ucid_mapping(ucid) + var _existed := id_map.erase(ucid) + + +## Returns an array of [InSimSoloButton] created with [constant InSim.UCID_ALL]. +func get_broadcast_buttons() -> Array[InSimSoloButton]: + return _broadcast_buttons + + +## Returns the [InSimButton] matching the given [param click_id] for the given [param ucid], +## or [code]null[/code] if no matching button is found.[br] +## [b]Important:[/b] If a single UCID mapping from an [InSimMultiButton] matches, +## the complete button is returned; if you are using this method to delete buttons, +## make sure not to delete more buttons than intended. +func get_button_by_id(ucid: int, click_id: int) -> InSimButton: + if not id_map.has(ucid) or not id_map[ucid].has(click_id): + return null + for button in buttons: + if button is InSimSoloButton: + var solo_button := button as InSimSoloButton + if solo_button.ucid == ucid and solo_button.click_id == click_id: + return button + elif button is InSimMultiButton: + var multi_button := button as InSimMultiButton + if multi_button.ucid_mappings.has(ucid): + if multi_button.ucid_mappings[ucid].click_id == click_id: + return button + return null + + +## Returns all [InSimButton] buttons in the given range of [param click_id] to +## [param click_max] (both ends inclusive) for the given [param ucid], or an empty +## array if no matching button is found.[br] +## [b]Important:[/b] If a single UCID mapping from an [InSimMultiButton] matches, +## the complete button is included in the array; if you are using this method to delete +## buttons, make sure not to delete more buttons than intended. +func get_buttons_by_id_range(ucid: int, click_id: int, click_max := 0) -> Array[InSimButton]: + if not id_map.has(ucid) or not id_map[ucid].has(click_id): + return [] + var matching_buttons: Array[InSimButton] = [] + for button in buttons: + if button is InSimSoloButton: + var solo_button := button as InSimSoloButton + if ( + solo_button.ucid == ucid + and solo_button.click_id >= click_id + and solo_button.click_id <= click_max + ): + matching_buttons.append(solo_button) + elif button is InSimMultiButton: + var multi_button := button as InSimMultiButton + if ( + multi_button.ucid_mappings.has(ucid) + and multi_button.ucid_mappings[ucid].click_id >= click_id + and multi_button.ucid_mappings[ucid].click_id <= click_max + and multi_button not in matching_buttons + ): + matching_buttons.append(multi_button) + return matching_buttons + + +## Returns the first [InSimButton] matching the given [param name] for the given [param ucid], +## or [code]null[/code] if no matching button is found.[br] +## [b]Note:[/b] If multiple buttons have the same name, the returned button may not be +## the expected one; you should always make sure to give unique names to your buttons. +func get_button_by_name(ucid: int, name: StringName) -> InSimButton: + if not id_map.has(ucid): + return null + for button in buttons: + if button.name == name: + return button + return null + + +## Returns all [InSimButton]s whose name starts with the given [param prefix] for +## the given [param ucid], or an empty array if no matching button is found. +func get_buttons_by_prefix(ucid: int, prefix: StringName) -> Array[InSimButton]: + if not id_map.has(ucid): + return [] + var matching_buttons: Array[InSimButton] = [] + for button in buttons: + if button.name.begins_with(prefix): + matching_buttons.append(button) + return matching_buttons + + +## Returns all [InSimButton]s whose name matches the given [param regex] for the +## given [param ucid], or an empty array if no matching button is found. +func get_buttons_by_regex(ucid: int, regex: RegEx) -> Array[InSimButton]: + if not id_map.has(ucid): + return [] + var matching_buttons: Array[InSimButton] = [] + for button in buttons: + if regex.search(button.name): + matching_buttons.append(button) + return matching_buttons + + +## Returns all global buttons as an array of [InSimBTNPacket]s targetting the given +## [param for_ucid], to be sent to the corresponding player. +func get_global_button_packets(for_ucid: int) -> Array[InSimBTNPacket]: + var packets: Array[InSimBTNPacket] = [] + for button in _global_buttons: + var packet := button.get_btn_packet(for_ucid) + packets.append(packet) + _add_id_mapping(for_ucid, button.ucid_mappings[for_ucid].click_id) + return packets + + +## Returns all [InSimMultiButton]s that were either created with an empty UCID array, +## or later marked as global with [method set_global_multi_button]. +func get_global_buttons() -> Array[InSimMultiButton]: + return _global_buttons + + +## Sets the allowed clickID range for created buttons from [param min_id] to [param max_id] +## (both ends inclusive). Existing buttons with clickIDs outside of the new range are +## not deleted. +func set_click_id_range(min_id: int, max_id: int) -> void: + id_range = Vector2i(min_id, max_id) + + +## Sets the given [param button] as a global button, or unsets it. +## See [member _global_buttons] for more info. +func set_global_multi_button(button: InSimMultiButton, global: bool) -> void: + if global and button not in _global_buttons: + _global_buttons.append(button) + for ucid in insim.connections.keys() as Array[int]: + if not button.ucid_mappings.has(ucid): + button.add_ucid_mapping(ucid, _get_free_click_id(ucid), "") + elif not global: + _global_buttons.erase(button) + + +## Updates the given [param button]'s [param text] and [param caption], and optionally its +## [param type_in], and returns an [InSimBTNPacket] to be sent. A value of [code]-1[/code] +## for [param type_in] will leave it unchanged. To avoid changing the [param caption], +## this function should pass [code]button.caption[/code] to [param caption]. +func update_solo_button( + button: InSimSoloButton, text: String, caption := "", type_in := -1 +) -> InSimBTNPacket: + if button.ucid in disabled_ucids: + return null + button.text = text + button.caption = caption + if type_in > -1: + button.type_in = type_in + return button.get_btn_packet(true) + + +## Updates the [param text] of the given [param button], its [param caption], and optionally +## its [param type_in] (see [method update_solo_button] for more details), and returns +## an array of [InSimBTNPacket]s to be sent by [InSim]. As for [method add_multi_button], +## you can pass a [Callable] to [param text], [param caption], and [param type_in] for +## values tailored to each UCID.[br] +## [b]Note:[/b] The returned array only contains packets corresponding to buttons that +## were actually updated, to avoid sending unnecessary packets. +func update_multi_button( + button: InSimMultiButton, text: Variant, caption: Variant = "", type_in: Variant = -1 +) -> Array[InSimBTNPacket]: + var packets: Array[InSimBTNPacket] + for ucid in button.ucid_mappings: + _apply_multi_button_input( + button, ucid, button.ucid_mappings[ucid].click_id, text, caption, type_in + ) + if button.get_ucid_mapping(ucid).dirty_flag: + packets.append(button.get_btn_packet(ucid, true)) + return packets + + +# Intended for use by [InSim] only, when a new player joins the server. +func _add_global_button_mapping(ucid: int) -> void: + for button in _global_buttons: + button.add_ucid_mapping(ucid, _get_free_click_id(ucid), "") + + +func _add_id_mapping(ucid: int, click_id: int) -> void: + if not id_map.has(ucid): + id_map[ucid] = [] + id_map[ucid].erase(click_id) + id_map[ucid].append(click_id) + + +func _apply_multi_button_input( + button: InSimMultiButton, ucid: int, click_id: int, text: Variant, + caption: Variant, type_in: Variant, +) -> void: + var mapping := button.get_ucid_mapping(ucid) + if not mapping: + button.add_ucid_mapping(ucid, click_id, "") + var button_text := "^1INVALID" # Error message if no String or valid Callable + var text_type := typeof(text) + if text_type in [TYPE_STRING, TYPE_STRING_NAME]: + button_text = str(text) + elif text_type == TYPE_CALLABLE: + var text_function := text as Callable + if text_function.is_valid(): + button_text = text_function.call(ucid) + var button_caption := "^1INVALID" # Error message if no String or valid Callable + var caption_type := typeof(caption) + if caption_type in [TYPE_STRING, TYPE_STRING_NAME]: + button_caption = str(caption) + elif caption_type == TYPE_CALLABLE: + var caption_function := caption as Callable + if caption_function.is_valid(): + button_caption = caption_function.call(ucid) + var button_type_in := 0 + var type_in_type := typeof(type_in) + if type_in_type == TYPE_INT: + if type_in > -1: + button_type_in = type_in + elif type_in_type == TYPE_CALLABLE: + var type_in_function := type_in as Callable + if type_in_function.is_valid(): + button_type_in = type_in_function.call(ucid) + button.update_ucid_mapping(ucid, button_text, button_caption, button_type_in) + + +func _compact_bfn_packets(packets: Array[InSimBFNPacket]) -> Array[InSimBFNPacket]: + var compacted_packets: Array[InSimBFNPacket] = [] + packets.sort_custom(func(a: InSimBFNPacket, b: InSimBFNPacket) -> bool: + if 1000 * a.ucid + a.click_id < 1000 * b.ucid + b.click_id: + return true + return false + ) + var count := packets.size() + var i := 0 + while i < count: + var packet := packets[i] + var new_packet := InSimBFNPacket.create( + packet.subtype, packet.ucid, packet.click_id, packet.click_max + ) + var j := i + 1 + while j < count: + var next_packet := packets[j] + if ( + next_packet.ucid == packet.ucid + and ( + new_packet.click_max > new_packet.click_id + and next_packet.click_id == new_packet.click_max + 1 + or next_packet.click_id == new_packet.click_id + 1 + ) + ): + if next_packet.click_max > next_packet.click_id: + new_packet.click_max = next_packet.click_max + else: + new_packet.click_max = next_packet.click_id + i += 1 + j += 1 + else: + break + compacted_packets.append(new_packet) + i += 1 + return compacted_packets + + +# Returns -1 if no ID is available in id_range. +func _get_free_click_id(for_ucid: int) -> int: + if ( + for_ucid != InSim.UCID_ALL + and not id_map.has(for_ucid) + and not id_map.has(InSim.UCID_ALL) + ): + return id_range.x + if for_ucid == InSim.UCID_ALL: + for i in id_range.y - id_range.x + 1: + var test_id := id_range.x + i + if id_map.has(InSim.UCID_ALL) and id_map[InSim.UCID_ALL].has(test_id): + continue + var invalid_id := false + for ucid in id_map: + if id_map[ucid].has(test_id): + invalid_id = true + break + if invalid_id: + continue + return test_id + else: + for i in id_range.y - id_range.x + 1: + var test_id := id_range.x + i + if ( + id_map.has(for_ucid) and id_map[for_ucid].has(test_id) + or id_map.has(InSim.UCID_ALL) and id_map[InSim.UCID_ALL].has(test_id) + ): + continue + return test_id + return -1 + + +func _remove_button_mapping(for_ucid: int, click_id: int) -> void: + if id_map.has(for_ucid): + id_map[for_ucid].erase(click_id) + + +func _remove_id_mapping(ucid: int, click_id: int) -> void: + if not id_map.has(ucid): + return + id_map[ucid].erase(click_id) + if id_map[ucid].is_empty(): + var _existed := id_map.erase(ucid) diff --git a/addons/godot_insim/src/buttons/insim_buttons.gd.uid b/addons/godot_insim/src/buttons/insim_button_manager.gd.uid similarity index 100% rename from addons/godot_insim/src/buttons/insim_buttons.gd.uid rename to addons/godot_insim/src/buttons/insim_button_manager.gd.uid diff --git a/addons/godot_insim/src/buttons/insim_buttons.gd b/addons/godot_insim/src/buttons/insim_buttons.gd deleted file mode 100644 index bbbf48c..0000000 --- a/addons/godot_insim/src/buttons/insim_buttons.gd +++ /dev/null @@ -1,527 +0,0 @@ -class_name InSimButtons -extends RefCounted -## InSimButton manager -## -## This class keeps track of existing [InSimButton]s and allows adding/deleting buttons. -## An instance is included in the [InSim] object. - -## The maximum number of buttons across all InSim apps. -const MAX_BUTTONS := 240 - -## A reference to the parent [InSim] instance, used to fetch [member InSim.connections] for -## per-player button management. -var insim: InSim = null -## Determines whether buttons cleared by pressing [kbd]Shift + I[/kbd] are remembered or -## forgotten, i.e. whether their corresponding mappings are deleted. -## [signal InSim.connection_cleared_buttons] is emitted right after an InSim clear request. -var forget_cleared_buttons := false - -## The range of click IDs this instance can use. This range should remain as small as possible -## to allow other instances or InSim apps to manage their own range of buttons. -var id_range := Vector2i(0, MAX_BUTTONS - 1): - set(value): - id_range = Vector2i( - clampi(value.x, 0, MAX_BUTTONS - 1), - clampi(value.y, value.x, MAX_BUTTONS - 1) - ) -## A dictionary mapping all button clickIDs for every connection. You should not need -## to use this directly. -var id_map: Dictionary[int, Array] = {} -## Current known buttons, as a [Dictionary] with keys corresponding to connection IDs -## (including 0 for local buttons and 255 for buttons common to all connections), and values -## corresponding to [InSimButtonDictionary] objects for each connection ID, containing -## the actual [InSimButton] objects. Any InSim app using buttons should listen to [InSimBTNPacket] -## and [InSimBFNPacket] to keep this dictionary as up to date as possible, using -## [code]req_i[/code] to distinguish buttons created by other apps. -var buttons: Dictionary[int, InSimButtonDictionary] = {} -## The list of current global button IDs, i.e. buttons that were created with a UCID of 255; -## those are converted to individual buttons for each UCID in the server, and the buttons' -## clickIDs become reserved overall until the corresponding buttons are cleared. The dictionary's -## keys are the button clickIDs, and the values are the lists of UCIDs displaying buttons with -## those clickIDs. When a player connects, they are sent all currently existing global buttons; -## conversely, buttons are removed when a player disconnects. -var global_buttons: Dictionary[int, Array] = {} -## The list of UCIDs corresponding to players who have disabled InSim buttons (by pressing -## [kbd]Shift + I[/kbd]); adding and updating buttons (including global buttons with UCID 255) -## should be disabled for those. -var disabled_ucids: Array[int] = [] - - -func _init(insim_instance: InSim) -> void: - insim = insim_instance - - -## Creates an [InSimButton] from the given parameters, which is added to the [member buttons] -## [Dictionary] for each UCID in [param ucids]. Button creation can fail if there is no clickID -## available for the button for each UCID. Successfully created buttons generate a corresponding -## [InSimBTNPacket], an array of which is returned.[br] -## [param text] usually takes a [String], but you can create buttons with text tailored -## to each UCID in [param ucids] by passing a valid [Callable] instead, which takes an [int] -## as its single argument, corresponding to the player's UCID, and returns a [String]. -## See the example below, which sends a button to each connection and displays their name: -## [codeblock] -## insim.add_button( -## [], # empty array, will retrieve all connections -## Vector2i(0, 0), -## Vector2i(30, 5), -## InSim.ButtonStyle.ISB_DARK, -## func(ucid: int) -> String: return insim.get_connection_nickname(ucid), -## ) -## [/codeblock] -func add_button( - ucids: Array[int], position: Vector2i, size: Vector2i, style: int, text: Variant, - button_name := "", type_in := 0, caption := "", show_everywhere := false -) -> Array[InSimBTNPacket]: - var inst := 0 | (InSimButton.INST_ALWAYS_ON if show_everywhere else 0) - var packets: Array[InSimBTNPacket] = [] - var text_type := typeof(text) - var new_id := -1 - var global_button := false - # Allow UCID 255 if it is the only UCID passed to the function (for "true" global buttons - # that will update regardless of disabled_ucids). - if ucids.is_empty() or InSim.UCID_ALL in ucids and ucids.size() > 1: - global_button = true - ucids = insim.connections.keys() - new_id = get_free_global_id() - for ucid in ucids: - if ucid in disabled_ucids: - continue - if not has_ucid(ucid): - buttons[ucid] = InSimButtonDictionary.new() - if not global_button: - new_id = get_free_id(ucid) - if new_id == -1: - push_warning("Cannot create button for UCID %d: no clickID available." % [ucid]) - continue - var button_text := "^1INVALID" # Serves as an error message if no String or valid Callable - if text_type in [TYPE_STRING, TYPE_STRING_NAME]: - button_text = str(text) - elif text_type == TYPE_CALLABLE: - var text_function := text as Callable - if text_function.is_valid(): - button_text = text_function.call(ucid) - var button := InSimButton.create( - ucid, new_id, inst, style, position, size, button_text, button_name, type_in, caption - ) - register_buttons([button]) - if global_button: - _register_global_button(button) - var packet := button.get_btn_packet() - packets.append(packet) - return packets - - -## Adds a global button (shown to every player). See [method add_button] for more details. -func add_global_button( - position: Vector2i, size: Vector2i, style: int, text: Variant, - button_name := "", type_in := 0, caption := "", show_everywhere := false -) -> Array[InSimBTNPacket]: - return add_button( - [], position, size, style, text, button_name, type_in, caption, show_everywhere - ) - - -## Returns an array of [InSimBFNPacket]s requesting the deletion of the given button -## [param click_id] for all [param ucids]. If [param max_id] is non-zero, and greater than -## [param click_id], all buttons from [param click_id] to [param max_id] are deleted. -func delete_buttons_by_id(ucids: Array[int], click_id: int, max_id := 0) -> Array[InSimBFNPacket]: - var packets: Array[InSimBFNPacket] = [] - for ucid in ucids: - if not has_ucid(ucid): - continue - if buttons[ucid].has_id(click_id): - var packet := InSimBFNPacket.create( - InSim.ButtonFunction.BFN_DEL_BTN, ucid, click_id, max_id if max_id > click_id else 0 - ) - packets.append(packet) - _delete_button(ucid, click_id) - return packets - - -## Returns an array of [InSimBFNPacket]s requesting the deletion of the given button -## [param name] for all [param ucids]. -func delete_buttons_by_name(ucids: Array[int], name: StringName) -> Array[InSimBFNPacket]: - var packets: Array[InSimBFNPacket] = [] - for ucid in ucids: - if not has_ucid(ucid): - continue - for button_id in buttons[ucid].buttons: - var button := buttons[ucid].buttons[button_id] - if button.name == name: - packets.append_array(delete_buttons_by_id([ucid], button.click_id)) - _delete_button(ucid, button.click_id) - return packets - - -## Returns an array of [InSimBFNPacket]s requesting the deletion of all buttons that have a -## [member InSimButton.name] starting with [param prefix] for all [param ucids]. -func delete_buttons_by_prefix(ucids: Array[int], prefix: String) -> Array[InSimBFNPacket]: - var packets: Array[InSimBFNPacket] = [] - for ucid in ucids: - if not has_ucid(ucid): - continue - # For some reason, not adding .keys() only visits the first id... - for button_id in buttons[ucid].buttons.keys() as Array[int]: - var button := buttons[ucid].buttons[button_id] - if button.name.begins_with(prefix): - packets.append_array(delete_buttons_by_id([ucid], button.click_id)) - _delete_button(ucid, button.click_id) - return _compact_bfn_packets(packets) - - -## Returns an array of [InSimBFNPacket]s requesting the deletion of all buttons that have a -## [member InSimButton.name] matching the given [param regex] for all [param ucids]. -func delete_buttons_by_regex(ucids: Array[int], regex: RegEx) -> Array[InSimBFNPacket]: - var packets: Array[InSimBFNPacket] = [] - for ucid in ucids: - if not has_ucid(ucid): - continue - for button_id in buttons[ucid].buttons.keys() as Array[int]: - var button := buttons[ucid].buttons[button_id] - if regex.search(button.name): - packets.append_array(delete_buttons_by_id([ucid], button.click_id)) - _delete_button(ucid, button.click_id) - return _compact_bfn_packets(packets) - - -## Deletes a global button (shown to every player) selected by its click [param id]. See -## [method delete_buttons_by_id] for buttons specific to specific UCIDs. -func delete_global_buttons_by_id(id: int) -> Array[InSimBFNPacket]: - var packets := delete_buttons_by_id(insim.connections.keys(), id) - for packet in packets: - var _existed := global_buttons.erase(packet.click_id) - return packets - - -## Deletes a global button (shown to every player) selected by its [param name]. See -## [method delete_buttons_by_name] for buttons specific to specific UCIDs. -func delete_global_buttons_by_name(name: StringName) -> Array[InSimBFNPacket]: - var packets := delete_buttons_by_name(insim.connections.keys(), name) - for packet in packets: - var _existed := global_buttons.erase(packet.click_id) - return packets - - -## Deletes global buttons (shown to every player) selected by their [param prefix]. See -## [method delete_buttons_by_prefix] for buttons specific to specific UCIDs. -func delete_global_buttons_by_prefix(prefix: StringName) -> Array[InSimBFNPacket]: - var packets := delete_buttons_by_prefix(insim.connections.keys(), prefix) - for packet in packets: - var _existed := global_buttons.erase(packet.click_id) - return packets - - -## Deletes global buttons (shown to every player) matching [param regex]. See -## [method delete_buttons_by_regex] for buttons specific to specific UCIDs. -func delete_global_buttons_by_regex(regex: RegEx) -> Array[InSimBFNPacket]: - var packets := delete_buttons_by_regex(insim.connections.keys(), regex) - for packet in packets: - var _existed := global_buttons.erase(packet.click_id) - return packets - - -## Disables button updates by adding [param ucid] to [member disabled_ucids]. If -## [member forget_cleared_buttons] is [code]true[/code], all button mappings for [param ucid] -## are removed. -func disable_buttons_for_ucid(ucid: int) -> void: - if forget_cleared_buttons: - _forget_buttons_for_ucid(ucid) - disabled_ucids.erase(ucid) - disabled_ucids.append(ucid) - - -## Allows buttons to be sent to the given [param ucid] again, by removing it from -## [member disabled_ucids]. If button mappings were not removed, all remembered buttons are -## immediately sent again. -func enable_buttons_for_ucid(ucid: int) -> void: - disabled_ucids.erase(ucid) - if not id_map.has(ucid): - return - for click_id in id_map[ucid] as Array[int]: - var button := get_button_by_id(click_id, ucid) - if button: - insim.send_packet(button.get_btn_packet()) - - -## Returns the [InSimButton] at the given [param id], or [code]null[/code] if it does not exist. -func get_button_by_id(id: int, ucid: int) -> InSimButton: - if has_ucid(ucid): - if buttons[ucid].has_id(id): - return buttons[ucid].buttons[id] - return null - - -## Returns the first [InSimButton] matching the given [param name] and [param ucid], -## or [code]null[/code] if no matching button is found. -func get_button_by_name(name: StringName, ucid: int) -> InSimButton: - if has_ucid(ucid): - for click_id in buttons[ucid].buttons: - var button := buttons[ucid].buttons[click_id] - if button.name == name: - return button - return null - - -## Returns all [InSimButton]s whose [member InSimButton.name] starts with the given -## [param prefix] and [param ucid], or an empty array if no matching button is found. -func get_buttons_by_prefix(prefix: StringName, ucid: int) -> Array[InSimButton]: - var matching_buttons: Array[InSimButton] = [] - if has_ucid(ucid): - for click_id in buttons[ucid].buttons: - var button := buttons[ucid].buttons[click_id] - if button.name.begins_with(prefix): - matching_buttons.append(button) - return matching_buttons - - -## Returns a free [code]click_id[/code] value to create a new button for UCID [param for_ucid], -## or [code]-1[/code] if no ID is available in the [member id_range]. -func get_free_id(for_ucid: int) -> int: - if ( - for_ucid != InSim.UCID_ALL - and not id_map.has(for_ucid) - and not id_map.has(InSim.UCID_ALL) - ): - return id_range.x - if for_ucid == InSim.UCID_ALL: - for i in id_range.y - id_range.x + 1: - var test_id := id_range.x + i - if id_map.has(InSim.UCID_ALL) and id_map[InSim.UCID_ALL].has(test_id): - continue - var invalid_id := false - for ucid in id_map: - if id_map[ucid].has(test_id): - invalid_id = true - break - if invalid_id: - continue - return test_id - else: - for i in id_range.y - id_range.x + 1: - var test_id := id_range.x + i - if ( - id_map.has(for_ucid) and id_map[for_ucid].has(test_id) - or id_map.has(InSim.UCID_ALL) and id_map[InSim.UCID_ALL].has(test_id) - ): - continue - return test_id - return -1 - - -## Returns a free [code]click_id[/code] value to create a new button for all connected UCIDs, -## or [code]-1[/code] if no ID is available in the [member id_range]. -func get_free_global_id() -> int: - var ucids := insim.connections.keys() as Array[int] - ucids.append(InSim.UCID_ALL) - for i in id_range.y - id_range.x + 1: - var test_id := id_range.x + i - var valid_id := true - for ucid in ucids: - if not id_map.has(ucid): - continue - if id_map[ucid].has(test_id): - valid_id = false - break - if valid_id: - return test_id - return -1 - - -## Returns all global buttons as an array of [InSimBTNPacket]s, including the given -## [param for_ucid], to be sent to that UCID. -func get_global_buttons(for_ucid: int) -> Array[InSimBTNPacket]: - var packets: Array[InSimBTNPacket] = [] - for click_id in global_buttons: - var button := get_button_by_id(click_id, global_buttons[click_id][0] as int) - var packet := button.get_btn_packet() - packet.ucid = for_ucid - packets.append(packet) - _add_button_mapping(for_ucid, click_id) - return packets - - -## Returns an array of [InSimButton] objects corresponding to the global button identified by -## the given [param click_id]. -func get_global_button_by_id(click_id: int) -> Array[InSimButton]: - var found_buttons: Array[InSimButton] = [] - for ucid in insim.connections: - var button := get_button_by_id(click_id, ucid) - if button: - found_buttons.append(button) - return found_buttons - - -## Returns an array of [InSimButton] objects corresponding to the global button identified by -## the given [param name]. -func get_global_button_by_name(name: String) -> Array[InSimButton]: - var found_buttons: Array[InSimButton] = [] - for ucid in insim.connections: - var button := get_button_by_name(name, ucid) - if button: - found_buttons.append(button) - return found_buttons - - -## Returns an array of [InSimButton] objects corresponding to all global buttons matchin the -## given [param prefix]. -func get_global_buttons_by_prefix(prefix: String) -> Array[InSimButton]: - var found_buttons: Array[InSimButton] = [] - for ucid in insim.connections: - var button := get_buttons_by_prefix(prefix, ucid) - if button: - found_buttons.append(button) - return found_buttons - - -## Returns the clickID of the first global button matching the given [param button_name]. -## This can help get the clicKID without having to handle the array returned by -## [method get_global_button_by_name]. Returns [code]-1[/code] if no button can be found. -func get_global_button_id_from_name(button_name: String) -> int: - var button_array := insim.get_global_button_by_name(button_name) - if not button_array.is_empty(): - return button_array[0].click_id - return -1 - - -## Returns [code]true[/code] if the given [param ucid] has an entry in [member buttons]. -func has_ucid(ucid: int) -> bool: - return true if buttons.has(ucid) else false - - -## Adds the given [param new_buttons] to the [member id_map]. If [param register_global] is -## [code]true[/code], the clickIDs and UCIDs will also be added to [member global_buttons]. -func register_buttons(new_buttons: Array[InSimButton], register_global := false) -> void: - for button in new_buttons: - var ucid := button.ucid - var click_id := button.click_id - _add_button_mapping(ucid, click_id) - if register_global: - _register_global_button(button) - if not buttons.has(ucid): - buttons[ucid] = InSimButtonDictionary.new() - buttons[ucid].buttons[click_id] = button - - -## Sends all global buttons to UCID [param for_ucid], and registers them while doing so. -## [param for_ucid], to be sent to that UCID. -func restore_global_buttons(for_ucid: int) -> void: - var new_buttons: Array[InSimButton] = [] - for click_id in global_buttons: - if global_buttons[click_id].is_empty(): - var _existed := global_buttons.erase(click_id) - continue - var button := get_button_by_id(click_id, global_buttons[click_id][0] as int) - if not button: - continue - var new_button := InSimButton.create( - for_ucid, click_id, button.inst, button.style, button.position, button.size, - button.text, button.name, button.type_in, button.caption, - ) - new_buttons.append(new_button) - insim.send_packet(new_button.get_btn_packet()) - register_buttons(new_buttons, true) - - -## Updates the given [param button]'s [param text], and returns an [InSimBTNPacket] to be sent. -func update_button_text(button: InSimButton, text: String, caption := "") -> InSimBTNPacket: - if button.ucid in disabled_ucids: - return null - button.text = text - button.caption = caption - return button.get_btn_packet(true) - - -## Updates the text of the global button (show to every player) identified by its[param click_id], -## using the given [param text], which can be either a [String] or a [Callable] taking a single -## UCID parameter and returning a [String]. Returns an array of [InSimBTNPacket]s corresponding -## to the button's updated text. -func update_global_button_text(click_id: int, text: String, caption := "") -> Array[InSimBTNPacket]: - var packets: Array[InSimBTNPacket] - for button in get_global_button_by_id(click_id): - if button.ucid in disabled_ucids: - continue - button.text = text - button.caption = caption - packets.append(button.get_btn_packet(true)) - return packets - - -func _add_button_mapping(for_ucid: int, click_id: int) -> void: - if not id_map.has(for_ucid): - id_map[for_ucid] = [] - if not id_map[for_ucid].has(click_id): - id_map[for_ucid].append(click_id) - - -func _compact_bfn_packets(packets: Array[InSimBFNPacket]) -> Array[InSimBFNPacket]: - var compacted_packets: Array[InSimBFNPacket] = [] - packets.sort_custom(func(a: InSimBFNPacket, b: InSimBFNPacket) -> bool: - if 1000 * a.ucid + a.click_id < 1000 * b.ucid + b.click_id: - return true - return false - ) - var count := packets.size() - var i := 0 - while i < count: - var packet := packets[i] - var new_packet := InSimBFNPacket.create( - packet.subtype, packet.ucid, packet.click_id, packet.click_max - ) - var j := i + 1 - while j < count: - var next_packet := packets[j] - if ( - next_packet.ucid == packet.ucid - and ( - new_packet.click_max > new_packet.click_id - and next_packet.click_id == new_packet.click_max + 1 - or next_packet.click_id == new_packet.click_id + 1 - ) - ): - if next_packet.click_max > next_packet.click_id: - new_packet.click_max = next_packet.click_max - else: - new_packet.click_max = next_packet.click_id - i += 1 - j += 1 - else: - break - compacted_packets.append(new_packet) - i += 1 - return compacted_packets - - -func _delete_button(ucid: int, click_id: int) -> void: - _remove_button_mapping(ucid, click_id) - if not buttons.has(ucid): - return - var _discard := buttons[ucid].buttons.erase(click_id) - if buttons[ucid].buttons.is_empty(): - _discard = buttons.erase(ucid) - - -func _forget_buttons_for_ucid(ucid: int) -> void: - if id_map.has(ucid): - for click_id in id_map[ucid] as Array[int]: - if global_buttons.has(click_id): - for global_ucid in global_buttons[click_id] as Array[int]: - if global_ucid == ucid: - global_buttons[click_id].erase(global_ucid) - break - var _existed := buttons.erase(ucid) - _existed = id_map.erase(ucid) - disabled_ucids.erase(ucid) - - -func _register_global_button(button: InSimButton) -> void: - var click_id := button.click_id - var ucid := button.ucid - if click_id not in global_buttons: - global_buttons[click_id] = [] - if not global_buttons[click_id].has(ucid): - global_buttons[click_id].append(ucid) - - -func _remove_button_mapping(for_ucid: int, click_id: int) -> void: - if id_map.has(for_ucid): - id_map[for_ucid].erase(click_id) diff --git a/addons/godot_insim/src/insim/insim.gd b/addons/godot_insim/src/insim/insim.gd index c183521..91a90ae 100644 --- a/addons/godot_insim/src/insim/insim.gd +++ b/addons/godot_insim/src/insim/insim.gd @@ -992,15 +992,15 @@ var lfs_state := LFSState.new() var connections: Dictionary[int, Connection] = {} ## Dictionary of PLID/UCID pairs, updated automatically. var players: Dictionary[int, Player] = {} -## Dictionary of UCID/[InSimButtonDictionary] pairs containing all known [InSimButton] objects. -var buttons: InSimButtons = null +## Manager object for all known/registered [InSimButton] objects. +var button_manager: InSimButtonManager = null # Set to true in initialize(), used to silence unconnected InSim warning when sending packets var _initializing := false func _init() -> void: - buttons = InSimButtons.new(self) + button_manager = InSimButtonManager.new(self) nlp_mci_connection = LFSConnectionUDP.new() add_child(nlp_mci_connection) ping_timer = Timer.new() @@ -1287,188 +1287,165 @@ func send_packets(packets: Array[InSimPacket], sender := "InSim") -> void: #region Buttons -## Creates an [InSimButton] for all given [param ucids], and sends the corresponding -## [InSimBTNPacket]s. Button definition is mostly in line with [InSimBTNPacket], replacing -## [member InSimBTNPacket.inst] with the [param show_everywhere] boolean and using [Vector2i] -## for [param position] and [param size]. If [param ucids] is empty, the button will be sent -## to every connection; you should consider using [method add_global_button] instead.[br] -## When sending a button to multiple connections, you can map the button's text to each UCID -## by passing a [Callable] to [param text] instead of a regular string (see example code for -## [method InSimButtons.add_button]).[br] -## If you set [param type_in] to a value greater than [code]0[/code], the -## [constant InSim.ButtonStyle.ISB_CLICK] is automatically set. -func add_button( +## Sends an [InSimBTNPacket] corresponding to every button defined by the data passed. +## See [method add_solo_button] for more details on button definition. Internally, this +## calls [method InSimButtonManager.add_multi_button]. If [param ucids] is empty, the button +## will be considered global, and every player will receive it; additionally, any player +## joining the server will also receive that button. You can connect to the +## [signal global_buttons_restored] signal to [method update_multi_button].[br] +## When sending a button to multiple connections, you can map the button's text to +## each UCID by passing a [Callable] to [param text] instead of a regular [String] +## (see example code below). Similarly, you can customize the [param caption] and +## [param type_in]. For each of those parameters, the [Callable] must return the +## appropriate type, and should take a UCID as argument, so the value can be mapped +## to each UCID properly.[br] +## The following example shows how you can tailor the button's text to each player, +## by displaying their name in the upper-left corner of the screen: +## [codeblock] +## # Assuming your InSim instance is named insim +## insim.add_multi_button( +## [], # empty array, will retrieve all connections (global button) +## Vector2i(0, 0), +## Vector2i(30, 5), +## InSim.ButtonStyle.ISB_DARK, +## func(ucid: int) -> String: return insim.get_connection_nickname(ucid), +## ) +## [/codeblock] +func add_multi_button( ucids: Array[int], position: Vector2i, size: Vector2i, style: int, text: Variant, - button_name := "", type_in := 0, caption := "", show_everywhere := false, sender := "InSim", + button_name := "", type_in: Variant = 0, caption: Variant = "", show_everywhere := false, + sender := "InSim", ) -> void: - # Allow UCID 255 if it is the only UCID passed to the function; this results in - # "true" global buttons that bypass InSimButtons.disabled_ucids. - if UCID_ALL in ucids and ucids.size() > 1: - ucids.clear() if type_in > 0: style |= InSim.ButtonStyle.ISB_CLICK - for packet in buttons.add_button( + for packet in button_manager.add_multi_button( ucids, position, size, style, text, button_name, type_in, caption, show_everywhere ): send_packet(packet, sender) -## Creates an [InSimButton] for every connected player, and sends the corresponding -## [InSimBTNPacket]s. See [method add_button] for more details, as the parameters are the same, -## except UCIDs are not needed here. -func add_global_button( - position: Vector2i, size: Vector2i, style: int, text: Variant, - button_name := "", type_in := 0, caption := "", show_everywhere := false, sender := "InSim", +## Sends an [InSimBTNPacket] corresponding to the data passed. Button definition is +## mostly in line with [InSimBTNPacket], replacing [member InSimBTNPacket.inst] with +## the [param show_everywhere] boolean and using [Vector2i] for [param position] and +## [param size]. Internally, this function calls [method InSimButtonManager.add_solo_button].[br] +## If you set [param type_in] to a value greater than [code]0[/code], the +## [constant InSim.ButtonStyle.ISB_CLICK] flag is automatically added to [param style].[br] +## [b]Note:[/b] This function allows you to create a single button, i.e. fixed data for +## a given UCID (including [constant UCID_ALL]) and a given (generated) clickID. +## If you want to send a button to multiple players (or everyone), and possibly customize +## the contents for every player, use [method add_multi_button] instead. +func add_solo_button( + ucid: int, position: Vector2i, size: Vector2i, style: int, text: String, button_name := "", + type_in := 0, caption := "", show_everywhere := false, sender := "InSim", ) -> void: if type_in > 0: style |= InSim.ButtonStyle.ISB_CLICK - for packet in buttons.add_global_button( - position, size, style, text, button_name, type_in, caption, show_everywhere - ): - send_packet(packet, sender) - - -## Deletes an [InSimButton] and sends the corresponding [InSimBFNPacket] for all [param ucids], -## based on the given button [param click_id]. If [param ucids] is empty, this function will try -## to delete the button for every UCID in the current connection list. If [param max_id] is greater -## than [param click_id], all buttons from [param click_id] to [param max_id] are deleted. -func delete_buttons_by_id(ucids: Array[int], click_id: int, max_id := 0, sender := "InSim") -> void: - if ucids.is_empty() or UCID_ALL in ucids and ucids.size() > 1: - ucids = connections.keys() - for packet in buttons.delete_buttons_by_id(ucids, click_id, max_id): - send_packet(packet, sender) - - -## Deletes all [InSimButton]s matching the given [param button_name] for all [param ucids], -## and sends the corresponding [InSimBFNPacket]s. If [param ucids] is empty, this function will try -## to delete buttons for every UCID in the current connection list. -func delete_buttons_by_name(ucids: Array[int], button_name: StringName, sender := "InSim") -> void: - if ucids.is_empty() or UCID_ALL in ucids and ucids.size() > 1: - ucids = connections.keys() - for packet in buttons.delete_buttons_by_name(ucids, button_name): - send_packet(packet, sender) - - -## Deletes all [InSimButton]s with a name starting with [param prefix] for all [param ucids], -## and sends the corresponding [InSimBFNPacket]s. If [param ucids] is empty, this function -## will try to delete buttons for every UCID in the current connection list. -func delete_buttons_by_prefix(ucids: Array[int], prefix: StringName, sender := "InSim") -> void: - if ucids.is_empty() or UCID_ALL in ucids and ucids.size() > 1: - ucids = connections.keys() - for packet in buttons.delete_buttons_by_prefix(ucids, prefix): - send_packet(packet, sender) - - -## Deletes all [InSimButton]s with a name matching the given [param regex] for all [param ucids], -## and sends the corresponding [InSimBFNPacket]s. If [param ucids] is empty, this functions -## will try to delete buttons for every UCID in the current connection list. -func delete_buttons_by_regex(ucids: Array[int], regex: RegEx, sender := "InSim") -> void: - if ucids.is_empty() or UCID_ALL in ucids and ucids.size() > 1: - ucids = connections.keys() - for packet in buttons.delete_buttons_by_regex(ucids, regex): - send_packet(packet, sender) - - -## Deletes a global button (shown to every player) selected by its click [param id]. -func delete_global_button_by_id(id: int, sender := "InSim") -> void: - for packet in buttons.delete_global_buttons_by_id(id): - send_packet(packet, sender) - - -## Deletes a global button (shown to every player) selected by its [param button_name]. -func delete_global_button_by_name(button_name: StringName, sender := "InSim") -> void: - for packet in buttons.delete_global_buttons_by_name(button_name): + var packet := button_manager.add_solo_button( + ucid, position, size, style, text, button_name, type_in, caption, show_everywhere + ) + if packet: send_packet(packet, sender) -## Deletes global buttons (shown to every player) selected by their [param prefix]. -func delete_global_buttons_by_prefix(prefix: StringName, sender := "InSim") -> void: - for packet in buttons.delete_global_buttons_by_prefix(prefix): +## Deletes an [InSimButton] using the given [param button] reference, and sends the +## corresponding [InSimBFNPacket]s. When deleting an [InSimMultiButton], [param ucids] +## allows you to delete the button only for some players, while leaving it empty will +## delete it for everyone; [param ucids] is ignored when deleting an [InSimSoloButton]. +## If you want to delete multiple buttons in one call, you can use [method delete_buttons]. +func delete_button(button: InSimButton, ucids: Array[int] = [], sender := "InSim") -> void: + for packet in button_manager.delete_button(button, ucids): send_packet(packet, sender) -## Deletes global buttons (shown to every player) matching [param regex]. -func delete_global_buttons_by_regex(regex: RegEx, sender := "InSim") -> void: - for packet in buttons.delete_global_buttons_by_regex(regex): +## Deletes all given buttons [param to_delete], calling [method delete_button] for each +## of them, and sends the corresponding [InSimBFNPacket]s. Passing an non-empty +## [param ucids] array allows you to "filter" which players will have their buttons +## deleted (only for [InSimMultiButton], as [InSimSoloButton] will always be deleted). +func delete_buttons( + to_delete: Array[InSimButton], ucids: Array[int] = [], sender := "InSim" +) -> void: + for packet in button_manager.delete_buttons(to_delete, ucids): send_packet(packet, sender) -## Disables button updates for the given [param ucid]. See [member InSimButtons.disabled_ucids] -## and [member InSimButtons.forget_cleared_buttons] for details, and -## [method enable_buttons_for_ucid] for the opposite method. +## Disables button updates for the given [param ucid]. +## See [member InSimButtonManager.disabled_ucids] for details, +## and [method enable_buttons_for_ucid] for the opposite method. func disable_buttons_for_ucid(ucid: int) -> void: - buttons.disable_buttons_for_ucid(ucid) + button_manager.disable_buttons_for_ucid(ucid) -## Enables button updates for the given [param ucid]. Removes [param ucid] from -## [member InSimButtons.disabled_ucids] +## Enables button updates for the given [param ucid]. +## See [member InSimButtonManager.disabled_ucids] for details, +## and [method disable_buttons_for_ucid] for the opposite method. func enable_buttons_for_ucid(ucid: int) -> void: - buttons.enable_buttons_for_ucid(ucid) + button_manager.enable_buttons_for_ucid(ucid) -## Returns the [InSimButton] at the given [param id], or [code]null[/code] if it does not exist. -func get_button_by_id(id: int, ucid: int) -> InSimButton: - return buttons.get_button_by_id(id, ucid) +## Returns the [InSimButton] matching the given [param ucid] and [param click_id], or +## [code]null[/code] if no matching button is found. +func get_button_by_id(ucid: int, click_id: int) -> InSimButton: + return button_manager.get_button_by_id(ucid, click_id) -## Returns the first [InSimButton] matching the given [param button_name] and [param ucid], -## or [code]null[/code] if no matching button is found. -func get_button_by_name(button_name: StringName, ucid: int) -> InSimButton: - return buttons.get_button_by_name(button_name, ucid) +## Returns the first [InSimButton] matching the given [param ucid] and [param button_name], +## or [code]null[/code] if no matching button is found.[br] +## [b]Note:[/b] If multiple buttons have the same name, the returned button may not be +## the expected one; you should always make sure to give unique names to your buttons. +func get_button_by_name(ucid: int, button_name: StringName) -> InSimButton: + return button_manager.get_button_by_name(ucid, button_name) -## Returns all [InSimButton]s whose [member InSimButton.name] starts with the given -## [param prefix] and [param ucid], or an empty array if no matching button is found. -func get_buttons_by_prefix(prefix: StringName, ucid: int) -> Array[InSimButton]: - return buttons.get_buttons_by_prefix(prefix, ucid) +## Returns an [InSimButton] [Array] containing all buttons with clickIDs in the range +## defined by [param click_id] and [param click_max] (both ends inclusive) for the +## given [param ucid], or an empty array if no matching button is found. +func get_buttons_by_id_range(ucid: int, click_id: int, click_max := 0) -> Array[InSimButton]: + return button_manager.get_buttons_by_id_range(ucid, click_id, click_max) -## Returns the [InSimButton] at the given [param id], or [code]null[/code] if it does not exist. -func get_global_button_by_id(id: int) -> Array[InSimButton]: - return buttons.get_global_button_by_id(id) +## Returns all [InSimButton]s whose name starts with the given [param prefix] for +## the given [param ucid], or an empty array if no matching button is found. +func get_buttons_by_prefix(ucid: int, prefix: StringName) -> Array[InSimButton]: + return button_manager.get_buttons_by_prefix(ucid, prefix) -## Returns the first [InSimButton] matching the given [param button_name] and [param button_name], -## or [code]null[/code] if no matching button is found. -func get_global_button_by_name(button_name: StringName) -> Array[InSimButton]: - return buttons.get_global_button_by_name(button_name) +## Returns all [InSimButton]s whose name matches the given [param regex] for the +## given [param ucid], or an empty array if no matching button is found. +func get_buttons_by_regex(ucid: int, regex: RegEx) -> Array[InSimButton]: + return button_manager.get_buttons_by_regex(ucid, regex) -## Returns all [InSimButton]s whose [member InSimButton.name] starts with the given -## [param prefix], or an empty array if no matching button is found. -func get_global_buttons_by_prefix(prefix: StringName) -> Array[InSimButton]: - return buttons.get_global_buttons_by_prefix(prefix) +## Sends all [member InSimButtonManager._global_buttons] to the player associated +## with the given [param ucid], and emits the [signal global_buttons_restored], which +## you can connect to in order to update those buttons' contents. +func restore_global_buttons(ucid: int) -> void: + for packet in button_manager.get_global_button_packets(ucid): + send_packet(packet) + global_buttons_restored.emit(ucid) -## Updates the text of the given [param button] to [param text], with an optional [param caption]. -func update_button_text( - button: InSimButton, text: String, caption := "", sender := "InSim" +## Updates the contents of the [param button] using the given [param text] and +## [param caption]. +func update_solo_button( + button: InSimSoloButton, text: String, caption := "", sender := "InSim" ) -> void: if not button: push_error("Cannot update button text, button is null.") return - send_packet(buttons.update_button_text(button, text, caption), sender) + send_packet(button_manager.update_solo_button(button, text, caption), sender) -## Updates the text of the global button (shown to everyone) with the given [param text], which -## can be either a [String] or a [Callable] taking a [code]ucid[/code] parameter and returning -## a [String]. -func update_global_button_text( - click_id: int, text: String, caption := "", sender := "InSim" +## Updates the text of the [param button] with the given [param text], [param caption], +## and [param type_in], using be either a [String] ([int] for [param type_in]) or +## a [Callable] taking a [code]ucid[/code] parameter and returning a [String] ([int] +## for [param type_in]). The default value of [code]-1[/code] for [param type_in] leaves +## its value unchanged. +func update_multi_button( + button: InSimMultiButton, text: Variant, caption: Variant = "", type_in: Variant = -1, + sender := "InSim", ) -> void: - for packet in buttons.update_global_button_text(click_id, text, caption): + for packet in button_manager.update_multi_button(button, text, caption, type_in): send_packet(packet, sender) - - -# Removes all buttons and mappings for the given ucid. -func _forget_buttons_for_ucid(ucid: int) -> void: - buttons._forget_buttons_for_ucid(ucid) - - -# Adds global buttons to the given ucid. -func _send_global_buttons(to_ucid: int) -> void: - buttons.restore_global_buttons(to_ucid) - global_buttons_restored.emit(to_ucid) #endregion @@ -1781,7 +1758,7 @@ func _on_bfn_packet_received(packet: InSimBFNPacket) -> void: func _on_cnl_packet_received(packet: InSimCNLPacket) -> void: var _discard := connections.erase(packet.ucid) - _forget_buttons_for_ucid(packet.ucid) + button_manager.forget_buttons_for_ucid(packet.ucid) lfs_state.num_connections = packet.total @@ -1798,11 +1775,14 @@ func _on_ism_packet_received(packet: InSimISMPacket) -> void: func _on_ncn_packet_received(packet: InSimNCNPacket) -> void: - if packet.req_i in [0, GIS_REQI]: - connections[packet.ucid] = Connection.create_from_ncn_packet(packet) - if packet.req_i == 0: - _send_global_buttons(packet.ucid) - lfs_state.num_connections = packet.total + if packet.req_i not in [0, GIS_REQI]: + return + var ucid := packet.ucid + connections[ucid] = Connection.create_from_ncn_packet(packet) + lfs_state.num_connections = packet.total + if packet.req_i == 0: + button_manager._add_global_button_mapping(ucid) + restore_global_buttons(ucid) func _on_npl_packet_received(packet: InSimNPLPacket) -> void: diff --git a/addons/godot_insim/test/buttons/test_insim_buttons.gd b/addons/godot_insim/test/buttons/test_insim_buttons.gd index 40744e6..953bf2a 100644 --- a/addons/godot_insim/test/buttons/test_insim_buttons.gd +++ b/addons/godot_insim/test/buttons/test_insim_buttons.gd @@ -14,8 +14,21 @@ func before_test() -> void: insim.insim_connected = true -func test_add_button() -> void: - var packets := insim.buttons.add_button( +func test_add_solo_button() -> void: + var packet := insim.button_manager.add_solo_button( + 1, Vector2i(10, 50), Vector2i(20, 5), InSim.ButtonStyle.ISB_DARK, "text", "button" + ) + var _test: GdUnitAssert = assert_object(packet).is_equal( + InSimBTNPacket.create(1, 0, 0, InSim.ButtonStyle.ISB_DARK, 0, 10, 50, 20, 5, "text") + ) + _test = assert_array(insim.button_manager.buttons).has_size(1) + var button := insim.button_manager.buttons[0] + _test = assert_object(button).is_instanceof(InSimSoloButton) + _test = assert_str(button.name).is_equal("button") + + +func test_add_multi_button() -> void: + var packets := insim.button_manager.add_multi_button( [1, 2], Vector2i(10, 50), Vector2i(20, 5), InSim.ButtonStyle.ISB_DARK, "text", "button" ) var _test: GdUnitAssert = assert_int(packets.size()).is_equal(2) @@ -23,10 +36,22 @@ func test_add_button() -> void: InSimBTNPacket.create(1, 0, 0, InSim.ButtonStyle.ISB_DARK, 0, 10, 50, 20, 5, "text"), InSimBTNPacket.create(2, 0, 0, InSim.ButtonStyle.ISB_DARK, 0, 10, 50, 20, 5, "text"), ]) - _test = assert_bool( - insim.get_button_by_name("button", 1) != null - and insim.get_button_by_name("button", 2) != null - ).is_true() + _test = assert_array(insim.button_manager.buttons).has_size(1) + var button := insim.button_manager.buttons[0] + _test = assert_object(button).is_instanceof(InSimMultiButton) + _test = assert_str(button.name).is_equal("button") + _test = assert_array((button as InSimMultiButton).ucid_mappings.keys()).is_equal([1, 2]) + + +func test_add_multiple_buttons() -> void: + insim.add_solo_button(0, Vector2i.ONE, Vector2i.ONE, 0, "") + insim.add_multi_button([0, 1, 2], Vector2i.ONE, Vector2i.ONE, 0, "") + var _test: GdUnitAssert = assert_array(insim.button_manager.buttons).has_size(2) + _test = assert_dict(insim.button_manager.id_map).is_equal({ + 0: [0, 1], + 1: [0], + 2: [0], + }) func test_compact_bfn_packets() -> void: @@ -48,7 +73,7 @@ func test_compact_bfn_packets() -> void: InSimBFNPacket.create(InSim.ButtonFunction.BFN_DEL_BTN, 4, 5, 0), InSimBFNPacket.create(InSim.ButtonFunction.BFN_DEL_BTN, 4, 21, 0), ] - var compacted := insim.buttons._compact_bfn_packets(packets) + var compacted := insim.button_manager._compact_bfn_packets(packets) var buffer_expected: Array[int] = [] for packet in expected: packet.fill_buffer() @@ -60,57 +85,110 @@ func test_compact_bfn_packets() -> void: var _test := assert_array(buffer_compacted).is_equal(buffer_expected) -func test_delete_buttons_by_id() -> void: - insim.add_button([1, 2], Vector2i.ZERO, Vector2i.ZERO, 0, "", "button") - insim.add_button([2, 3], Vector2i.ZERO, Vector2i.ZERO, 0, "", "test_button") - insim.add_button([1, 2], Vector2i.ZERO, Vector2i.ZERO, 0, "", "hello") - var packets := insim.buttons.delete_buttons_by_id([1, 2], 1, 2) - var _test: GdUnitAssert = assert_int(packets.size()).is_equal(2) - # NOTE: UCID 1 only has buttons 0 and 1, but only the click_id is checked for existence, so - # the expected result is still click_id == 1 and max_id == 2. - _test = assert_array([ - packets[0].ucid, packets[0].click_id, packets[0].click_max, - packets[1].ucid, packets[1].click_id, packets[1].click_max, - ]).is_equal([1, 1, 2, 2, 1, 2]) - - -func test_delete_buttons_by_name() -> void: - insim.add_button([1, 2], Vector2i.ZERO, Vector2i.ZERO, 0, "", "button") - insim.add_button([2, 3], Vector2i.ZERO, Vector2i.ZERO, 0, "", "test_button") - insim.add_button([1, 2], Vector2i.ZERO, Vector2i.ZERO, 0, "", "hello") - var packets := insim.buttons.delete_buttons_by_prefix([1, 2], "button") - var _test: GdUnitAssert = assert_int(packets.size()).is_equal(2) - _test = assert_array([ - packets[0].ucid, packets[0].click_id, packets[0].click_max, - packets[1].ucid, packets[1].click_id, packets[1].click_max, - ]).is_equal([1, 0, 0, 2, 0, 0]) - - -func test_delete_buttons_by_prefix() -> void: - insim.add_button([1, 2], Vector2i.ZERO, Vector2i.ZERO, 0, "", "a/b") - insim.add_button([2, 3], Vector2i.ZERO, Vector2i.ZERO, 0, "", "c/d/e") - insim.add_button([1, 2], Vector2i.ZERO, Vector2i.ZERO, 0, "", "c/desktop") - var packets := insim.buttons.delete_buttons_by_prefix([1, 2], "c/d") - var _test: GdUnitAssert = assert_int(packets.size()).is_equal(2) - _test = assert_array([ - packets[0].ucid, packets[0].click_id, packets[0].click_max, - packets[1].ucid, packets[1].click_id, packets[1].click_max, - ]).is_equal([1, 1, 0, 2, 1, 2]) - - -func test_delete_buttons_by_regex() -> void: - insim.add_button([1, 2], Vector2i.ZERO, Vector2i.ZERO, 0, "", "a/b") - insim.add_button([2, 3], Vector2i.ZERO, Vector2i.ZERO, 0, "", "c/d/e") - insim.add_button([1, 2], Vector2i.ZERO, Vector2i.ZERO, 0, "", "no_category") - var packets := insim.buttons.delete_buttons_by_regex([1, 2], RegEx.create_from_string(".+?/.+")) - var _test: GdUnitAssert = assert_int(packets.size()).is_equal(2) - _test = assert_array([ - packets[0].ucid, packets[0].click_id, packets[0].click_max, - packets[1].ucid, packets[1].click_id, packets[1].click_max, - ]).is_equal([1, 0, 0, 2, 0, 1]) - packets = insim.buttons.delete_buttons_by_regex([1, 2], RegEx.create_from_string(".+?_cat.*?")) - _test = assert_int(packets.size()).is_equal(2) - _test = assert_array([ - packets[0].ucid, packets[0].click_id, packets[0].click_max, - packets[1].ucid, packets[1].click_id, packets[1].click_max, - ]).is_equal([1, 1, 0, 2, 2, 0]) +func test_delete_button() -> void: + insim.add_solo_button(0, Vector2i.ONE, Vector2i.ONE, 0, "") + insim.delete_button(insim.button_manager.buttons[0]) + var _test: GdUnitAssert = assert_array(insim.button_manager.buttons).is_empty() + insim.add_multi_button([0, 1, 2], Vector2i.ONE, Vector2i.ONE, 0, "") + var multi_button := insim.button_manager.buttons[0] as InSimMultiButton + insim.delete_button(multi_button, [0, 1]) + _test = assert_array(insim.button_manager.buttons).has_size(1) + _test = assert_dict(insim.button_manager.id_map).is_equal({2: [0]}) + + +func test_delete_buttons() -> void: + insim.add_solo_button(0, Vector2i.ONE, Vector2i.ONE, 0, "") + var solo_button := insim.button_manager.buttons[-1] as InSimSoloButton + insim.add_multi_button([0, 1, 2], Vector2i.ONE, Vector2i.ONE, 0, "") + var multi_button := insim.button_manager.buttons[-1] as InSimMultiButton + insim.delete_button(solo_button, [2]) # UCIDs should be ignored for InSimSoloButton. + insim.delete_button(multi_button, [0, 1]) + var _test: GdUnitAssert = assert_array(insim.button_manager.buttons).has_size(1) + _test = assert_dict(insim.button_manager.id_map).is_equal({2: [0]}) + + + +func test_get_button_by_id() -> void: + insim.add_multi_button([1, 2], Vector2i.ONE, Vector2i.ONE, 0, "", "button") + insim.add_multi_button([2, 3], Vector2i.ONE, Vector2i.ONE, 0, "", "test_button") + insim.add_multi_button([1, 2], Vector2i.ONE, Vector2i.ONE, 0, "", "hello") + insim.add_solo_button(1, Vector2i.ONE, Vector2i.ONE, 0, "", "another_button") + var button := insim.get_button_by_id(1, 1) + var _test: GdUnitAssert = assert_object(button).is_not_null() + _test = assert_object(button).is_instanceof(InSimMultiButton) + button = insim.get_button_by_id(2, 1) + _test = assert_object(button).is_not_null() + _test = assert_object(button).is_instanceof(InSimMultiButton) + button = insim.get_button_by_id(1, 2) + _test = assert_object(button).is_not_null() + _test = assert_object(button).is_instanceof(InSimSoloButton) + button = insim.get_button_by_id(3, 2) + _test = assert_object(button).is_null() + + +func test_get_button_by_id_range() -> void: + insim.add_multi_button([1, 2], Vector2i.ONE, Vector2i.ONE, 0, "", "button") + insim.add_multi_button([2, 3], Vector2i.ONE, Vector2i.ONE, 0, "", "test_button") + insim.add_multi_button([1, 2], Vector2i.ONE, Vector2i.ONE, 0, "", "hello") + insim.add_solo_button(1, Vector2i.ONE, Vector2i.ONE, 0, "", "another_button") + var buttons := insim.get_buttons_by_id_range(1, 1, 2) + var _test: GdUnitAssert = assert_int(buttons.size()).is_equal(2) + _test = assert_object(buttons[0]).is_not_null() + _test = assert_object(buttons[0]).is_instanceof(InSimMultiButton) + _test = assert_object(buttons[1]).is_not_null() + _test = assert_object(buttons[1]).is_instanceof(InSimSoloButton) + var multi_button := buttons[0] as InSimMultiButton + _test = assert_int(multi_button.ucid_mappings[1].click_id).is_equal(1) + + +func test_get_buttons_by_name() -> void: + insim.add_multi_button([1, 2], Vector2i.ONE, Vector2i.ONE, 0, "", "button") + insim.add_multi_button([2, 3], Vector2i.ONE, Vector2i.ONE, 0, "", "test_button") + insim.add_multi_button([1, 2], Vector2i.ONE, Vector2i.ONE, 0, "", "hello") + var button := insim.button_manager.get_button_by_name(2, "button") + var _test: GdUnitAssert = assert_object(button).is_not_null() + _test = assert_object(button).is_instanceof(InSimMultiButton) + + +func test_get_buttons_by_prefix() -> void: + insim.add_multi_button([1, 2], Vector2i.ONE, Vector2i.ONE, 0, "", "a/b") + insim.add_multi_button([2, 3], Vector2i.ONE, Vector2i.ONE, 0, "", "c/d/e") + insim.add_multi_button([1, 2], Vector2i.ONE, Vector2i.ONE, 0, "", "c/desktop") + var buttons := insim.button_manager.get_buttons_by_prefix(2, "c/d") + var _test: GdUnitAssert = assert_array(buttons).has_size(2) + _test = assert_object(buttons[0]).is_not_null() + _test = assert_object(buttons[1]).is_not_null() + _test = assert_str(buttons[0].name).is_equal("c/d/e") + _test = assert_str(buttons[1].name).is_equal("c/desktop") + + +func test_get_buttons_by_regex() -> void: + insim.add_multi_button([1, 2], Vector2i.ONE, Vector2i.ONE, 0, "", "a/b") + insim.add_multi_button([2, 3], Vector2i.ONE, Vector2i.ONE, 0, "", "c/d/e") + insim.add_multi_button([1, 2], Vector2i.ONE, Vector2i.ONE, 0, "", "no_category") + var buttons := insim.button_manager.get_buttons_by_regex(2, RegEx.create_from_string(".+?/.+")) + var _test: GdUnitAssert = assert_array(buttons).has_size(2) + buttons = insim.button_manager.get_buttons_by_regex(2, RegEx.create_from_string(".+?_cat.*?")) + _test = assert_array(buttons).has_size(1) + + +func test_update_solo_button() -> void: + insim.add_solo_button(0, Vector2i.ONE, Vector2i.ONE, 0, "", "button 1") + var solo_button := insim.button_manager.buttons[-1] as InSimSoloButton + insim.add_multi_button([1, 2], Vector2i.ONE, Vector2i.ONE, 0, "", "button 2") + var multi_button := insim.button_manager.buttons[-1] as InSimMultiButton + insim.update_solo_button(solo_button, "test") + insim.update_multi_button(multi_button, "update") + var _test: GdUnitAssert = assert_str(solo_button.text).is_equal("test") + _test = assert_str(multi_button.ucid_mappings[1].text).is_equal("update") + + +func test_update_multi_button_callable_text() -> void: + insim.add_multi_button([1, 2], Vector2i.ONE, Vector2i.ONE, 0, "", "button 2") + var button := insim.button_manager.buttons[-1] as InSimMultiButton + var set_text_to_ucid := func(ucid: int) -> String: + return str(ucid) + insim.update_multi_button(button, set_text_to_ucid) + for ucid in button.ucid_mappings: + var mapping := button.ucid_mappings[ucid] + var _test: GdUnitAssert = assert_str(mapping.text).is_equal(str(mapping.ucid)) -- GitLab