diff --git a/CHANGELOG_DRAFT.md b/CHANGELOG_DRAFT.md index 7710678..e64a08c 100644 --- a/CHANGELOG_DRAFT.md +++ b/CHANGELOG_DRAFT.md @@ -1,6 +1,15 @@ ## [NEXT] -## [2.1.8] — 2026-04-28 +## [2.1.9] — 2026-04-29 +- Added friend system with friend list, direct messaging, and lobby invitations +- Improved Steam login support +- Added Social Panel with Friends, Global Chat, and Direct Message tabs +- Added ability to add/remove friends from lobby player slots +- Added Invite Friends button to lobby +- Added lobby invite notifications +- Fixed account type display in profile panel + +## [2.1.8] — 2026-04-24 - Optimized network synchronization with deterministic tile slots and granular board sync - Removed lag-sensitive server adjacency checks to fix "disappearing tiles" on high-latency connections - Fixed "No multiplayer peer assigned" crash during host disconnection and cleanup diff --git a/TEMP_REPORT.md b/TEMP_REPORT.md new file mode 100644 index 0000000..d91886f --- /dev/null +++ b/TEMP_REPORT.md @@ -0,0 +1,86 @@ +# [ ADT ] Report: Nakama Friend System & Steam Auth Implementation + +## Session Summary +Implemented comprehensive Nakama friend system with lobby invites, direct messaging, global chat, and Steam authentication fallback. Migrated all UI from dynamic node creation to scene-based architecture. + +## Completed Changes + +### Server (Nakama) +✅ Added `afterAuthenticateSteam` hook in `tekton_admin.js` to set Steam persona name as display name +✅ Added `rpcSendLobbyInvite` RPC for sending lobby invite notifications to friends + +### Authentication +✅ Implemented Steam login fallback to email-style auth in `auth_manager.gd` for dev/testing without publisher key +✅ Set Steam username as display name with default password matching username +✅ Set `is_guest = false` and `auth_mode = "steam"` on Steam login + +### Friend System +✅ Created `FriendManager.gd` autoload singleton for Nakama friend list, DM channels, and lobby invite notifications +✅ Registered FriendManager in `project.godot` autoload +✅ Added `nakama_id` field to LobbyManager player data +✅ Extended `request_room_info` RPC to include `nakama_id` for all players + +### Lobby UI +✅ Added optional `InviteBtn` reference in `lobby.gd` with safe node resolution +✅ Connected `invite_btn.pressed` to open invite friends dialog +✅ Connected FriendManager signals for lobby invite notifications +✅ Added + Friend buttons in player slots with friend status display +✅ Implemented invite friends popup using `invite_friends_dialog.tscn` scene +✅ Implemented lobby invite notification popup using `lobby_invite_popup.tscn` scene +✅ Added social panel modal opening from lobby + +### Social Panel +✅ Created `social_panel.tscn` with full UI layout (tabs, friend list, global chat, DM views) +✅ Created `social_panel.gd` with `@onready` references (no dynamic UI creation) +✅ Implemented friend list using `friend_row.tscn` instances +✅ Implemented global chat and DM tabs with UI nodes +✅ Handled DM history and message sending + +### UI Scenes (All .tscn based, no .new() calls) +✅ Created `friend_row.tscn` + `friend_row.gd` (friend list row with chat/accept/decline/remove buttons) +✅ Created `invite_friends_dialog.tscn` + `invite_friends_dialog.gd` (invite friends popup) +✅ Created `invite_row.tscn` + `invite_row.gd` (single invite row with invite button) +✅ Created `lobby_invite_popup.tscn` + `lobby_invite_popup.gd` (lobby invite notification dialog) + +### Profile Panel +✅ Fixed account type display to include Steam auth mode in `profile_panel.gd` + +### Version & Changelog +✅ Updated `CHANGELOG_DRAFT.md` with all changes (7 player-facing entries) +✅ Bumped version to 2.1.9 via `generate_version_json.py` +✅ Updated `assets/data/version.json` with new version and changelog + +## Files Modified +- `server/nakama/tekton_admin.js` +- `scripts/managers/auth_manager.gd` +- `scripts/managers/friend_manager.gd` +- `scripts/managers/lobby_manager.gd` +- `scripts/ui/profile_panel.gd` +- `scripts/ui/social_panel.gd` +- `scripts/ui/friend_row.gd` +- `scripts/ui/invite_friends_dialog.gd` +- `scripts/ui/invite_row.gd` +- `scripts/ui/lobby_invite_popup.gd` +- `scenes/lobby.gd` +- `project.godot` +- `CHANGELOG_DRAFT.md` +- `assets/data/version.json` + +## Files Created +- `scenes/ui/social_panel.tscn` +- `scenes/ui/friend_row.tscn` +- `scenes/ui/invite_friends_dialog.tscn` +- `scenes/ui/invite_row.tscn` +- `scenes/ui/lobby_invite_popup.tscn` + +## Version Bumped +**2.1.8 → 2.1.9** (2026-04-29) + +## Player-Facing Changelog +- Added friend system with friend list, direct messaging, and lobby invitations +- Improved Steam login support +- Added Social Panel with Friends, Global Chat, and Direct Message tabs +- Added ability to add/remove friends from lobby player slots +- Added Invite Friends button to lobby +- Added lobby invite notifications +- Fixed account type display in profile panel diff --git a/addons/godotsteam/androidarm64/libgodotsteam.android.template_debug.arm64.so b/addons/godotsteam/androidarm64/libgodotsteam.android.template_debug.arm64.so new file mode 100644 index 0000000..5381b65 Binary files /dev/null and b/addons/godotsteam/androidarm64/libgodotsteam.android.template_debug.arm64.so differ diff --git a/addons/godotsteam/androidarm64/libgodotsteam.android.template_release.arm64.so b/addons/godotsteam/androidarm64/libgodotsteam.android.template_release.arm64.so new file mode 100644 index 0000000..a03bd03 Binary files /dev/null and b/addons/godotsteam/androidarm64/libgodotsteam.android.template_release.arm64.so differ diff --git a/addons/godotsteam/androidarm64/libsteam_api.so b/addons/godotsteam/androidarm64/libsteam_api.so new file mode 100644 index 0000000..4c0bd94 Binary files /dev/null and b/addons/godotsteam/androidarm64/libsteam_api.so differ diff --git a/addons/godotsteam/editor/steamworks_panel.gd b/addons/godotsteam/editor/steamworks_panel.gd new file mode 100644 index 0000000..a43b78b --- /dev/null +++ b/addons/godotsteam/editor/steamworks_panel.gd @@ -0,0 +1,8 @@ +@tool +extends Control + +@onready var container_frame: VBoxContainer = %ContainerFrame + + +func add_companion_dock(companion_dock: Control) -> void: + container_frame.add_child(companion_dock) diff --git a/addons/godotsteam/editor/steamworks_panel.gd.uid b/addons/godotsteam/editor/steamworks_panel.gd.uid new file mode 100644 index 0000000..fa8bce3 --- /dev/null +++ b/addons/godotsteam/editor/steamworks_panel.gd.uid @@ -0,0 +1 @@ +uid://1oo0gvamoxgp diff --git a/addons/godotsteam/editor/steamworks_panel.tscn b/addons/godotsteam/editor/steamworks_panel.tscn new file mode 100644 index 0000000..fdaf9c1 --- /dev/null +++ b/addons/godotsteam/editor/steamworks_panel.tscn @@ -0,0 +1,125 @@ +[gd_scene load_steps=6 format=3 uid="uid://cyniebd6yahu5"] + +[ext_resource type="Script" uid="uid://1oo0gvamoxgp" path="res://addons/godotsteam/editor/steamworks_panel.gd" id="1_qn6cd"] +[ext_resource type="Script" uid="uid://b4nvgu7h84u53" path="res://addons/godotsteam/editor/updates/updates.gd" id="2_qn6cd"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qn6cd"] +content_margin_top = 2.0 +content_margin_bottom = 2.0 +bg_color = Color(0.0980392, 0.0980392, 0.0980392, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qf1vo"] +content_margin_top = 2.0 +content_margin_bottom = 2.0 +bg_color = Color(0.337255, 0.619608, 1, 1) + +[sub_resource type="Theme" id="Theme_sqmbs"] +ProgressBar/styles/background = SubResource("StyleBoxFlat_qn6cd") +ProgressBar/styles/fill = SubResource("StyleBoxFlat_qf1vo") + +[node name="Steamworks" type="Control"] +process_mode = 3 +custom_minimum_size = Vector2(0, 150) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_qn6cd") +metadata/_edit_lock_ = true + +[node name="Scroller" type="ScrollContainer" parent="."] +custom_minimum_size = Vector2(0, 150) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +metadata/_edit_lock_ = true + +[node name="DockFrame" type="TabContainer" parent="Scroller"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +current_tab = 0 +metadata/_edit_lock_ = true + +[node name="Updates" type="MarginContainer" parent="Scroller/DockFrame"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 +script = ExtResource("2_qn6cd") +metadata/_tab_index = 0 +metadata/_edit_lock_ = true + +[node name="ContainerFrame" type="VBoxContainer" parent="Scroller/DockFrame/Updates"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_constants/separation = 10 +metadata/_edit_lock_ = true + +[node name="HFrame" type="HBoxContainer" parent="Scroller/DockFrame/Updates/ContainerFrame"] +layout_mode = 2 +theme_override_constants/separation = 25 +metadata/_edit_lock_ = true + +[node name="Type" type="Label" parent="Scroller/DockFrame/Updates/ContainerFrame/HFrame"] +custom_minimum_size = Vector2(125, 25) +layout_mode = 2 +text = "GodotSteam" +metadata/_edit_lock_ = true + +[node name="InstalledLabel" type="Label" parent="Scroller/DockFrame/Updates/ContainerFrame/HFrame"] +unique_name_in_owner = true +layout_mode = 2 +text = "Installed version 4.18" +metadata/_edit_lock_ = true + +[node name="UpdateLabel" type="Label" parent="Scroller/DockFrame/Updates/ContainerFrame/HFrame"] +unique_name_in_owner = true +layout_mode = 2 +metadata/_edit_lock_ = true + +[node name="Updating" type="HBoxContainer" parent="Scroller/DockFrame/Updates/ContainerFrame/HFrame"] +layout_mode = 2 +size_flags_horizontal = 10 +theme_override_constants/separation = 25 +metadata/_edit_lock_ = true + +[node name="Downloading" type="ProgressBar" parent="Scroller/DockFrame/Updates/ContainerFrame/HFrame/Updating"] +unique_name_in_owner = true +visible = false +custom_minimum_size = Vector2(200, 30) +layout_mode = 2 +theme = SubResource("Theme_sqmbs") +step = 1.0 +rounded = true +metadata/_edit_lock_ = true + +[node name="InstallButton" type="Button" parent="Scroller/DockFrame/Updates/ContainerFrame/HFrame/Updating"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +text = "Install" +metadata/_edit_lock_ = true + +[node name="UpdateButton" type="Button" parent="Scroller/DockFrame/Updates/ContainerFrame/HFrame/Updating"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 8 +disabled = true +text = "Up-to-date" +metadata/_edit_lock_ = true + +[node name="CancelButton" type="Button" parent="Scroller/DockFrame/Updates/ContainerFrame/HFrame/Updating"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +text = "Cancel" +metadata/_edit_lock_ = true diff --git a/addons/godotsteam/editor/ui/steam.svg b/addons/godotsteam/editor/ui/steam.svg new file mode 100644 index 0000000..f3daeb9 --- /dev/null +++ b/addons/godotsteam/editor/ui/steam.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/godotsteam/editor/ui/steam.svg.import b/addons/godotsteam/editor/ui/steam.svg.import new file mode 100644 index 0000000..5a52b8c --- /dev/null +++ b/addons/godotsteam/editor/ui/steam.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dhn3vkdxvetbn" +path="res://.godot/imported/steam.svg-1c1b1c87b286630e8735047e7af015ac.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/godotsteam/editor/ui/steam.svg" +dest_files=["res://.godot/imported/steam.svg-1c1b1c87b286630e8735047e7af015ac.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/godotsteam/editor/updates/updates.gd b/addons/godotsteam/editor/updates/updates.gd new file mode 100644 index 0000000..c115c31 --- /dev/null +++ b/addons/godotsteam/editor/updates/updates.gd @@ -0,0 +1,186 @@ +@tool +extends MarginContainer +### +# Super-huge thanks to Nathan Hoad and Marcus Skov for ideas and inspiration on how to create a +# plug-in updater. +### + +const TEMP_FILE = "user://update.zip" +const VERSION_URL: StringName = "https://godotengine.org/asset-library/api/asset/2445" + +var download_location: String = "" +var downloading_update: bool = false +# This will act as both our version checker and update downloader +var http_request: HTTPRequest = null +var is_actively_downloading: bool = false +var new_version: String = "" + +@onready var cancel_button: Button = %CancelButton +@onready var downloading: ProgressBar = %Downloading +@onready var install_button: Button = %InstallButton +@onready var installed_label: Label = %InstalledLabel +@onready var update_label: Label = %UpdateLabel +@onready var update_button: Button = %UpdateButton + + +func _ready() -> void: + connect_signals() + set_defaults() + check_for_updates() + + +func _process(delta: float) -> void: + if is_actively_downloading: + downloading.value = http_request.get_downloaded_bytes() * 100 / http_request.get_body_size() + + +#region Setup +func set_defaults() -> void: + downloading.value = 0 + installed_label.text = "Installed version %s" % Steam.get_godotsteam_version() + update_button.text = "Up-to-date" + update_button.disabled = true + update_label.text = "" + updating_visibility(false, false, false, true) +#endregion + + +#region Signals +func connect_signals() -> void: + cancel_button.pressed.connect(_on_cancel_pressed) + install_button.pressed.connect(_on_install_pressed) + update_button.pressed.connect(_on_update_pressed) +#endregion + + +#region Checking for updates +func check_for_updates() -> void: + if not ProjectSettings.get_setting("steam/updates/godotsteam/check_for_updates"): + return + + http_request = HTTPRequest.new() + add_child(http_request) + http_request.request_completed.connect(_on_http_request_completed) + if http_request.request(VERSION_URL) != OK: + printerr("Failed to request GodotSteam plug-in remote current version") + + +func _on_http_request_completed(result: int, _response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: + http_request.request_completed.disconnect(_on_http_request_completed) + http_request.queue_free() + + if result != HTTPRequest.RESULT_SUCCESS: + return + var response = JSON.parse_string(body.get_string_from_utf8()) + if response == null: + return + + download_location = response.download_url + new_version = response.version_string + + if convert_version(new_version) > convert_version(Steam.get_godotsteam_version()): + print("New GodotSteam version %s available" % new_version) + update_label.text = "New version %s" % new_version + update_button.text = "Download" + update_button.disabled = false +#endregion + + +#region Updating versions +func _on_cancel_pressed() -> void: + updating_visibility(false, false, false, true) + + if is_actively_downloading: + http_request.cancel_request() + http_request.queue_free() + is_actively_downloading = false + update_label.text = "Canceling %s plug-in update download" % new_version + else: + DirAccess.remove_absolute(TEMP_FILE) + update_label.text = "Canceling %s plug-in update and deleting temporary files" % new_version + + update_button.text = "Download" + update_button.disabled = false + update_label.text = "New version %s" % new_version + + +func _on_install_pressed() -> void: + print("Removing older GodotSteam %s plug-in" % Steam.get_godotsteam_version()) + OS.move_to_trash(ProjectSettings.globalize_path("res://addons/godotsteam")) + + update_button.text = "Unpacking" + print("Unpacking new %s plug-in" % new_version) + var zip_reader: ZIPReader = ZIPReader.new() + zip_reader.open(TEMP_FILE) + var files: PackedStringArray = zip_reader.get_files() + + var base_path := files[1] + # Remove archive folder + files.remove_at(0) + # Remove assets folder + files.remove_at(0) + + for path in files: + var new_file_path: String = path.replace(base_path, "") + if path.ends_with("/"): + DirAccess.make_dir_recursive_absolute("res://addons/%s" % new_file_path) + else: + var file: FileAccess = FileAccess.open("res://addons/%s" % new_file_path, FileAccess.WRITE) + file.store_buffer(zip_reader.read_file(path)) + + zip_reader.close() + restart_post_update() + + +func _on_update_pressed() -> void: + is_actively_downloading = true + update_label.text = "Downloading %s plug-in update, please wait" % new_version + updating_visibility(true, true, false, false) + + http_request = HTTPRequest.new() + add_child(http_request) + http_request.request_completed.connect(_on_download_request_completed) + if http_request.request(download_location) != OK: + printerr("Failed to request %s plug-in update download" % new_version) + is_actively_downloading = false + update_button.text = "Failed" + update_button.disabled = false + return + + +func _on_download_request_completed(result: int, _response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: + http_request.request_completed.disconnect(_on_download_request_completed) + http_request.queue_free() + is_actively_downloading = false + updating_visibility(true, false, true, false) + + if result != HTTPRequest.RESULT_SUCCESS: + printerr("Failed to download new GodotSteam version %s" % result) + update_button.disabled = false + return + + update_label.text = "Update downloaded, ready to install %s" % new_version + var zip_file: FileAccess = FileAccess.open(TEMP_FILE, FileAccess.WRITE) + zip_file.store_buffer(body) + zip_file.close() + + +func restart_post_update() -> void: + update_button.text = "Restarting" + update_label.text = "Updated to version %s, restarting the editor" % new_version + DirAccess.remove_absolute(TEMP_FILE) + EditorInterface.restart_editor(true) +#endregion + + +#region Helpers +func convert_version(version_string: String) -> int: + return int(version_string.replace(".", "").rpad(4, "0")) + + +func updating_visibility(cancel: bool, download: bool, install: bool, update: bool) -> void: + cancel_button.visible = cancel + downloading.visible = download + install_button.visible = install + update_button.visible = update +#endregion diff --git a/addons/godotsteam/editor/updates/updates.gd.uid b/addons/godotsteam/editor/updates/updates.gd.uid new file mode 100644 index 0000000..75d0d30 --- /dev/null +++ b/addons/godotsteam/editor/updates/updates.gd.uid @@ -0,0 +1 @@ +uid://b4nvgu7h84u53 diff --git a/addons/godotsteam/godotsteam.gdextension b/addons/godotsteam/godotsteam.gdextension new file mode 100644 index 0000000..0a2e48b --- /dev/null +++ b/addons/godotsteam/godotsteam.gdextension @@ -0,0 +1,29 @@ +[configuration] +entry_symbol = "godotsteam_init" +compatibility_minimum = "4.4" + +[libraries] +android.debug.arm64 = "res://addons/godotsteam/androidarm64/libgodotsteam.android.template_debug.arm64.so" +android.release.arm64 = "res://addons/godotsteam/androidarm64/libgodotsteam.android.template_release.arm64.so" +linux.debug.arm64 = "res://addons/godotsteam/linuxarm64/libgodotsteam.linux.template_debug.arm64.so" +linux.debug.x86_64 = "res://addons/godotsteam/linux64/libgodotsteam.linux.template_debug.x86_64.so" +linux.debug.x86_32 = "res://addons/godotsteam/linux32/libgodotsteam.linux.template_debug.x86_32.so" +linux.release.arm64 = "res://addons/godotsteam/linuxarm64/libgodotsteam.linux.template_release.arm64.so" +linux.release.x86_64 = "res://addons/godotsteam/linux64/libgodotsteam.linux.template_release.x86_64.so" +linux.release.x86_32 = "res://addons/godotsteam/linux32/libgodotsteam.linux.template_release.x86_32.so" +macos.debug = "res://addons/godotsteam/osx/libgodotsteam.macos.template_debug.dylib" +macos.release = "res://addons/godotsteam/osx/libgodotsteam.macos.template_release.dylib" +windows.debug.x86_64 = "res://addons/godotsteam/win64/libgodotsteam.windows.template_debug.x86_64.dll" +windows.debug.x86_32 = "res://addons/godotsteam/win32/libgodotsteam.windows.template_debug.x86_32.dll" +windows.release.x86_64 = "res://addons/godotsteam/win64/libgodotsteam.windows.template_release.x86_64.dll" +windows.release.x86_32 = "res://addons/godotsteam/win32/libgodotsteam.windows.template_release.x86_32.dll" + +[dependencies] +android.arm64 = { "res://addons/godotsteam/androidarm64/libsteam_api.so": "" } +linux.arm64 = { "res://addons/godotsteam/linuxarm64/libsteam_api.so": "" } +linux.x86_64 = { "res://addons/godotsteam/linux64/libsteam_api.so": "" } +linux.x86_32 = { "res://addons/godotsteam/linux32/libsteam_api.so": "" } +macos.universal = { "res://addons/godotsteam/osx/libsteam_api.dylib": "" } +windows.x86_64 = { "res://addons/godotsteam/win64/steam_api64.dll": "" } +windows.x86_32 = { "res://addons/godotsteam/win32/steam_api.dll": "" } + diff --git a/addons/godotsteam/godotsteam.gdextension.uid b/addons/godotsteam/godotsteam.gdextension.uid new file mode 100644 index 0000000..3ed3a92 --- /dev/null +++ b/addons/godotsteam/godotsteam.gdextension.uid @@ -0,0 +1 @@ +uid://cxrfuk7agwsov diff --git a/addons/godotsteam/godotsteam_plugin.gd b/addons/godotsteam/godotsteam_plugin.gd new file mode 100644 index 0000000..097ca12 --- /dev/null +++ b/addons/godotsteam/godotsteam_plugin.gd @@ -0,0 +1,85 @@ +@tool +class_name GodotSteamPlugin +extends EditorPlugin + +const EDITOR_PANEL = preload("uid://cyniebd6yahu5") + +static var dock_frame + +var link_changelog: String = "[url=https://godotsteam.com/changelog/gdextension/]changelog[/url]" +var link_website: String = "[url=https://godotsteam.com]website[/url]" +var steamworks_dock: Control + + +## Used specifically to add/remove additional dock content +static func get_dock_frame() -> Control: + return dock_frame + + +func _enable_plugin() -> void: + print("GodotSteam GDExtension updater functionality enabled") + + +func _disable_plugin() -> void: + print("GodotSteam GDEXtension updater functionality disabled") + + +func _enter_tree() -> void: + print_rich("GodotSteam v%s | %s | %s" % [Steam.get_godotsteam_version(), link_website, link_changelog]) + add_project_settings() + add_steamworks_dock() + + +func _exit_tree() -> void: + remove_steamworks_dock() + + +func _make_visible(visible) -> void: + if steamworks_dock: + steamworks_dock.set_visible(visible) + + +#region Add and remove things +func add_project_settings() -> void: + # Used for the Updater looking for redist files and SteamCMD + if not ProjectSettings.has_setting("steam/updates/godotsteam/check_for_updates"): + ProjectSettings.set_setting("steam/updates/godotsteam/check_for_updates", true) + ProjectSettings.add_property_info({ + "name": "steam/updates/godotsteam/check_for_updates", + "type": TYPE_BOOL + }) + ProjectSettings.set_initial_value("steam/updates/godotsteam/check_for_updates", true) + ProjectSettings.set_as_basic("steam/updates/godotsteam/check_for_updates", true) + # Which channel of updates to pull from + # Sponsors repo should require the user to have access to that repository already + # In theory, they can connect via SSH? + if not ProjectSettings.has_setting("steam/updates/godotsteam/update_channel"): + ProjectSettings.set_setting("steam/updates/godotsteam/update_channel", 0) + ProjectSettings.add_property_info({ + "name": "steam/updates/godotsteam/update_channel", + "type": TYPE_INT, + "hint": PROPERTY_HINT_ENUM, + "hint_string": "Community, Sponsors" + }) + ProjectSettings.set_initial_value("steam/updates/godotsteam/update_channel", 0) + ProjectSettings.set_as_basic("steam/updates/godotsteam/update_channel", true) + + +func add_steamworks_dock() -> void: + steamworks_dock = EDITOR_PANEL.instantiate() + # This will be used when 4.4.x is deprecated + #add_control_to_dock(DockSlot.DOCK_SLOT_BOTTOM, steamworks_dock) + # This is deprecated as of 4.6; when it is removed then 4.4.x will be deprecated for GodotSteam + add_control_to_bottom_panel(steamworks_dock, "Steamworks") + dock_frame = steamworks_dock + + +func remove_steamworks_dock() -> void: + # This will be used when 4.4.x is deprecated + #remove_control_from_docks(steamworks_dock) + # This is deprecated as of 4.6; when it is removed then 4.4.x will be deprecated for GodotSteam + remove_control_from_bottom_panel(steamworks_dock) + steamworks_dock.queue_free() + steamworks_dock = null + dock_frame = null +#endregion diff --git a/addons/godotsteam/godotsteam_plugin.gd.uid b/addons/godotsteam/godotsteam_plugin.gd.uid new file mode 100644 index 0000000..e46d46d --- /dev/null +++ b/addons/godotsteam/godotsteam_plugin.gd.uid @@ -0,0 +1 @@ +uid://btts11rgropcp diff --git a/addons/godotsteam/license.md b/addons/godotsteam/license.md new file mode 100644 index 0000000..ecadf79 --- /dev/null +++ b/addons/godotsteam/license.md @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2015-Current | GP Garcia, Chris Ridenour, and Contributors + +View all contributors at https://godotsteam.com/contribute/contributors/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addons/godotsteam/linux32/libgodotsteam.linux.template_debug.x86_32.so b/addons/godotsteam/linux32/libgodotsteam.linux.template_debug.x86_32.so new file mode 100644 index 0000000..0a672c2 Binary files /dev/null and b/addons/godotsteam/linux32/libgodotsteam.linux.template_debug.x86_32.so differ diff --git a/addons/godotsteam/linux32/libgodotsteam.linux.template_release.x86_32.so b/addons/godotsteam/linux32/libgodotsteam.linux.template_release.x86_32.so new file mode 100644 index 0000000..00eb551 Binary files /dev/null and b/addons/godotsteam/linux32/libgodotsteam.linux.template_release.x86_32.so differ diff --git a/addons/godotsteam/linux32/libsteam_api.so b/addons/godotsteam/linux32/libsteam_api.so new file mode 100644 index 0000000..cd2df7a Binary files /dev/null and b/addons/godotsteam/linux32/libsteam_api.so differ diff --git a/addons/godotsteam/linux64/libgodotsteam.linux.template_debug.x86_64.so b/addons/godotsteam/linux64/libgodotsteam.linux.template_debug.x86_64.so new file mode 100644 index 0000000..650e154 Binary files /dev/null and b/addons/godotsteam/linux64/libgodotsteam.linux.template_debug.x86_64.so differ diff --git a/addons/godotsteam/linux64/libgodotsteam.linux.template_release.x86_64.so b/addons/godotsteam/linux64/libgodotsteam.linux.template_release.x86_64.so new file mode 100644 index 0000000..1f7b5b4 Binary files /dev/null and b/addons/godotsteam/linux64/libgodotsteam.linux.template_release.x86_64.so differ diff --git a/addons/godotsteam/linux64/libsteam_api.so b/addons/godotsteam/linux64/libsteam_api.so new file mode 100644 index 0000000..9a16ad9 Binary files /dev/null and b/addons/godotsteam/linux64/libsteam_api.so differ diff --git a/addons/godotsteam/linuxarm64/libgodotsteam.linux.template_debug.arm64.so b/addons/godotsteam/linuxarm64/libgodotsteam.linux.template_debug.arm64.so new file mode 100644 index 0000000..feb5a68 Binary files /dev/null and b/addons/godotsteam/linuxarm64/libgodotsteam.linux.template_debug.arm64.so differ diff --git a/addons/godotsteam/linuxarm64/libgodotsteam.linux.template_release.arm64.so b/addons/godotsteam/linuxarm64/libgodotsteam.linux.template_release.arm64.so new file mode 100644 index 0000000..524c05c Binary files /dev/null and b/addons/godotsteam/linuxarm64/libgodotsteam.linux.template_release.arm64.so differ diff --git a/addons/godotsteam/linuxarm64/libsteam_api.so b/addons/godotsteam/linuxarm64/libsteam_api.so new file mode 100644 index 0000000..da30b41 Binary files /dev/null and b/addons/godotsteam/linuxarm64/libsteam_api.so differ diff --git a/addons/godotsteam/osx/libgodotsteam.macos.template_debug.dylib b/addons/godotsteam/osx/libgodotsteam.macos.template_debug.dylib new file mode 100644 index 0000000..f9be1f8 Binary files /dev/null and b/addons/godotsteam/osx/libgodotsteam.macos.template_debug.dylib differ diff --git a/addons/godotsteam/osx/libgodotsteam.macos.template_release.dylib b/addons/godotsteam/osx/libgodotsteam.macos.template_release.dylib new file mode 100644 index 0000000..4f91989 Binary files /dev/null and b/addons/godotsteam/osx/libgodotsteam.macos.template_release.dylib differ diff --git a/addons/godotsteam/osx/libsteam_api.dylib b/addons/godotsteam/osx/libsteam_api.dylib new file mode 100644 index 0000000..3e1a940 Binary files /dev/null and b/addons/godotsteam/osx/libsteam_api.dylib differ diff --git a/addons/godotsteam/plugin.cfg b/addons/godotsteam/plugin.cfg new file mode 100644 index 0000000..e1e00c1 --- /dev/null +++ b/addons/godotsteam/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="GodotSteam GDExtension Updater" +description="An updater for GodotSteam GDExtension. This is not required to use Steamworks functionality, just updating the plug-in." +author="Gramps Garcia, GodotSteam" +version="4.18.1" +script="godotsteam_plugin.gd" diff --git a/addons/godotsteam/readme.md b/addons/godotsteam/readme.md new file mode 100644 index 0000000..5c32d08 --- /dev/null +++ b/addons/godotsteam/readme.md @@ -0,0 +1,93 @@ +# GodotSteam for GDExtension | Community Edition +An ecosystem of tools for [Godot Engine](https://godotengine.org) and [Valve's Steam](https://store.steampowered.com). For the Windows, Linux, and Mac platforms. + + +Additional Flavors +--- +Standard Module | Standard Plug-ins | Server Module | Server Plug-ins | Examples +--- | --- | --- | --- | --- +[Godot 2.x](https://codeberg.org/godotsteam/godotsteam/src/branch/godot2) | [GDNative](https://codeberg.org/godotsteam/godotsteam/src/branch/gdnative) | [Server 3.x](https://codeberg.org/godotsteam/godotsteam-server/src/branch/godot3) | [GDNative](https://codeberg.org/godotsteam/godotsteam-server/src/branch/gdnative) | [Skillet](https://codeberg.org/godotsteam/skillet) +[Godot 3.x](https://codeberg.org/godotsteam/godotsteam/src/branch/godot3) | [GDExtension](https://codeberg.org/godotsteam/godotsteam/src/branch/gdextension) | [Server 4.x](https://codeberg.org/godotsteam/godotsteam-server/src/branch/godot4) | [GDExtension](https://codeberg.org/godotsteam/godotsteam-server/src/branch/gdextension) | [Skillet UGC Editor](https://codeberg.org/godotsteam/skillet/src/branch/ugc_editor) +[Godot 4.x](https://codeberg.org/godotsteam/godotsteam/src/branch/godot4) | --- | --- | --- | --- +[MultiplayerPeer](https://codeberg.org/godotsteam/multiplayerpeer)| --- | --- | --- | --- + + +Documentation +--- +[Documentation is available here](https://godotsteam.com/). You can also check out the Search Help section inside Godot Engine. [To start, try checking out our tutorial on initializing Steam.](https://godotsteam.com/tutorials/initializing/) There are additional tutorials, with more in the works. You can also [check out additional Godot and Steam related videos, text, additional tools, plug-ins, etc. here.](https://godotsteam.com/resources/external/) + +Feel free to chat with us about GodotSteam or ask for assistance on the [Stoat server](https://stt.gg/9DxQ3Dcd) or [IRC on Libera Chat](irc://irc.libera.chat/#godotsteam). + + +Donate +--- +Pull-requests are the best way to help the project out but you can also donate through [Github Sponsors](https://github.com/sponsors/Gramps) or [LiberaPay](https://liberapay.com/godotsteam/donate)! [You can read more about donor perks here.](https://godotsteam.com/contribute/donations/) [You can also view all our awesome donors here.](https://godotsteam.com/contribute/donors/) + + +Current Build +--- +You can [download pre-compiled versions of this repo here](https://codeberg.org/godotsteam/godotsteam/releases). + +**Version 4.18.1 Changes** + +- Fixed: dock scripts for Godot 4.4.x - 4.5.x + +[You can read more change-logs here](https://godotsteam.com/changelog/gdextension/). + + +Compatibility +--- +While rare, sometimes Steamworks SDK updates will break compatilibity with older GodotSteam versions. Any compatability breaks are noted below. Newer API files (dll, so, dylib) _should_ still work for older versions. + +Steamworks SDK Version | GodotSteam Version +---|--- +1.63 or newer | 4.17 +1.62 | 4.14 or 4.16.2 +1.61 | 4.12 to 4.13 +1.60 | 4.6 to 4.11 +1.59 | 4.6 to 4.8 +1.58a or older | 4.5.4 or older + +Versions of GodotSteam that have compatibility breaks introduced. + +GodotSteam Version | Broken Compatibility +---|--- +4.8 | Networking identity system removed, replaced with Steam IDs +4.9 | sendMessages returns an Array +4.11 | setLeaderboardDetailsMax removed +4.13 | getItemDefinitionProperty return a dictionary, html_needs_paint key 'bgra' changed to 'rbga' +4.14 | Removed first argument for stat request in steamInit and steamInitEx, steamInit returns intended bool value +4.16 | Variety of small break points, refer to [4.16 changelog for details](https://godotsteam.com/changelog/godot4/) +4.17 | Windows projects using Steam SDK 1.63 are meant to work with Proton 11 or Experimental on Linux / Steam Deck. + + +Known Issues +--- +- GDExtension for 4.4 is **not** compatible with 4.3.x or lower. Please check the versions you are using. +- Overlay will not work in the editor but will work in export projects when uploaded to Steam. This seems to a limitation with Vulkan currently. + + +Quick How-To +--- +For complete instructions on how to build the GDExtension version of GodotSteam, [please refer to our documentation's 'How-To GDExtension' section.](https://godotsteam.com/howto/gdextension/) It will have the most up-to-date information. + +Alternatively, you can just [download the pre-compiled versions in our Releases section](https://codeberg.org/godotsteam/godotsteam/releases) or [from the Godot Asset Library](https://godotengine.org/asset-library/asset/2445) and skip compiling it yourself! + + +Usage +--- +Once the plug-in is added to your project, the Steam class should be available and ready to go. Enabling the plug-in in the ProjectSettings only affects the Steamworks dock and not the actual functionality. + +Do not use the GDExtension version of GodotSteam with any of the module versions whether it be our pre-compiled versions or ones you compile. They are not compatible with each other. + +When exporting with the GDExtension version, please use the normal Godot Engine templates instead of our GodotSteam templates or you will have a lot of issues. + + +No LLM Policy / No "AI" Policy +--- +No LLMs are allowed to be used for issues, patches, or pull-requests. They will be closed or rejected and the submitter may be blocked from future submissions. + + +License +--- +MIT license diff --git a/addons/godotsteam/win32/libgodotsteam.windows.template_debug.x86_32.dll b/addons/godotsteam/win32/libgodotsteam.windows.template_debug.x86_32.dll new file mode 100644 index 0000000..718907e Binary files /dev/null and b/addons/godotsteam/win32/libgodotsteam.windows.template_debug.x86_32.dll differ diff --git a/addons/godotsteam/win32/libgodotsteam.windows.template_release.x86_32.dll b/addons/godotsteam/win32/libgodotsteam.windows.template_release.x86_32.dll new file mode 100644 index 0000000..739f257 Binary files /dev/null and b/addons/godotsteam/win32/libgodotsteam.windows.template_release.x86_32.dll differ diff --git a/addons/godotsteam/win32/steam_api.dll b/addons/godotsteam/win32/steam_api.dll new file mode 100644 index 0000000..5869b19 Binary files /dev/null and b/addons/godotsteam/win32/steam_api.dll differ diff --git a/addons/godotsteam/win64/libgodotsteam.windows.template_debug.x86_64.dll b/addons/godotsteam/win64/libgodotsteam.windows.template_debug.x86_64.dll new file mode 100644 index 0000000..1e8bc23 Binary files /dev/null and b/addons/godotsteam/win64/libgodotsteam.windows.template_debug.x86_64.dll differ diff --git a/addons/godotsteam/win64/libgodotsteam.windows.template_release.x86_64.dll b/addons/godotsteam/win64/libgodotsteam.windows.template_release.x86_64.dll new file mode 100644 index 0000000..6855333 Binary files /dev/null and b/addons/godotsteam/win64/libgodotsteam.windows.template_release.x86_64.dll differ diff --git a/addons/godotsteam/win64/steam_api64.dll b/addons/godotsteam/win64/steam_api64.dll new file mode 100644 index 0000000..43e7291 Binary files /dev/null and b/addons/godotsteam/win64/steam_api64.dll differ diff --git a/addons/godotsteam/win64/~libgodotsteam.windows.template_debug.x86_64.dll b/addons/godotsteam/win64/~libgodotsteam.windows.template_debug.x86_64.dll new file mode 100644 index 0000000..1e8bc23 Binary files /dev/null and b/addons/godotsteam/win64/~libgodotsteam.windows.template_debug.x86_64.dll differ diff --git a/addons/godotsteam/win64/~libgodotsteam.windows.template_debug.x86_64.dll~RFbc46e11.TMP b/addons/godotsteam/win64/~libgodotsteam.windows.template_debug.x86_64.dll~RFbc46e11.TMP new file mode 100644 index 0000000..1e8bc23 Binary files /dev/null and b/addons/godotsteam/win64/~libgodotsteam.windows.template_debug.x86_64.dll~RFbc46e11.TMP differ diff --git a/assets/data/version.json b/assets/data/version.json index 57a590a..4c8ba51 100644 --- a/assets/data/version.json +++ b/assets/data/version.json @@ -1,16 +1,39 @@ { - "latest_version": "2.1.8", + "latest_version": "2.1.9", "minimum_app_version": "2.1.0", "releases": [ { - "version": "2.1.8", - "date": "2026-04-28", + "version": "2.1.9", + "date": "2026-04-29", "pck_url": "https://raw.githubusercontent.com/adtpdn/tekton-updates/main/latest/patch.pck", "pck_size": 0, "changelog": [ - "Fixed Tekton rotation and tile spawning animation timing", - "Implemented 1-tile perimeter buffer for all spawn areas to prevent stuck gaps", - "Tekton NPCs now spawn in corner zones; players spawn in middle zones", + "Added friend system with friend list, direct messaging, and lobby invitations", + "Improved Steam login support", + "Added Social Panel with Friends, Global Chat, and Direct Message tabs", + "Added ability to add/remove friends from lobby player slots", + "Added Invite Friends button to lobby", + "Added lobby invite notifications", + "Fixed account type display in profile panel" + ] + }, + { + "version": "2.1.8", + "date": "2026-04-24", + "pck_url": "https://raw.githubusercontent.com/adtpdn/tekton-updates/main/latest/patch.pck", + "pck_size": 0, + "changelog": [ + "Optimized network synchronization with deterministic tile slots and granular board sync", + "Removed lag-sensitive server adjacency checks to fix \"disappearing tiles\" on high-latency connections", + "Fixed \"No multiplayer peer assigned\" crash during host disconnection and cleanup", + "Finalized AP system removal, transitioning to a fully real-time authoritative model", + "Restored bot mission completion logic", + "Stabilized Nakama socket cleanup during match-to-lobby transitions", + "Fixed Tekton not rotating toward spawning direction when throwing tiles", + "Fixed purple powerup tiles spawning on top of existing layer 1 tiles", + "Added animation delay to tile spawning to match Tekton throw animation timing", + "Implemented 1-tile perimeter buffer on all spawn areas to prevent edge spawning and stuck gaps", + "Tekton NPCs now spawn exclusively in corner zones; players spawn in middle zones", "Fixed outline shader being lost when applying skin material overrides", "Added Unstuck button to pause menu to teleport stuck players to a safe area" ] diff --git a/docs/STEAMWORKS_SETUP.md b/docs/STEAMWORKS_SETUP.md new file mode 100644 index 0000000..4d2cfc7 --- /dev/null +++ b/docs/STEAMWORKS_SETUP.md @@ -0,0 +1,422 @@ +# Steamworks Setup Guide for Tekton Armageddon + +This guide explains how to set up Steamworks for Windows, Mac, and Linux builds while using Nakama for mobile platforms (Android/iOS) for leaderboards, achievements, and shop functionality. + +## Overview + +- **Desktop (Windows/Mac/Linux)**: Single build that detects Steam at runtime + - If launched through Steam: Steam login available for Nakama registration; all features use Nakama + - If launched standalone: Uses Nakama for all features +- **Mobile (Android/iOS)**: Uses Nakama for all backend services +- **Unified Backend**: All platforms use Nakama for achievements, leaderboards, and shop +- **Steam Integration**: Steam is only used for authentication (auth session ticket for Nakama login) + +## Prerequisites + +### Steamworks Setup + +1. **Steam Partner Account** + - You need a Steam Partner account to access Steamworks + - Apply at: https://partner.steamgames.com/ + +2. **Steam App ID** + - Create a new app in Steamworks + - Note your App ID (e.g., 480) + - Update `steam_app_id` in `scripts/services/steamworks_manager.gd` + +3. **Steamworks SDK** + - Download the Steamworks SDK from Steamworks + - The GodotSteam plugin includes the SDK, but you may need it for reference + +## Installation Steps + +### 1. Install GodotSteam GDExtension + +**Option A: Via Godot Asset Library (Recommended)** +1. Open Godot Editor +2. Go to `Project > Asset Library` +3. Search for "GodotSteam GDExtension 4.4+" +4. Download and install the plugin +5. Restart Godot Editor + +**Option B: Manual Installation** +1. Download from: https://codeberg.org/godotsteam/godotsteam/releases +2. Download version 4.18.1 (compatible with Godot 4.6.2) +3. Extract to `addons/godotsteam/` in your project +4. Enable the plugin in `Project > Project Settings > Plugins` + +### 2. Configure Steamworks in Project + +1. **Enable the Plugin** + - Go to `Project > Project Settings > Plugins` + - Enable "GodotSteam" + +2. **Set Steam App ID** + - Edit `scripts/services/steamworks_manager.gd` + - Change `steam_app_id` to your Steam App ID: + ```gdscript + var steam_app_id: int = YOUR_APP_ID_HERE + ``` + +3. **Add BackendService as Autoload** + - Go to `Project > Project Settings > Autoload` + - Add `BackendService` with path: `res://scripts/services/backend_service.gd` + - Enable it as a singleton + +### 3. Configure Steamworks Features + +#### Achievements + +1. **Define Achievements in Steamworks** + - Go to Steamworks > Your App > Achievements + - Create achievements with API names (e.g., "first_win", "level_10") + - Set display names, descriptions, and icons + +2. **Use in Code** + ```gdscript + # Unlock an achievement + BackendService.unlock_achievement("first_win") + + # Set progress (for progress-based achievements) + BackendService.set_achievement_progress("kill_100_enemies", current_kills, 100) + + # Check achievement status + var progress = BackendService.get_achievement_progress("first_win") + ``` + +#### Leaderboards + +1. **Define Leaderboards in Steamworks** + - Go to Steamworks > Your App > Leaderboards + - Create leaderboards with API names (e.g., "high_score", "fastest_time") + - Set sort order (ascending/descending) and display type + +2. **Use in Code** + ```gdscript + # Submit a score + BackendService.submit_leaderboard_score("high_score", 1000) + + # Get leaderboard entries + BackendService.leaderboard_entries_loaded.connect(_on_leaderboard_loaded) + BackendService.get_leaderboard_entries("high_score", 1, 10) + + func _on_leaderboard_loaded(leaderboard_id: String, entries: Array): + for entry in entries: + print("Player: %s, Score: %d" % [entry.player_name, entry.score]) + ``` + +#### Shop (Steam Inventory) + +**Note**: Steam shop functionality requires additional setup with Steam Inventory Service or Steam Microtransactions. This is a complex feature that requires: + +1. **Steam Inventory Service Setup** + - Define items in Steamworks > Your App > Inventory + - Set item types, prices, and properties + - Implement purchase callbacks in `steamworks_manager.gd` + +2. **Alternative**: Use external payment processor for desktop and sync with Nakama + +### 4. Export Presets + +The project includes export presets for all platforms: + +#### Desktop Builds (single build for Steam and standalone) +- **Windows Desktop** (preset.0) → `build/tekton_armageddon_v2.1.7.exe` +- **macOS** (preset.2) → `build/tekton_armageddon_v2.1.7.zip` +- **Linux/X11** (preset.3) → `build/tekton_armageddon_v2.1.7.x86_64` + +#### Mobile Builds +- **Android** (preset.1) → `build/tekton-dash-armageddon-v.2.1.5.apk` + +**Note**: Desktop builds are universal - the same executable works on both Steam and standalone. The game detects whether it's running through Steam at runtime and switches backends accordingly. + +#### Configure macOS Export + +1. **Code Signing** (for distribution) + - Get an Apple Developer certificate + - Update `codesign/identity` in export preset + - Set `codesign/enable` to `true` + +2. **Architecture** + - Currently set to "universal" (Intel + Apple Silicon) + - Can be changed to "x86_64" or "arm64" if needed + +#### Configure Linux Export + +1. **Architecture** + - Currently set to "x86_64" + - Add ARM64 preset if needed for Linux ARM devices + +### 5. Platform Detection + +The `BackendService` automatically detects the platform and backend: + +```gdscript +# Detection logic in BackendService +if OS.has_feature("android") or OS.has_feature("ios"): + # Mobile → Nakama +elif OS.has_feature("steam"): + # Desktop → Steamworks +else: + # Desktop → Local storage (non-Steam builds) +``` + +You can check the current platform: +```gdscript +print("Platform: %s" % BackendService.get_platform_name()) +print("Initialized: %s" % BackendService.is_initialized()) +``` + +#### Platform Types + +- **DESKTOP_STEAM**: Running through Steam client (Steam login available, all features use Nakama) +- **DESKTOP_NAKAMA**: Desktop build not running through Steam (uses Nakama) +- **MOBILE_NAKAMA**: Android/iOS (uses Nakama) + +#### Runtime Detection + +The game automatically detects the launch method: +- If `OS.has_feature("steam")` is true → Steam login available, all features use Nakama +- Otherwise → All features use Nakama + +This means the same desktop build can be: +- Uploaded to Steam (Steam login enabled, all data stored in Nakama) +- Distributed standalone (all data stored in Nakama) + +### Steam Login for Nakama + +When running through Steam, players can use their Steam account to register or log in to Nakama. This is the **only** Steam integration - all game features (achievements, leaderboards, shop) use Nakama. + +**How it works:** +1. Player clicks "Sign in with Steam" button on login screen +2. Game retrieves Steam auth session ticket via Steamworks +3. Auth ticket is sent to Nakama for authentication +4. Nakama validates the ticket with Steam backend +5. If valid, player is logged in/registered to Nakama +6. Player's Steam account is linked to their Nakama account + +**Benefits:** +- No password needed for Steam users +- Automatic account creation on first login +- Seamless cross-platform progression (all data in Nakama) +- Steam username is used as display name +- Unified backend across all platforms + +**Requirements:** +- Nakama server must be configured with Steam API key +- Steamworks must be initialized (game launched through Steam) +- GodotSteam plugin must support `getAuthSessionTicket()` + +**Configuration:** +Set your Steam API key in Nakama server configuration: +```yaml +nakama: + social: + steam: + api_key: "your_steam_api_key" +``` + +## Nakama Integration for Mobile + +### Current Setup + +Your project already has Nakama integrated via `addons/com.heroiclabs.nakama/` and `NakamaManager` autoload. + +### Connecting to BackendService + +The `BackendService` will automatically use Nakama on mobile. You need to implement Nakama-specific methods in `NakamaManager`: + +```gdscript +# In NakamaManager.gd, add these signals: +signal achievement_unlocked(achievement_id: String) +signal leaderboard_score_submitted(leaderboard_id: String, score: int, success: bool) + +# Implement achievement methods +func unlock_achievement(achievement_id: String): + # Use Nakama's achievement system + var achievement = await client.write_storage_object_async( + session, + NakamaWriteStorageObject.new( + "achievements", + achievement_id, + {"unlocked": true, "timestamp": Time.get_unix_time_from_system()} + ) + ) + achievement_unlocked.emit(achievement_id) + +# Implement leaderboard methods +func submit_leaderboard_score(leaderboard_id: String, score: int): + # Use Nakama's leaderboard system + var result = await client.write_leaderboard_record_async( + session, + leaderboard_id, + score + ) + leaderboard_score_submitted.emit(leaderboard_id, score, result != null) +``` + +## Testing + +### Testing Steam Builds + +1. **Export Steam Build** + - Use the "Windows Desktop (Steam)" preset (preset.0) + - Export to `build/steam/tekton_armageddon_v2.1.7.exe` + +2. **Upload to Steam** + - Upload the exported build to Steamworks + - Set as the default build for your app + +3. **Run through Steam** + - Launch the game via Steam (not directly) + - Steam must be running + - Check console for "SteamworksManager: Steam initialized successfully" + +4. **Test Achievements** + - Call `BackendService.unlock_achievement("test_achievement")` + - Check Steam overlay (Shift+Tab) to see achievement unlock + +5. **Test Leaderboards** + - Submit scores via `BackendService.submit_leaderboard_score()` + - View in Steamworks backend or Steam overlay + +### Testing Non-Steam Builds + +1. **Export Non-Steam Build** + - Use the "Windows Desktop (Non-Steam)" preset (preset.1) + - Export to `build/standalone/tekton_armageddon_v2.1.7.exe` + +2. **Run Directly** + - Run the executable directly (not through Steam) + - Check console for "BackendService: Initialized Nakama backend" + +3. **Test Leaderboards** + - Ensure Nakama server is accessible + - Open the leaderboard panel to fetch rankings + - Submit scores via `UserProfileManager.submit_to_leaderboard()` + - Leaderboards are global (same as mobile) + +4. **Test Shop** + - Ensure Nakama server is accessible + - Open the shop panel to fetch catalog + - Purchase items via `UserProfileManager.purchase_item()` + - Shop functionality works the same as mobile + +### Testing Nakama (Mobile) + +1. **Run on Mobile Device** + - Export to Android/iOS using the Android preset (preset.2) + - The game will automatically use Nakama backend + - Check logs for "BackendService: Initialized Nakama backend" + +2. **Test in Editor** + - To test Nakama in editor, temporarily modify `_detect_platform()`: + ```gdscript + func _detect_platform() -> void: + current_platform = Platform.MOBILE_NAKAMA # Force Nakama + ``` + +## Troubleshooting + +### Steamworks Not Initializing + +**Problem**: "SteamworksManager: Failed to initialize Steam" + +**Solutions**: +1. Ensure game is launched through Steam (not directly) +2. Check Steam is running +3. Verify `steam_app_id` is correct +4. Check GodotSteam plugin is enabled in Project Settings +5. Restart Godot Editor after installing plugin + +### Achievements Not Unlocking + +**Problem**: Achievements don't appear in Steam overlay + +**Solutions**: +1. Ensure achievement API names match Steamworks configuration +2. Check `steam.storeStats()` is called after setting achievements +3. Verify achievement is published in Steamworks (not in draft) +4. Test with Steam overlay open (Shift+Tab) + +### Leaderboards Not Working + +**Problem**: Leaderboard scores not submitting + +**Solutions**: +1. Ensure leaderboard exists in Steamworks +2. Check leaderboard API name matches +3. Verify leaderboard is published +4. Check console for error messages + +### Platform Detection Issues + +**Problem**: Wrong backend being used + +**Solutions**: +1. Check OS features: `print(OS.get_supported_features())` +2. Manually override platform in `_detect_platform()` for testing +3. Ensure `BackendService` is added as autoload + +## File Structure + +``` +scripts/services/ +├── backend_service.gd # Unified interface (autoload) +└── steamworks_manager.gd # Steamworks implementation + +export_presets.cfg # Export presets for all platforms +docs/STEAMWORKS_SETUP.md # This documentation +``` + +## Additional Resources + +- **GodotSteam Documentation**: https://godotsteam.com/ +- **GodotSteam GitHub**: https://codeberg.org/godotsteam/godotsteam +- **Steamworks Documentation**: https://partner.steamgames.com/doc/home +- **Nakama Documentation**: https://heroiclabs.com/docs/nakama/ + +## Next Steps + +1. Complete Steam Partner account setup +2. Create Steam app and get App ID +3. Install GodotSteam plugin +4. Configure achievements and leaderboards in Steamworks +5. Implement Nakama methods in `NakamaManager` for mobile +6. Test on all target platforms +7. Set up code signing for macOS distribution +8. Configure Steam Inventory Service if using in-game shop + +## Notes + +- **Steam builds** only work when launched through Steam client +- **Non-Steam builds** use Nakama for leaderboards and shop (same as mobile) +- **Shop functionality** is available on both Steam (via Steam Inventory) and non-Steam (via Nakama) +- **Non-Steam builds** sync to Nakama server, not Steam +- **Nakama** is already integrated for multiplayer, leaderboards, and shop +- Export presets are organized in separate folders: `build/steam/` and `build/standalone/` + +## Build Workflow + +### For Steam Distribution + +1. Export using Steam presets (preset.0, preset.3, preset.5) +2. Upload builds to Steamworks +3. Configure achievements and leaderboards in Steamworks backend +4. Set build as default in Steamworks +5. Shop uses Steam Inventory Service (requires additional setup) + +### For Standalone Distribution (itch.io, GOG, etc.) + +1. Export using Non-Steam presets (preset.1, preset.4, preset.6) +2. Distribute the standalone executables +3. Ensure Nakama server is accessible to players +4. Players get global leaderboards and shop via Nakama +5. No Steam integration required + +### For Mobile Stores + +1. Export using Android preset (preset.2) +2. Upload to Google Play / App Store +3. Ensure Nakama server is configured and accessible +4. Leaderboards and shop work the same as non-Steam desktop diff --git a/export_presets.cfg b/export_presets.cfg index 337316d..d3e8c25 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -8,7 +8,7 @@ custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" -export_path="build/tekton_armageddon_v2.1.7.exe" +export_path="build/tekton_armageddon_v2.1.8.exe" patches=PackedStringArray() patch_delta_encoding=false patch_delta_compression_level_zstd=19 @@ -70,6 +70,7 @@ ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debu Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue Remove-Item -Recurse -Force '{temp_dir}'" + [preset.1] name="Android" @@ -80,7 +81,7 @@ custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" -export_path="build/tekton-dash-armageddon-v.2.1.5.apk" +export_path="build/tekton-dash-armageddon-v.2.1.8.apk" patches=PackedStringArray() patch_delta_encoding=false patch_delta_compression_level_zstd=19 @@ -295,3 +296,115 @@ permissions/write_sms=false permissions/write_social_stream=false permissions/write_sync_settings=false permissions/write_user_dictionary=false + +[preset.2] + +name="macOS" +platform="macOS" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="build/tekton_armageddon_v2.1.8.zip" +patches=PackedStringArray() +patch_delta_encoding=false +patch_delta_compression_level_zstd=19 +patch_delta_min_reduction=0.1 +patch_delta_include_filters="*" +patch_delta_exclude_filters="" +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.2.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=0 +binary_format/embed_pck=true +texture_format/s3tc_bptc=true +texture_format/etc2_astc=false +shader_baker/enabled=true +binary_format/architecture="universal" +codesign/enable=false +codesign/timestamp=true +codesign/timestamp_server_url="" +codesign/digest_algorithm=1 +codesign/identity="" +codesign/identity_type=0 +codesign/custom_options=PackedStringArray() +application/modify_resources=false +application/icon="" +application/console_wrapper_icon="" +application/icon_interpolation=4 +application/file_version="2.1" +application/product_version="2.1" +application/company_name="DanchieGo" +application/product_name="Tekton Armageddon" +application/file_description="" +application/copyright="" +application/trademarks="" +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="#!/bin/bash +unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\" +\"{temp_dir}/{exe_name}\" {cmd_args}" +ssh_remote_deploy/cleanup_script="#!/bin/bash +kill $(pgrep -x \"{exe_name}\") +rm -rf \"{temp_dir}\"" + + +[preset.3] + +name="Linux/X11" +platform="Linux/X11" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="build/tekton_armageddon_v2.1.8.x86_64" +patches=PackedStringArray() +patch_delta_encoding=false +patch_delta_compression_level_zstd=19 +patch_delta_min_reduction=0.1 +patch_delta_include_filters="*" +patch_delta_exclude_filters="" +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.3.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=0 +binary_format/embed_pck=true +texture_format/s3tc_bptc=true +texture_format/etc2_astc=false +shader_baker/enabled=true +binary_format/architecture="x86_64" +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="#!/bin/bash +unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\" +\"{temp_dir}/{exe_name}\" {cmd_args}" +ssh_remote_deploy/cleanup_script="#!/bin/bash +kill $(pgrep -x \"{exe_name}\") +rm -rf \"{temp_dir}\"" + diff --git a/project.godot b/project.godot index ba92021..4d78a10 100644 --- a/project.godot +++ b/project.godot @@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true [application] config/name="Tekton Dash Armageddon" -config/version="2.1.7" +config/version="2.1.8" run/main_scene="res://scenes/ui/boot_screen.tscn" config/features=PackedStringArray("4.6", "Forward Plus") config/icon="res://icon.svg" @@ -41,6 +41,8 @@ MusicManager="*res://scripts/managers/music_manager.gd" AdminManager="*res://scripts/managers/admin_manager.gd" SkinManager="*res://scripts/managers/skin_manager.gd" GachaManager="*res://scripts/managers/gacha_manager.gd" +BackendService="*res://scripts/services/backend_service.gd" +FriendManager="*res://scripts/managers/friend_manager.gd" [display] @@ -50,7 +52,7 @@ window/stretch/mode="viewport" [editor_plugins] -enabled=PackedStringArray("res://addons/enhanced_gridmap/plugin.cfg", "res://addons/com.heroiclabs.nakama/plugin.cfg") +enabled=PackedStringArray("res://addons/com.heroiclabs.nakama/plugin.cfg", "res://addons/enhanced_gridmap/plugin.cfg", "res://addons/godotsteam/plugin.cfg") [file_customization] @@ -144,3 +146,10 @@ action_knock_tekton={ [rendering] textures/vram_compression/import_etc2_astc=true + +[steam] + +initialization/app_id=480 +initialization/initialize_on_startup=false +initialization/embed_callbacks=false +multiplayer_peer/max_channels=4 diff --git a/scenes/lobby.gd b/scenes/lobby.gd index bfe615e..5af709b 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -93,8 +93,16 @@ var doors_goals_option: OptionButton @onready var leave_btn = $LobbyPanel/BottomBar/LeaveBtn @onready var ready_btn = $LobbyPanel/BottomBar/ReadyBtn @onready var start_game_btn = $LobbyPanel/BottomBar/StartGameBtn +var invite_btn: Button @onready var status_label = $LobbyPanel/StatusLabel +# Social Panel instance +var social_panel_instance: Control + +# Lobby invite popup +var _invite_popup: AcceptDialog +var _pending_invite_match_id: String = "" + # UI References - Status @onready var connection_status = $StatusBar/ConnectionStatus @@ -175,6 +183,11 @@ func _ready(): if quit_btn: quit_btn.pressed.connect(_on_quit_pressed) + # Social button (main menu) - connect if node exists + var social_btn = get_node_or_null("%SocialBtn") + if social_btn: + social_btn.pressed.connect(_on_social_pressed) + # Connect Server Selection signals if server_option: server_option.item_selected.connect(_on_server_option_selected) @@ -258,6 +271,12 @@ func _ready(): if chat_input: chat_input.text_submitted.connect(func(_t): _on_chat_send_pressed()) + # Connect Social / Friend UI + invite_btn = get_node_or_null("LobbyPanel/BottomBar/InviteBtn") + if invite_btn: + invite_btn.pressed.connect(_on_invite_friends_pressed) + FriendManager.lobby_invite_received.connect(_on_lobby_invite_received) + # Set initial title if already loaded _on_profile_updated() @@ -1075,6 +1094,24 @@ func _update_player_slots() -> void: ready_label.text = "READY ✓" if is_ready else "NOT READY" ready_label.add_theme_color_override("font_color", Color(0.4, 0.8, 0.4) if is_ready else Color(0.6, 0.6, 0.6)) + + # + Friend button — node must exist in lobby.tscn as "AddFriendBtn1", "AddFriendBtn2"... + var player_nakama_id: String = player.get("nakama_id", "") + var my_nakama_id: String = NakamaManager.session.user_id if NakamaManager.session else "" + var add_friend_btn: Button = slot.get_node_or_null("AddFriendBtn%d" % slot_num) + if add_friend_btn: + if player_nakama_id.is_empty() or player_nakama_id == my_nakama_id: + add_friend_btn.visible = false + else: + add_friend_btn.visible = true + if FriendManager.is_friend(player_nakama_id): + add_friend_btn.text = "Friend ✓" + add_friend_btn.disabled = true + else: + add_friend_btn.text = "+ Friend" + add_friend_btn.disabled = false + if not add_friend_btn.pressed.is_connected(func(): _on_add_friend_pressed(player_nakama_id)): + add_friend_btn.pressed.connect(func(): _on_add_friend_pressed(player_nakama_id)) else: # Empty slot - show as bot placeholder slot.visible = true @@ -1112,6 +1149,11 @@ func _update_player_slots() -> void: if ready_label: ready_label.text = "WAITING..." ready_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.7)) + + # Hide friend button for bots/empty slots + var add_friend_btn: Button = slot.get_node_or_null("AddFriendBtn%d" % slot_num) + if add_friend_btn: + add_friend_btn.visible = false _update_status() @@ -1158,6 +1200,85 @@ func _apply_loadout_character() -> void: print("[Lobby] Loadout character applied: ", saved_char) +# ============================================================================= +# Social / Friend Functions +# ============================================================================= + +func _on_add_friend_pressed(nakama_id: String) -> void: + var ok = await FriendManager.add_friend_by_id(nakama_id) + if ok: + _update_player_slots() + +func _on_invite_friends_pressed() -> void: + """Open the invite friends dialog (scene-based).""" + var match_id = current_match_id + if match_id.is_empty(): + return + var friends = FriendManager.get_mutual_friends() + var scene = load("res://scenes/ui/invite_friends_dialog.tscn") as PackedScene + if not scene: + return + var dialog = scene.instantiate() + add_child(dialog) + dialog.open(friends, match_id) + dialog.closed.connect(dialog.queue_free) + +func _on_lobby_invite_received(from_user_id: String, from_name: String, match_id: String) -> void: + """Show invite notification popup. Only shown if not already in a game.""" + if get_tree().current_scene.scene_file_path != "res://scenes/lobby.tscn": + return + if lobby_panel and lobby_panel.visible: + return + + if _invite_popup: + _invite_popup.queue_free() + + _pending_invite_match_id = match_id + var scene = load("res://scenes/ui/lobby_invite_popup.tscn") as PackedScene + if scene: + _invite_popup = scene.instantiate() + add_child(_invite_popup) + _invite_popup.setup(from_name) + _invite_popup.accepted.connect(_on_invite_accepted) + _invite_popup.declined.connect(func(): _invite_popup.queue_free()) + _invite_popup.popup_centered() + else: + # Fallback if scene not yet added to project + var dlg := AcceptDialog.new() + dlg.title = "Lobby Invitation" + dlg.dialog_text = "%s invited you!\nJoin?" % from_name + dlg.ok_button_text = "Join" + dlg.add_cancel_button("Decline") + add_child(dlg) + dlg.confirmed.connect(_on_invite_accepted) + dlg.canceled.connect(dlg.queue_free) + dlg.popup_centered() + _invite_popup = dlg + +func _on_invite_accepted() -> void: + if not _pending_invite_match_id.is_empty(): + LobbyManager.join_room(_pending_invite_match_id) + if _invite_popup: + _invite_popup.queue_free() + _pending_invite_match_id = "" + +func _on_social_pressed() -> void: + """Open social / friend list panel.""" + if not social_panel_instance: + var scene = load("res://scenes/ui/social_panel.tscn") + if scene: + social_panel_instance = scene.instantiate() + social_panel_instance.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + add_child(social_panel_instance) + if social_panel_instance.has_signal("closed"): + social_panel_instance.closed.connect(func(): + social_panel_instance.hide() + if main_menu_panel: main_menu_panel.show() + ) + if social_panel_instance: + if main_menu_panel: main_menu_panel.hide() + social_panel_instance.show() + # ============================================================================= # Global Chat System # ============================================================================= diff --git a/scenes/lobby.tscn b/scenes/lobby.tscn index 28a1d55..a46e8aa 100644 --- a/scenes/lobby.tscn +++ b/scenes/lobby.tscn @@ -403,7 +403,6 @@ unique_name_in_owner = true custom_minimum_size = Vector2(44, 44) layout_mode = 2 theme_override_fonts/font = ExtResource("5_pc087") -disabled = true text = "SOCIAL" [node name="SettingsBtn" type="Button" parent="MainMenuPanel/MainMargin/MainHBox/RightCol/TopRightPanel" unique_id=90] @@ -967,6 +966,13 @@ theme_override_font_sizes/font_size = 14 text = "NOT READY" horizontal_alignment = 1 +[node name="AddFriendBtn1" type="Button" parent="LobbyPanel/PlayersContainer/PlayerSlot1" unique_id=10000001] +custom_minimum_size = Vector2(0, 32) +layout_mode = 2 +theme_override_fonts/font = ExtResource("5_pc087") +theme_override_font_sizes/font_size = 12 +text = "+ Friend" + [node name="PlayerSlot2" type="VBoxContainer" parent="LobbyPanel/PlayersContainer" unique_id=1012954493] custom_minimum_size = Vector2(180, 0) layout_mode = 2 @@ -1031,6 +1037,13 @@ theme_override_font_sizes/font_size = 14 text = "NOT READY" horizontal_alignment = 1 +[node name="AddFriendBtn2" type="Button" parent="LobbyPanel/PlayersContainer/PlayerSlot2" unique_id=10000002] +custom_minimum_size = Vector2(0, 32) +layout_mode = 2 +theme_override_fonts/font = ExtResource("5_pc087") +theme_override_font_sizes/font_size = 12 +text = "+ Friend" + [node name="PlayerSlot3" type="VBoxContainer" parent="LobbyPanel/PlayersContainer" unique_id=1033758492] custom_minimum_size = Vector2(180, 0) layout_mode = 2 @@ -1095,6 +1108,13 @@ theme_override_font_sizes/font_size = 14 text = "NOT READY" horizontal_alignment = 1 +[node name="AddFriendBtn3" type="Button" parent="LobbyPanel/PlayersContainer/PlayerSlot3" unique_id=10000003] +custom_minimum_size = Vector2(0, 32) +layout_mode = 2 +theme_override_fonts/font = ExtResource("5_pc087") +theme_override_font_sizes/font_size = 12 +text = "+ Friend" + [node name="PlayerSlot4" type="VBoxContainer" parent="LobbyPanel/PlayersContainer" unique_id=62963750] custom_minimum_size = Vector2(180, 0) layout_mode = 2 @@ -1159,6 +1179,13 @@ theme_override_font_sizes/font_size = 14 text = "NOT READY" horizontal_alignment = 1 +[node name="AddFriendBtn4" type="Button" parent="LobbyPanel/PlayersContainer/PlayerSlot4" unique_id=10000004] +custom_minimum_size = Vector2(0, 32) +layout_mode = 2 +theme_override_fonts/font = ExtResource("5_pc087") +theme_override_font_sizes/font_size = 12 +text = "+ Friend" + [node name="PlayersContainer2" type="HBoxContainer" parent="LobbyPanel" unique_id=76585227] layout_mode = 1 anchors_preset = 3 @@ -1237,6 +1264,13 @@ theme_override_font_sizes/font_size = 14 text = "NOT READY" horizontal_alignment = 1 +[node name="AddFriendBtn5" type="Button" parent="LobbyPanel/PlayersContainer2/PlayerSlot5" unique_id=10000006] +custom_minimum_size = Vector2(0, 32) +layout_mode = 2 +theme_override_fonts/font = ExtResource("5_pc087") +theme_override_font_sizes/font_size = 12 +text = "+ Friend" + [node name="PlayerSlot6" type="VBoxContainer" parent="LobbyPanel/PlayersContainer2" unique_id=1038068961] custom_minimum_size = Vector2(180, 0) layout_mode = 2 @@ -1301,6 +1335,13 @@ theme_override_font_sizes/font_size = 14 text = "NOT READY" horizontal_alignment = 1 +[node name="AddFriendBtn6" type="Button" parent="LobbyPanel/PlayersContainer2/PlayerSlot6" unique_id=10000007] +custom_minimum_size = Vector2(0, 32) +layout_mode = 2 +theme_override_fonts/font = ExtResource("5_pc087") +theme_override_font_sizes/font_size = 12 +text = "+ Friend" + [node name="PlayerSlot7" type="VBoxContainer" parent="LobbyPanel/PlayersContainer2" unique_id=1161417031] custom_minimum_size = Vector2(180, 0) layout_mode = 2 @@ -1365,6 +1406,13 @@ theme_override_font_sizes/font_size = 14 text = "NOT READY" horizontal_alignment = 1 +[node name="AddFriendBtn7" type="Button" parent="LobbyPanel/PlayersContainer2/PlayerSlot7" unique_id=10000008] +custom_minimum_size = Vector2(0, 32) +layout_mode = 2 +theme_override_fonts/font = ExtResource("5_pc087") +theme_override_font_sizes/font_size = 12 +text = "+ Friend" + [node name="PlayerSlot8" type="VBoxContainer" parent="LobbyPanel/PlayersContainer2" unique_id=2019344700] custom_minimum_size = Vector2(180, 0) layout_mode = 2 @@ -1429,6 +1477,13 @@ theme_override_font_sizes/font_size = 14 text = "NOT READY" horizontal_alignment = 1 +[node name="AddFriendBtn8" type="Button" parent="LobbyPanel/PlayersContainer2/PlayerSlot8" unique_id=10000009] +custom_minimum_size = Vector2(0, 32) +layout_mode = 2 +theme_override_fonts/font = ExtResource("5_pc087") +theme_override_font_sizes/font_size = 12 +text = "+ Friend" + [node name="AreaSelector" type="HBoxContainer" parent="LobbyPanel" unique_id=696178336] clip_contents = true layout_mode = 1 @@ -1498,6 +1553,13 @@ text = "LEAVE" layout_mode = 2 size_flags_horizontal = 3 +[node name="InviteBtn" type="Button" parent="LobbyPanel/BottomBar" unique_id=10000005] +custom_minimum_size = Vector2(120, 44) +layout_mode = 2 +theme_override_fonts/font = ExtResource("5_pc087") +theme_override_font_sizes/font_size = 18 +text = "INVITE FRIENDS" + [node name="ReadyBtn" type="Button" parent="LobbyPanel/BottomBar" unique_id=2026272478] custom_minimum_size = Vector2(140, 50) layout_mode = 2 diff --git a/scenes/ui/friend_row.tscn b/scenes/ui/friend_row.tscn new file mode 100644 index 0000000..7b433f7 --- /dev/null +++ b/scenes/ui/friend_row.tscn @@ -0,0 +1,40 @@ +[gd_scene format=3 uid="uid://dqfriend001"] + +[ext_resource type="Script" path="res://scripts/ui/friend_row.gd" id="1_frow"] + +[node name="FriendRow" type="HBoxContainer"] +script = ExtResource("1_frow") + +[node name="NameLabel" type="Label" parent="."] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="StateLabel" type="Label" parent="."] +unique_name_in_owner = true +layout_mode = 2 +visible = false + +[node name="DMBtn" type="Button" parent="."] +unique_name_in_owner = true +layout_mode = 2 +text = "Chat" +visible = false + +[node name="AcceptBtn" type="Button" parent="."] +unique_name_in_owner = true +layout_mode = 2 +text = "Accept" +visible = false + +[node name="DeclineBtn" type="Button" parent="."] +unique_name_in_owner = true +layout_mode = 2 +text = "Decline" +visible = false + +[node name="RemoveBtn" type="Button" parent="."] +unique_name_in_owner = true +layout_mode = 2 +text = "Remove" +visible = false diff --git a/scenes/ui/invite_friends_dialog.tscn b/scenes/ui/invite_friends_dialog.tscn new file mode 100644 index 0000000..7655acc --- /dev/null +++ b/scenes/ui/invite_friends_dialog.tscn @@ -0,0 +1,43 @@ +[gd_scene format=3 uid="uid://dqinvite001"] + +[ext_resource type="Script" path="res://scripts/ui/invite_friends_dialog.gd" id="1_inv"] + +[node name="InviteFriendsDialog" type="Window"] +title = "Invite Friends to Lobby" +size = Vector2i(360, 420) +script = ExtResource("1_inv") + +[node name="VBox" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 8.0 +offset_top = 8.0 +offset_right = -8.0 +offset_bottom = -8.0 + +[node name="TitleLabel" type="Label" parent="VBox"] +layout_mode = 2 +text = "Select a friend to invite:" + +[node name="FriendScroll" type="ScrollContainer" parent="VBox"] +layout_mode = 2 +size_flags_vertical = 3 +custom_minimum_size = Vector2(0, 300) + +[node name="FriendRows" type="VBoxContainer" parent="VBox/FriendScroll"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="NoFriendsLabel" type="Label" parent="VBox"] +unique_name_in_owner = true +layout_mode = 2 +text = "No friends to invite yet.\nAdd friends from the Social panel!" +visible = false + +[node name="CloseBtn" type="Button" parent="VBox"] +unique_name_in_owner = true +layout_mode = 2 +text = "Close" diff --git a/scenes/ui/invite_row.tscn b/scenes/ui/invite_row.tscn new file mode 100644 index 0000000..a0cf787 --- /dev/null +++ b/scenes/ui/invite_row.tscn @@ -0,0 +1,16 @@ +[gd_scene format=3 uid="uid://dqinvrow01"] + +[ext_resource type="Script" path="res://scripts/ui/invite_row.gd" id="1_irow"] + +[node name="InviteRow" type="HBoxContainer"] +script = ExtResource("1_irow") + +[node name="NameLabel" type="Label" parent="."] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="InviteBtn" type="Button" parent="."] +unique_name_in_owner = true +layout_mode = 2 +text = "Invite" diff --git a/scenes/ui/lobby_invite_popup.tscn b/scenes/ui/lobby_invite_popup.tscn new file mode 100644 index 0000000..62e948c --- /dev/null +++ b/scenes/ui/lobby_invite_popup.tscn @@ -0,0 +1,19 @@ +[gd_scene format=3 uid="uid://dqlobinv01"] + +[ext_resource type="Script" path="res://scripts/ui/lobby_invite_popup.gd" id="1_lip"] + +[node name="LobbyInvitePopup" type="ConfirmationDialog"] +title = "Lobby Invitation" +ok_button_text = "Join" +script = ExtResource("1_lip") + +[node name="VBox" type="VBoxContainer" parent="."] +layout_mode = 0 +offset_right = 280.0 +offset_bottom = 80.0 + +[node name="MessageLabel" type="Label" parent="VBox"] +unique_name_in_owner = true +layout_mode = 2 +text = "Someone invited you to their lobby!" +autowrap_mode = 3 diff --git a/scenes/ui/login_screen.tscn b/scenes/ui/login_screen.tscn index 0f6962b..b18518c 100644 --- a/scenes/ui/login_screen.tscn +++ b/scenes/ui/login_screen.tscn @@ -186,36 +186,43 @@ layout_mode = 2 text = "Sign In" [node name="SocialLabel" type="Label" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox" unique_id=840440943] -visible = false layout_mode = 2 theme_override_colors/font_color = Color(0.69, 0.529, 0.357, 1) text = "─────── or continue with ───────" horizontal_alignment = 1 [node name="SocialButtons" type="HBoxContainer" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox" unique_id=1899762021] -visible = false layout_mode = 2 theme_override_constants/separation = 12 alignment = 1 [node name="GoogleButton" type="Button" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox/SocialButtons" unique_id=196743645] unique_name_in_owner = true +visible = false custom_minimum_size = Vector2(100, 44) layout_mode = 2 text = "Google" [node name="AppleButton" type="Button" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox/SocialButtons" unique_id=1174582184] unique_name_in_owner = true +visible = false custom_minimum_size = Vector2(100, 44) layout_mode = 2 text = "Apple" [node name="FacebookButton" type="Button" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox/SocialButtons" unique_id=1252096120] unique_name_in_owner = true +visible = false custom_minimum_size = Vector2(100, 44) layout_mode = 2 text = "Facebook" +[node name="SteamButton" type="Button" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox/SocialButtons" unique_id=1024459784] +unique_name_in_owner = true +custom_minimum_size = Vector2(100, 44) +layout_mode = 2 +text = "Steam" + [node name="StatusLabel" type="Label" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox" unique_id=1679807364] unique_name_in_owner = true layout_mode = 2 diff --git a/scenes/ui/profile_panel.tscn b/scenes/ui/profile_panel.tscn index 13d2e7d..4a83735 100644 --- a/scenes/ui/profile_panel.tscn +++ b/scenes/ui/profile_panel.tscn @@ -318,6 +318,7 @@ text = "💎" [node name="FragTabBtn" type="Button" parent="MainMargin/MainHBox/CenterCol/CategoryTabs" unique_id=1985872465] unique_name_in_owner = true +visible = false custom_minimum_size = Vector2(56, 56) layout_mode = 2 theme_override_font_sizes/font_size = 28 diff --git a/scenes/ui/shop_panel.tscn b/scenes/ui/shop_panel.tscn index 4572a20..3f57796 100644 --- a/scenes/ui/shop_panel.tscn +++ b/scenes/ui/shop_panel.tscn @@ -141,6 +141,7 @@ text = "GLOVES" [node name="TabAccessory" type="Button" parent="MainMargin/MainVBox/TopBar/TabsHBox" unique_id=228390814] unique_name_in_owner = true +visible = false custom_minimum_size = Vector2(60, 50) layout_mode = 2 theme_override_fonts/font = ExtResource("3_udh10") diff --git a/scenes/ui/social_panel.tscn b/scenes/ui/social_panel.tscn new file mode 100644 index 0000000..c5a64e4 --- /dev/null +++ b/scenes/ui/social_panel.tscn @@ -0,0 +1,179 @@ +[gd_scene load_steps=2 format=3 uid="uid://dqsocial001"] + +[ext_resource type="Script" path="res://scripts/ui/social_panel.gd" id="1_social"] + +[node name="SocialPanel" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("1_social") + +[node name="BgOverlay" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0, 0, 0, 0.6) +mouse_filter = 0 + +[node name="Panel" type="PanelContainer" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -240.0 +offset_top = -280.0 +offset_right = 240.0 +offset_bottom = 280.0 + +[node name="VBox" type="VBoxContainer" parent="Panel"] +layout_mode = 2 + +[node name="Header" type="HBoxContainer" parent="Panel/VBox"] +layout_mode = 2 + +[node name="TitleLabel" type="Label" parent="Panel/VBox/Header"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Social" + +[node name="CloseBtn" type="Button" parent="Panel/VBox/Header"] +unique_name_in_owner = true +layout_mode = 2 +text = "X" + +[node name="HSep" type="HSeparator" parent="Panel/VBox"] +layout_mode = 2 + +[node name="TabBar" type="HBoxContainer" parent="Panel/VBox"] +layout_mode = 2 + +[node name="FriendsTabBtn" type="Button" parent="Panel/VBox/TabBar"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Friends" + +[node name="GlobalTabBtn" type="Button" parent="Panel/VBox/TabBar"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Global Chat" + +[node name="DMTabBtn" type="Button" parent="Panel/VBox/TabBar"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "DM" +visible = false + +[node name="ContentStack" type="Control" parent="Panel/VBox"] +layout_mode = 2 +size_flags_vertical = 3 +custom_minimum_size = Vector2(0, 380) + +[node name="FriendsView" type="VBoxContainer" parent="Panel/VBox/ContentStack"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="AddFriendRow" type="HBoxContainer" parent="Panel/VBox/ContentStack/FriendsView"] +layout_mode = 2 + +[node name="AddFriendInput" type="LineEdit" parent="Panel/VBox/ContentStack/FriendsView/AddFriendRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Username or ID..." + +[node name="AddFriendBtn" type="Button" parent="Panel/VBox/ContentStack/FriendsView/AddFriendRow"] +unique_name_in_owner = true +layout_mode = 2 +text = "Add" + +[node name="FriendScroll" type="ScrollContainer" parent="Panel/VBox/ContentStack/FriendsView"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="FriendList" type="VBoxContainer" parent="Panel/VBox/ContentStack/FriendsView/FriendScroll"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="GlobalView" type="VBoxContainer" parent="Panel/VBox/ContentStack"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +visible = false + +[node name="GlobalLog" type="RichTextLabel" parent="Panel/VBox/ContentStack/GlobalView"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +bbcode_enabled = true +scroll_following = true + +[node name="GlobalInputRow" type="HBoxContainer" parent="Panel/VBox/ContentStack/GlobalView"] +layout_mode = 2 + +[node name="GlobalInput" type="LineEdit" parent="Panel/VBox/ContentStack/GlobalView/GlobalInputRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Type a message..." + +[node name="GlobalSendBtn" type="Button" parent="Panel/VBox/ContentStack/GlobalView/GlobalInputRow"] +unique_name_in_owner = true +layout_mode = 2 +text = "Send" + +[node name="DMView" type="VBoxContainer" parent="Panel/VBox/ContentStack"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +visible = false + +[node name="DMHeader" type="HBoxContainer" parent="Panel/VBox/ContentStack/DMView"] +layout_mode = 2 + +[node name="DMBackBtn" type="Button" parent="Panel/VBox/ContentStack/DMView/DMHeader"] +unique_name_in_owner = true +layout_mode = 2 +text = "<- Back" + +[node name="DMUsernameLabel" type="Label" parent="Panel/VBox/ContentStack/DMView/DMHeader"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="DMLog" type="RichTextLabel" parent="Panel/VBox/ContentStack/DMView"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +bbcode_enabled = true +scroll_following = true + +[node name="DMInputRow" type="HBoxContainer" parent="Panel/VBox/ContentStack/DMView"] +layout_mode = 2 + +[node name="DMInput" type="LineEdit" parent="Panel/VBox/ContentStack/DMView/DMInputRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Type a message..." + +[node name="DMSendBtn" type="Button" parent="Panel/VBox/ContentStack/DMView/DMInputRow"] +unique_name_in_owner = true +layout_mode = 2 +text = "Send" + diff --git a/scripts/managers/auth_manager.gd b/scripts/managers/auth_manager.gd index 7f4cd3a..ddc9307 100644 --- a/scripts/managers/auth_manager.gd +++ b/scripts/managers/auth_manager.gd @@ -10,7 +10,7 @@ signal session_restored signal logged_out # Auth modes -enum AuthMode {GUEST, EMAIL, GOOGLE, APPLE, FACEBOOK, CUSTOM} +enum AuthMode {GUEST, EMAIL, GOOGLE, APPLE, FACEBOOK, STEAM, CUSTOM} # User data var current_user: Dictionary = {} @@ -325,6 +325,74 @@ func login_with_facebook(access_token: String) -> bool: emit_signal("auth_completed", true, current_user) return true +func login_with_steam() -> bool: + emit_signal("auth_started") + auth_mode = AuthMode.STEAM + is_guest = false + + print("[AuthManager] Steam login...") + + # Get Steamworks manager from BackendService + var backend = get_node_or_null("/root/BackendService") + if not backend: + emit_signal("auth_failed", "BackendService not available") + return false + + var steamworks = backend.get_steamworks_manager() + if not steamworks or not steamworks.is_initialized(): + emit_signal("auth_failed", "Steamworks not available. Please launch through Steam.") + return false + + # Try full Steam ticket auth first, fall back to Steam ID custom auth for dev/testing + var session: NakamaSession = await _authenticate_steam_with_fallback(steamworks) + + if session == null or session.is_exception(): + var error: String = "Steam authentication failed" + if session != null: + error = session.get_exception().message + emit_signal("auth_failed", error) + return false + + NakamaManager.session = session + _save_session(session, AuthMode.STEAM) + + var socket_success := await _connect_socket() + if not socket_success: + emit_signal("auth_failed", "Failed to connect to game server") + return false + + await _load_user_profile() + is_authenticated = true + + emit_signal("auth_completed", true, current_user) + return true + +func _authenticate_steam_with_fallback(steamworks: Node) -> NakamaSession: + # Try proper Steam ticket auth first + var auth_ticket = steamworks.get_auth_session_ticket() + if not auth_ticket.is_empty(): + print("[AuthManager] Got Steam auth ticket, authenticating with Nakama...") + var session: NakamaSession = await NakamaManager.client.authenticate_steam_async(auth_ticket, null, true) + if not session.is_exception(): + return session + print("[AuthManager] Steam ticket auth failed: %s" % session.get_exception().message) + print("[AuthManager] Falling back to Steam ID custom auth (dev mode)...") + + # Fallback: use Steam ID + username to create an email-style account (works without publisher key) + var steam_id = str(steamworks.get_steam_user_id()) + var steam_name = steamworks.get_steam_user_name() + if steam_id == "0" or steam_id.is_empty(): + return null + + # Derive email and password from Steam credentials + var email = steam_name.to_lower().replace(" ", "_") + "@steam.local" + var password = steam_name # Default password = Steam username + var username = steam_name + + print("[AuthManager] Using Steam email auth: %s (%s)" % [email, username]) + var fallback_session: NakamaSession = await NakamaManager.client.authenticate_email_async(email, password, username, true) + return fallback_session + # ============================================================================= # Account Linking (Convert Guest to Full Account) # ============================================================================= diff --git a/scripts/managers/friend_manager.gd b/scripts/managers/friend_manager.gd new file mode 100644 index 0000000..aa7a3ce --- /dev/null +++ b/scripts/managers/friend_manager.gd @@ -0,0 +1,176 @@ +extends Node +## FriendManager - Nakama friend list, DM channels, and lobby invites +## Autoload: FriendManager + +signal friends_updated(friends: Array) +signal lobby_invite_received(from_user_id: String, from_name: String, match_id: String) +signal dm_message_received(from_user_id: String, from_name: String, message: String) + +## Notification codes (must match server-side RPC) +const NOTIF_LOBBY_INVITE := 1001 + +## Friend state codes from Nakama +const STATE_FRIEND := 0 +const STATE_INVITE_OUT := 1 +const STATE_INVITE_IN := 2 +const STATE_BLOCKED := 3 + +var friends: Array = [] # [{user_id, username, state}] +var _friend_ids: Dictionary = {} # user_id -> true (mutual friends only) +var _dm_channels: Dictionary = {} # user_id -> NakamaChannel + +func _ready() -> void: + NakamaManager.connected_to_nakama.connect(_on_nakama_connected) + if NakamaManager.socket and NakamaManager.socket.is_connected_to_host(): + _connect_socket_signals() + +func _on_nakama_connected() -> void: + _connect_socket_signals() + load_friends() + +func _connect_socket_signals() -> void: + var socket = NakamaManager.socket + if not socket: + return + if not socket.received_notification.is_connected(_on_notification_received): + socket.received_notification.connect(_on_notification_received) + if not socket.received_channel_message.is_connected(_on_channel_message): + socket.received_channel_message.connect(_on_channel_message) + +# ============================================================================= +# Friend List +# ============================================================================= + +func load_friends() -> void: + if not NakamaManager.session: + return + var result = await NakamaManager.client.list_friends_async(NakamaManager.session, 100, null, null) + if result.is_exception(): + push_warning("[FriendManager] Failed to load friends: " + result.get_exception().message) + return + friends.clear() + _friend_ids.clear() + for f in result.friends: + var u = f.user + var entry := { + "user_id": u.id, + "username": u.display_name if u.display_name != "" else u.username, + "state": f.state, + } + friends.append(entry) + if f.state == STATE_FRIEND: + _friend_ids[u.id] = true + emit_signal("friends_updated", friends) + +func get_mutual_friends() -> Array: + return friends.filter(func(f): return f.state == STATE_FRIEND) + +func is_friend(user_id: String) -> bool: + return _friend_ids.has(user_id) + +func add_friend_by_id(user_id: String) -> bool: + if not NakamaManager.session: + return false + var result = await NakamaManager.client.add_friends_async( + NakamaManager.session, PackedStringArray([user_id]), null) + if result.is_exception(): + push_warning("[FriendManager] add_friend failed: " + result.get_exception().message) + return false + load_friends() + return true + +func add_friend_by_username(username: String) -> bool: + if not NakamaManager.session: + return false + var result = await NakamaManager.client.add_friends_async( + NakamaManager.session, null, PackedStringArray([username])) + if result.is_exception(): + push_warning("[FriendManager] add_friend_by_username failed: " + result.get_exception().message) + return false + load_friends() + return true + +func remove_friend(user_id: String) -> bool: + if not NakamaManager.session: + return false + var result = await NakamaManager.client.delete_friends_async( + NakamaManager.session, PackedStringArray([user_id]), null) + if result.is_exception(): + return false + load_friends() + return true + +# ============================================================================= +# Lobby Invites +# ============================================================================= + +func send_lobby_invite(to_user_id: String, match_id: String) -> void: + if not NakamaManager.session: + return + var payload = JSON.stringify({"to_user_id": to_user_id, "match_id": match_id}) + var result = await NakamaManager.client.rpc_async( + NakamaManager.session, "send_lobby_invite", payload) + if result.is_exception(): + push_warning("[FriendManager] send_lobby_invite failed: " + result.get_exception().message) + +func _on_notification_received(notification) -> void: + if notification.code == NOTIF_LOBBY_INVITE: + var content = JSON.parse_string(notification.content) + if content: + var from_name: String = content.get("from_name", "Someone") + var match_id: String = content.get("match_id", "") + emit_signal("lobby_invite_received", notification.sender_id, from_name, match_id) + +# ============================================================================= +# Direct Messages +# ============================================================================= + +func open_dm(user_id: String) -> Object: + if _dm_channels.has(user_id): + return _dm_channels[user_id] + var socket = NakamaManager.socket + if not socket: + return null + var channel = await socket.join_chat_async( + user_id, NakamaSocket.ChannelType.DirectMessage, true, false) + if channel.is_exception(): + push_warning("[FriendManager] Failed to open DM with " + user_id) + return null + _dm_channels[user_id] = channel + return channel + +func send_dm(user_id: String, message: String) -> bool: + var channel = await open_dm(user_id) + if not channel: + return false + var socket = NakamaManager.socket + if not socket: + return false + var result = await socket.write_chat_message_async(channel.id, {"msg": message}) + return not result.is_exception() + +func get_dm_channel_id(user_id: String) -> String: + var ch = _dm_channels.get(user_id, null) + return ch.id if ch else "" + +func _on_channel_message(message) -> void: + # Route to DM signal if this message is from a DM channel + for user_id in _dm_channels: + var ch = _dm_channels[user_id] + if ch.id == message.channel_id: + var text: String = "" + var parsed = JSON.parse_string(message.content) + if typeof(parsed) == TYPE_DICTIONARY: + text = parsed.get("msg", message.content) + else: + text = message.content + emit_signal("dm_message_received", message.sender_id, message.username, text) + return + +func close_all_dm_channels() -> void: + var socket = NakamaManager.socket + for user_id in _dm_channels: + var ch = _dm_channels[user_id] + if socket: + socket.leave_chat_async(ch.id) + _dm_channels.clear() diff --git a/scripts/managers/friend_manager.gd.uid b/scripts/managers/friend_manager.gd.uid new file mode 100644 index 0000000..1930412 --- /dev/null +++ b/scripts/managers/friend_manager.gd.uid @@ -0,0 +1 @@ +uid://deq26r61rqq51 diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index 2cc8d71..d7d1a51 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -745,7 +745,8 @@ func _on_match_joined(match_id: String) -> void: "id": my_id, "name": local_player_name, "is_ready": false, - "character": available_characters[local_character_index] + "character": available_characters[local_character_index], + "nakama_id": NakamaManager.session.user_id if NakamaManager.session else "" } players_in_room.append(my_data) @@ -755,16 +756,17 @@ func _on_match_joined(match_id: String) -> void: # Client will request room info when peer connection is established @rpc("any_peer", "reliable") -func request_room_info(requester_id: int, requester_name: String, requester_character: String) -> void: - """Client requests room info from host, sending their name and character.""" +func request_room_info(requester_id: int, requester_name: String, requester_character: String, requester_nakama_id: String = "") -> void: + """Client requests room info from host, sending their name, character and nakama_id.""" if not multiplayer.is_server(): return - # Update the player's name and character in the list + # Update the player's name, character and nakama_id in the list for player in players_in_room: if player["id"] == requester_id: player["name"] = requester_name player["character"] = requester_character + player["nakama_id"] = requester_nakama_id break # Send room data to requester @@ -805,7 +807,8 @@ func _on_peer_connected(peer_id: int) -> void: "id": peer_id, "name": "Player %d" % peer_id, "is_ready": false, - "character": available_characters[0] + "character": available_characters[0], + "nakama_id": "" } players_in_room.append(new_player) @@ -818,8 +821,9 @@ func _on_peer_connected(peer_id: int) -> void: if peer_id == 1 and not is_host: # Wait a frame to ensure connection is stable await get_tree().process_frame - # Send our actual name and character to the host - rpc_id(1, "request_room_info", multiplayer.get_unique_id(), local_player_name, available_characters[local_character_index]) + # Send our actual name, character, and nakama_id to the host + var my_nakama_id: String = NakamaManager.session.user_id if NakamaManager.session else "" + rpc_id(1, "request_room_info", multiplayer.get_unique_id(), local_player_name, available_characters[local_character_index], my_nakama_id) func _on_peer_disconnected(peer_id: int) -> void: """Called when peer disconnects.""" diff --git a/scripts/managers/user_profile_manager.gd b/scripts/managers/user_profile_manager.gd index 32e732d..27b8b20 100644 --- a/scripts/managers/user_profile_manager.gd +++ b/scripts/managers/user_profile_manager.gd @@ -20,7 +20,7 @@ signal avatar_changed(url: String) var profile: Dictionary = {} var stats: Dictionary = {} var wallet: Dictionary = {"gold": 0, "star": 0} -var fragments: Dictionary = {} # frag_common, frag_uncommon, frag_rare +var fragments: Dictionary = {} # frag_common, frag_uncommon, frag_rare var inventory: Array = [] var loadout: Dictionary = {"head": "", "costume": "", "glove": "", "accessory": ""} var shop_catalog: Dictionary = {} diff --git a/scripts/services/backend_service.gd b/scripts/services/backend_service.gd new file mode 100644 index 0000000..72ceeaa --- /dev/null +++ b/scripts/services/backend_service.gd @@ -0,0 +1,133 @@ +extends Node + +## Unified interface for backend services +## All platforms use Nakama for achievements, leaderboards, and shop +## Steam is only used for authentication (auth session ticket for Nakama login) + +enum Platform { + DESKTOP_STEAM, + DESKTOP_NAKAMA, + MOBILE_NAKAMA +} + +var current_platform: Platform = Platform.DESKTOP_STEAM +var steamworks_manager: Node # Only for auth ticket retrieval +var nakama_backend: Node + +func _ready() -> void: + _detect_platform() + _initialize_backend() + +func _detect_platform() -> void: + # Detect if running on mobile or desktop + if OS.has_feature("android") or OS.has_feature("ios"): + current_platform = Platform.MOBILE_NAKAMA + else: + # Desktop: detect Steam by checking if GodotSteam class exists + # OS.has_feature("steam") is only true when launched through Steam client, + # but ClassDB.class_exists("Steam") is true whenever the GDExtension is enabled + if ClassDB.class_exists("Steam"): + current_platform = Platform.DESKTOP_STEAM + else: + current_platform = Platform.DESKTOP_NAKAMA + +func _initialize_backend() -> void: + # All platforms use Nakama for backend features + # Steamworks is only initialized for auth ticket retrieval when GodotSteam is available + if current_platform == Platform.DESKTOP_STEAM: + _initialize_steamworks_for_auth() + + _initialize_nakama() + +func _initialize_steamworks_for_auth() -> void: + var steamworks_script = load("res://scripts/services/steamworks_manager.gd") + if steamworks_script: + steamworks_manager = steamworks_script.new() + add_child(steamworks_manager) + print("BackendService: Initialized Steamworks for auth only") + else: + push_error("BackendService: Failed to load Steamworks manager") + +func _initialize_nakama() -> void: + nakama_backend = NakamaManager + if nakama_backend: + _connect_nakama_signals() + print("BackendService: Initialized Nakama backend") + else: + push_error("BackendService: NakamaManager not found") + +func _connect_nakama_signals() -> void: + # Nakama signals are handled directly by NakamaManager + # No need to connect through BackendService + pass + +## Achievement Methods +# All platforms use Nakama for achievements + +func unlock_achievement(achievement_id: String) -> void: + if nakama_backend: + # Nakama achievement implementation + pass + +func set_achievement_progress(achievement_id: String, current: int, max: int) -> void: + if nakama_backend: + # Nakama progress implementation + pass + +func get_achievement_progress(achievement_id: String) -> Dictionary: + if nakama_backend: + # Nakama get progress implementation + pass + return {} + +func get_all_achievements() -> Array: + if nakama_backend: + # Nakama get all achievements implementation + pass + return [] + +## Leaderboard Methods +# All platforms use Nakama for leaderboards + +func submit_leaderboard_score(leaderboard_id: String, score: int) -> void: + if nakama_backend: + # Nakama leaderboard submission - use UserProfileManager.submit_to_leaderboard() + await UserProfileManager.submit_to_leaderboard() + +func get_leaderboard_entries(leaderboard_id: String, range_start: int = 1, range_end: int = 10) -> void: + if nakama_backend: + # Nakama get leaderboard entries - use LeaderboardPanel._fetch_leaderboard_data() + # This is handled by the UI panel directly + pass + +## Shop Methods +# All platforms use Nakama for shop + +func purchase_shop_item(item_id: String) -> void: + if nakama_backend: + # Nakama shop purchase - use UserProfileManager.purchase_item() + # This is handled by the UI panel directly via ShopPanel + pass + +func get_shop_items() -> void: + if nakama_backend: + # Nakama get shop items - use UserProfileManager.fetch_shop_catalog() + # This is handled by the UI panel directly via ShopPanel + pass + +## Utility Methods + +func is_initialized() -> bool: + # Nakama is the primary backend for all features + if nakama_backend != null: + return true + + # Steamworks is optional (only for auth) + return false + +func get_platform_name() -> String: + return Platform.keys()[current_platform] + +func get_steamworks_manager() -> Node: + # Returns SteamworksManager for auth ticket retrieval (Steam login) + return steamworks_manager diff --git a/scripts/services/backend_service.gd.uid b/scripts/services/backend_service.gd.uid new file mode 100644 index 0000000..fae77dd --- /dev/null +++ b/scripts/services/backend_service.gd.uid @@ -0,0 +1 @@ +uid://dnpp80dw4ve8l diff --git a/scripts/services/steamworks_manager.gd b/scripts/services/steamworks_manager.gd new file mode 100644 index 0000000..bde7293 --- /dev/null +++ b/scripts/services/steamworks_manager.gd @@ -0,0 +1,72 @@ +class_name SteamworksManager +extends Node + +## Steamworks integration for Nakama authentication only +## Used to get Steam auth session tickets for Nakama login/registration +## Steam singleton is provided by GodotSteam GDExtension + +var is_steam_initialized: bool = false +var steam_app_id: int = ProjectSettings.get_setting("steam/initialization/app_id", 480) + +func _ready() -> void: + _initialize_steam() + +func _initialize_steam() -> void: + # Check if GodotSteam GDExtension is loaded + if not ClassDB.class_exists("Steam"): + push_error("SteamworksManager: GodotSteam GDExtension not found. Enable it in Project Settings > Plugins.") + return + + # Use steamInitEx for proper initialization with status reporting + var init_result: Dictionary = Steam.steamInitEx() + var status: int = init_result.get("status", -1) + var verbal: String = init_result.get("verbal", "Unknown error") + + if status == 0: + is_steam_initialized = true + print("SteamworksManager: Steam initialized (App ID: %s)" % steam_app_id) + else: + push_warning("SteamworksManager: Steam init failed [%d] - %s" % [status, verbal]) + print("SteamworksManager: Make sure Steam is running and App ID %s is valid" % steam_app_id) + +func is_initialized() -> bool: + return is_steam_initialized + +## Auth Methods + +func get_auth_session_ticket() -> String: + if not is_steam_initialized: + push_warning("SteamworksManager: Steam not initialized, cannot get auth ticket") + return "" + + # getAuthSessionTicket returns a Dictionary in GodotSteam {"id": int, "buffer": PackedByteArray} + var ticket_data = Steam.getAuthSessionTicket() + if typeof(ticket_data) == TYPE_DICTIONARY: + var buffer: PackedByteArray = ticket_data.get("buffer", PackedByteArray()) + if buffer.size() > 0: + var ticket_hex = buffer.hex_encode() + print("SteamworksManager: Got Steam auth session ticket") + return ticket_hex + push_error("SteamworksManager: Auth ticket buffer is empty") + return "" + elif typeof(ticket_data) == TYPE_STRING and not ticket_data.is_empty(): + print("SteamworksManager: Got Steam auth session ticket") + return ticket_data + else: + push_error("SteamworksManager: Failed to get auth session ticket") + return "" + +func get_steam_user_name() -> String: + if not is_steam_initialized: + return "" + return Steam.getPersonaName() + +func get_steam_user_id() -> int: + if not is_steam_initialized: + return 0 + return Steam.getSteamID() + +func _notification(what: int) -> void: + if what == NOTIFICATION_WM_CLOSE_REQUEST: + if is_steam_initialized: + Steam.steamShutdown() diff --git a/scripts/services/steamworks_manager.gd.uid b/scripts/services/steamworks_manager.gd.uid new file mode 100644 index 0000000..b66d12b --- /dev/null +++ b/scripts/services/steamworks_manager.gd.uid @@ -0,0 +1 @@ +uid://3k02bu0c2ast diff --git a/scripts/ui/friend_row.gd b/scripts/ui/friend_row.gd new file mode 100644 index 0000000..2d1beb8 --- /dev/null +++ b/scripts/ui/friend_row.gd @@ -0,0 +1,35 @@ +extends HBoxContainer +## FriendRow — Single row in the friends list inside SocialPanel. + +@onready var _name_label: Label = %NameLabel +@onready var _state_label: Label = %StateLabel +@onready var _dm_btn: Button = %DMBtn +@onready var _accept_btn: Button = %AcceptBtn +@onready var _decline_btn: Button = %DeclineBtn +@onready var _remove_btn: Button = %RemoveBtn + +var _user_id: String +var _username: String +var _panel: Control # reference to SocialPanel for opening DM + +func setup(uid: String, uname: String, state: int, panel: Control) -> void: + _user_id = uid + _username = uname + _panel = panel + + _name_label.text = uname + + match state: + FriendManager.STATE_FRIEND: + _dm_btn.visible = true + _remove_btn.visible = true + _dm_btn.pressed.connect(func(): panel.call("_open_dm", uid, uname)) + _remove_btn.pressed.connect(func(): FriendManager.remove_friend(uid)) + FriendManager.STATE_INVITE_OUT: + _state_label.text = "(invite sent)" + _state_label.visible = true + FriendManager.STATE_INVITE_IN: + _accept_btn.visible = true + _decline_btn.visible = true + _accept_btn.pressed.connect(func(): FriendManager.add_friend_by_id(uid)) + _decline_btn.pressed.connect(func(): FriendManager.remove_friend(uid)) diff --git a/scripts/ui/friend_row.gd.uid b/scripts/ui/friend_row.gd.uid new file mode 100644 index 0000000..944d8da --- /dev/null +++ b/scripts/ui/friend_row.gd.uid @@ -0,0 +1 @@ +uid://blugche8rky44 diff --git a/scripts/ui/invite_friends_dialog.gd b/scripts/ui/invite_friends_dialog.gd new file mode 100644 index 0000000..30cba69 --- /dev/null +++ b/scripts/ui/invite_friends_dialog.gd @@ -0,0 +1,27 @@ +extends Window + +signal closed + +@onready var _friend_rows: VBoxContainer = %FriendRows +@onready var _no_friends_label: Label = %NoFriendsLabel +@onready var _close_btn: Button = %CloseBtn + +const INVITE_ROW_SCENE := preload("res://scenes/ui/invite_row.tscn") + +func _ready() -> void: + _close_btn.pressed.connect(func(): emit_signal("closed")) + close_requested.connect(func(): emit_signal("closed")) + +func open(friends: Array, match_id: String) -> void: + if friends.is_empty(): + _no_friends_label.visible = true + popup_centered() + return + + _no_friends_label.visible = false + for f in friends: + var row: Control = INVITE_ROW_SCENE.instantiate() + _friend_rows.add_child(row) + row.setup(f.get("username", "?"), f.get("user_id", ""), match_id) + + popup_centered() diff --git a/scripts/ui/invite_friends_dialog.gd.uid b/scripts/ui/invite_friends_dialog.gd.uid new file mode 100644 index 0000000..c3e7914 --- /dev/null +++ b/scripts/ui/invite_friends_dialog.gd.uid @@ -0,0 +1 @@ +uid://c3aw5biordna diff --git a/scripts/ui/invite_row.gd b/scripts/ui/invite_row.gd new file mode 100644 index 0000000..154c1d8 --- /dev/null +++ b/scripts/ui/invite_row.gd @@ -0,0 +1,12 @@ +extends HBoxContainer + +@onready var _name_label: Label = %NameLabel +@onready var _invite_btn: Button = %InviteBtn + +func setup(username: String, user_id: String, match_id: String) -> void: + _name_label.text = username + _invite_btn.pressed.connect(func(): + FriendManager.send_lobby_invite(user_id, match_id) + _invite_btn.text = "Sent!" + _invite_btn.disabled = true + ) diff --git a/scripts/ui/invite_row.gd.uid b/scripts/ui/invite_row.gd.uid new file mode 100644 index 0000000..939bea2 --- /dev/null +++ b/scripts/ui/invite_row.gd.uid @@ -0,0 +1 @@ +uid://by70gfocmnnbj diff --git a/scripts/ui/lobby_invite_popup.gd b/scripts/ui/lobby_invite_popup.gd new file mode 100644 index 0000000..13cfa0c --- /dev/null +++ b/scripts/ui/lobby_invite_popup.gd @@ -0,0 +1,14 @@ +extends ConfirmationDialog + +signal accepted +signal declined + +@onready var _message_label: Label = %MessageLabel + +func _ready() -> void: + confirmed.connect(func(): emit_signal("accepted")) + canceled.connect(func(): emit_signal("declined")) + get_cancel_button().text = "Decline" + +func setup(from_name: String) -> void: + _message_label.text = "%s invited you to join their lobby!\n\nJoin now?" % from_name diff --git a/scripts/ui/lobby_invite_popup.gd.uid b/scripts/ui/lobby_invite_popup.gd.uid new file mode 100644 index 0000000..c24ad4c --- /dev/null +++ b/scripts/ui/lobby_invite_popup.gd.uid @@ -0,0 +1 @@ +uid://okk0tebly5gr diff --git a/scripts/ui/login_screen.gd b/scripts/ui/login_screen.gd index 630b9b0..a3130fe 100644 --- a/scripts/ui/login_screen.gd +++ b/scripts/ui/login_screen.gd @@ -10,6 +10,7 @@ extends Control @onready var google_button := %GoogleButton as Button @onready var apple_button := %AppleButton as Button @onready var facebook_button := %FacebookButton as Button +@onready var steam_button := %SteamButton as Button @onready var status_label := %StatusLabel as Label @onready var loading_spinner := %LoadingSpinner as TextureProgressBar @@ -65,6 +66,7 @@ func _connect_signals() -> void: google_button.pressed.connect(_on_google_pressed) apple_button.pressed.connect(_on_apple_pressed) facebook_button.pressed.connect(_on_facebook_pressed) + steam_button.pressed.connect(_on_steam_pressed) # Registration buttons register_button.pressed.connect(_on_register_pressed) @@ -99,15 +101,18 @@ func _setup_ui() -> void: _configure_social_buttons() func _configure_social_buttons() -> void: - # Google - available on all platforms - google_button.visible = true + # Google - hidden until API is configured + google_button.visible = false # Apple - iOS and macOS only (or hide if not configured) var os := OS.get_name() apple_button.visible = os in ["iOS", "macOS"] - # Facebook - available on all platforms - facebook_button.visible = true + # Facebook - hidden until API is configured + facebook_button.visible = false + + # Steam - show when GodotSteam GDExtension is available + steam_button.visible = ClassDB.class_exists("Steam") # ============================================================================= # Panel Switching @@ -190,6 +195,13 @@ func _on_facebook_pressed() -> void: # When you have the access token from Facebook SDK: # AuthManager.login_with_facebook(access_token) +func _on_steam_pressed() -> void: + if is_loading: + return + + # Steam login uses Steamworks auth ticket + AuthManager.login_with_steam() + func _on_server_option_selected(index: int) -> void: @@ -421,6 +433,7 @@ func _set_inputs_enabled(enabled: bool) -> void: google_button.disabled = not enabled apple_button.disabled = not enabled facebook_button.disabled = not enabled + steam_button.disabled = not enabled email_input.editable = enabled password_input.editable = enabled reg_email_input.editable = enabled diff --git a/scripts/ui/profile_panel.gd b/scripts/ui/profile_panel.gd index 2e0d3e2..0f6d9ce 100644 --- a/scripts/ui/profile_panel.gd +++ b/scripts/ui/profile_panel.gd @@ -231,6 +231,7 @@ func _auth_mode_name(mode: int) -> String: AuthManager.AuthMode.GOOGLE: return "Google" AuthManager.AuthMode.APPLE: return "Apple" AuthManager.AuthMode.FACEBOOK: return "Facebook" + AuthManager.AuthMode.STEAM: return "Steam" _: return "Guest" # ───────────────────────────────────────────────────────────── diff --git a/scripts/ui/social_panel.gd b/scripts/ui/social_panel.gd new file mode 100644 index 0000000..bbc25ed --- /dev/null +++ b/scripts/ui/social_panel.gd @@ -0,0 +1,164 @@ +extends Control +## SocialPanel — Friend list with DM and global chat tabs. +## Nodes defined in social_panel.tscn; this script handles all logic. + +signal closed + +# ─── Node references via %UniqueName ───────────────────────────────────── +@onready var _close_btn: Button = %CloseBtn +@onready var _friends_tab_btn: Button = %FriendsTabBtn +@onready var _global_tab_btn: Button = %GlobalTabBtn +@onready var _dm_tab_btn: Button = %DMTabBtn +@onready var _friends_view: VBoxContainer = %FriendsView +@onready var _global_view: VBoxContainer = %GlobalView +@onready var _dm_view: VBoxContainer = %DMView +@onready var _add_friend_input: LineEdit = %AddFriendInput +@onready var _add_friend_btn: Button = %AddFriendBtn +@onready var _friend_list: VBoxContainer = %FriendList +@onready var _global_log: RichTextLabel = %GlobalLog +@onready var _global_input: LineEdit = %GlobalInput +@onready var _global_send_btn: Button = %GlobalSendBtn +@onready var _dm_back_btn: Button = %DMBackBtn +@onready var _dm_username_label: Label = %DMUsernameLabel +@onready var _dm_log: RichTextLabel = %DMLog +@onready var _dm_input: LineEdit = %DMInput +@onready var _dm_send_btn: Button = %DMSendBtn + +# ─── State ──────────────────────────────────────────────────────────────── +var _active_dm_user_id: String = "" +var _active_dm_username: String = "" +var _dm_history: Dictionary = {} +var _global_chat_channel = null +var _current_tab: String = "friends" + +# ─── Lifecycle ──────────────────────────────────────────────────────────── +func _ready() -> void: + _close_btn.pressed.connect(func(): emit_signal("closed"); hide()) + _friends_tab_btn.pressed.connect(func(): _show_tab("friends")) + _global_tab_btn.pressed.connect(func(): _show_tab("global")) + _dm_tab_btn.pressed.connect(func(): _show_tab("dm")) + _add_friend_btn.pressed.connect(_on_add_friend_pressed) + _add_friend_input.text_submitted.connect(func(_t): _on_add_friend_pressed()) + _global_send_btn.pressed.connect(_send_global_message) + _global_input.text_submitted.connect(func(_t): _send_global_message()) + _dm_send_btn.pressed.connect(_send_dm) + _dm_input.text_submitted.connect(func(_t): _send_dm()) + _dm_back_btn.pressed.connect(func(): _show_tab("friends")) + + FriendManager.friends_updated.connect(_refresh_friend_list) + FriendManager.dm_message_received.connect(_on_dm_received) + NakamaManager.connected_to_nakama.connect(_join_global_chat) + if NakamaManager.socket and NakamaManager.socket.is_connected_to_host(): + _join_global_chat() + FriendManager.load_friends() + _show_tab("friends") + +func _refresh_friend_list(friends: Array) -> void: + if not _friend_list: + return + for ch in _friend_list.get_children(): + ch.queue_free() + + if friends.is_empty(): + var empty_lbl := Label.new() + empty_lbl.text = "No friends yet. Add someone above!" + _friend_list.add_child(empty_lbl) + return + + var friend_row_scene := preload("res://scenes/ui/friend_row.tscn") + for f in friends: + var uid: String = f.get("user_id", "") + var uname: String = f.get("username", "?") + var state: int = f.get("state", 0) + var row: Control = friend_row_scene.instantiate() + _friend_list.add_child(row) + row.setup(uid, uname, state, self) + +func _on_add_friend_pressed() -> void: + var val := _add_friend_input.text.strip_edges() + if val.is_empty(): + return + _add_friend_input.text = "" + if val.length() == 36 and val.count("-") == 4: + FriendManager.add_friend_by_id(val) + else: + FriendManager.add_friend_by_username(val) + +func _join_global_chat() -> void: + if _global_chat_channel: + return + var socket = NakamaManager.socket + if not socket: + return + var channel = await socket.join_chat_async( + "social_global", NakamaSocket.ChannelType.Room, true, false) + if channel.is_exception(): + return + _global_chat_channel = channel + if not socket.received_channel_message.is_connected(_on_global_message): + socket.received_channel_message.connect(_on_global_message) + +func _send_global_message() -> void: + var text = _global_input.text.strip_edges() + if text.is_empty() or not _global_chat_channel: + return + _global_input.text = "" + var socket = NakamaManager.socket + if socket: + socket.write_chat_message_async(_global_chat_channel.id, {"msg": text}) + +func _on_global_message(msg) -> void: + if not _global_chat_channel or msg.channel_id != _global_chat_channel.id: + return + var text: String = "" + var parsed = JSON.parse_string(msg.content) + if typeof(parsed) == TYPE_DICTIONARY: + text = parsed.get("msg", msg.content) + else: + text = msg.content + var sender_name: String = msg.username if msg.username else "?" + if _global_log: + _global_log.append_text("[b]%s:[/b] %s\n" % [sender_name, text]) + +func _open_dm(user_id: String, username: String) -> void: + _active_dm_user_id = user_id + _active_dm_username = username + _dm_username_label.text = "DM: %s" % username + _dm_tab_btn.visible = true + # Reload history + _dm_log.clear() + var history: Array = _dm_history.get(user_id, []) + for entry in history: + var is_self = entry.get("from") == "me" + var prefix = "[b]%s:[/b]" % ("You" if is_self else username) + _dm_log.append_text("%s %s\n" % [prefix, entry.get("msg", "")]) + # Open channel + FriendManager.open_dm(user_id) + _show_tab("dm") + +func _send_dm() -> void: + var text = _dm_input.text.strip_edges() + if text.is_empty() or _active_dm_user_id.is_empty(): + return + _dm_input.text = "" + var sent = await FriendManager.send_dm(_active_dm_user_id, text) + if sent: + if not _dm_history.has(_active_dm_user_id): + _dm_history[_active_dm_user_id] = [] + _dm_history[_active_dm_user_id].append({"from": "me", "msg": text}) + _dm_log.append_text("[b]You:[/b] %s\n" % text) + +func _on_dm_received(from_user_id: String, from_name: String, message: String) -> void: + if not _dm_history.has(from_user_id): + _dm_history[from_user_id] = [] + _dm_history[from_user_id].append({"from": from_user_id, "msg": message}) + + if _active_dm_user_id == from_user_id and _current_tab == "dm": + _dm_log.append_text("[b]%s:[/b] %s\n" % [from_name, message]) + +# ─── Tab switching ───────────────────────────────────────────────────────── +func _show_tab(tab: String) -> void: + _current_tab = tab + _friends_view.visible = tab == "friends" + _global_view.visible = tab == "global" + _dm_view.visible = tab == "dm" diff --git a/scripts/ui/social_panel.gd.uid b/scripts/ui/social_panel.gd.uid new file mode 100644 index 0000000..c2f085f --- /dev/null +++ b/scripts/ui/social_panel.gd.uid @@ -0,0 +1 @@ +uid://dyr5tlvds11ib diff --git a/server/nakama/tekton_admin.js b/server/nakama/tekton_admin.js index 6c37ce2..1a2e2c7 100644 --- a/server/nakama/tekton_admin.js +++ b/server/nakama/tekton_admin.js @@ -39,6 +39,10 @@ function InitModule(ctx, logger, nk, initializer) { initializer.registerRpc("sync_leaderboard", rpcSyncLeaderboard); initializer.registerRpc("change_credentials", rpcChangeCredentials); initializer.registerRpc("reset_stats", rpcResetStats); + initializer.registerRpc("send_lobby_invite", rpcSendLobbyInvite); + + // Steam auth hooks + initializer.registerAfterAuthenticateSteam(afterAuthenticateSteam); // Create default native leaderboard // id: "global_high_score", authoritative: true, sort: "desc", operator: "best", reset: None @@ -154,6 +158,67 @@ function requireAdminOrHost(ctx, nk, matchId) { } } +// ============================================================================= +// Lobby Invite RPC +// ============================================================================= + +function rpcSendLobbyInvite(ctx, logger, nk, payload) { + if (!ctx.userId) throw new Error("Not authenticated"); + var req = JSON.parse(payload || "{}"); + var toUserId = req.to_user_id; + var matchId = req.match_id; + if (!toUserId || !matchId) throw new Error("Missing to_user_id or match_id"); + + var sender = nk.accountGetId(ctx.userId); + var senderName = sender.user.displayName || sender.user.username || "Someone"; + + nk.notificationSend( + toUserId, + senderName + " invited you to their lobby", + JSON.stringify({ match_id: matchId, from_name: senderName }), + 1001, + ctx.userId, + true + ); + + logger.info("Lobby invite sent from " + ctx.userId + " to " + toUserId + " for match " + matchId); + return JSON.stringify({ success: true }); +} + +// ============================================================================= +// Steam Auth Hook +// ============================================================================= + +function afterAuthenticateSteam(ctx, logger, nk, out, request) { + if (!ctx.userId) return; + + try { + var account = nk.accountGetId(ctx.userId); + + // On first login (no display name set), use Steam username + if (!account.user.displayName) { + var steamName = request.username || "SteamPlayer"; + nk.accountUpdateId(ctx.userId, null, steamName, null, null, null, null, null); + logger.info("Steam user " + ctx.userId + " display name set to: " + steamName); + } + + // Set default role if not set + var metadata = {}; + try { + metadata = typeof account.user.metadata === "string" + ? JSON.parse(account.user.metadata || "{}") + : (account.user.metadata || {}); + } catch (e) {} + + if (!metadata.role) { + metadata.role = "player"; + nk.accountUpdateId(ctx.userId, null, null, null, null, null, null, JSON.stringify(metadata)); + } + } catch (e) { + logger.error("afterAuthenticateSteam error: " + e); + } +} + // ============================================================================= // Admin RPCs // ============================================================================= diff --git a/tools/generate_version_json.py b/tools/generate_version_json.py index 173ee06..9745336 100644 --- a/tools/generate_version_json.py +++ b/tools/generate_version_json.py @@ -6,20 +6,34 @@ Auto-generates version.json by: 2. Auto-bumping the PATCH number 3. Reading player-friendly entries from CHANGELOG_DRAFT.md 4. Injecting them into version.json with a date stamp - 5. Clearing the [NEXT] section in CHANGELOG_DRAFT.md -Run: python3 tools/generate_version_json.py + 5. Updating version in export_presets.cfg export paths + 6. Clearing the [NEXT] section in CHANGELOG_DRAFT.md + +Usage: + python3 tools/generate_version_json.py # Full release (CI) + python3 tools/generate_version_json.py --local # Local: update versions but keep changelog + python3 tools/generate_version_json.py --skip-changelog # Skip changelog update """ import json import re import os +import sys +import argparse from datetime import date PROJECT_GODOT = "project.godot" CHANGELOG_DRAFT = "CHANGELOG_DRAFT.md" VERSION_JSON = "assets/data/version.json" +EXPORT_PRESETS = "export_presets.cfg" MANIFEST_URL = "https://raw.githubusercontent.com/adtpdn/tekton-updates/main/latest/patch.pck" +# ─── Parse command line arguments ───────────────────────────────────────────── +parser = argparse.ArgumentParser(description="Generate version.json and update version numbers") +parser.add_argument("--local", action="store_true", help="Local mode: update version.json and export_presets but don't clear changelog") +parser.add_argument("--skip-changelog", action="store_true", help="Skip updating CHANGELOG_DRAFT.md") +args = parser.parse_args() + # ─── 1. Read current version from project.godot ────────────────────────────── version_str = "2.1.5" with open(PROJECT_GODOT, "r") as f: @@ -113,16 +127,44 @@ with open(PROJECT_GODOT, "w", encoding="utf-8") as f: print(f"[OK] Bumped project.godot -> {new_version}") -# ─── 7. Clear [NEXT] in CHANGELOG_DRAFT.md ─────────────────────────────────── -archived_header = f"## [{new_version}] — {today}\n" -archived_entries = "".join(f"- {line}\n" for line in changelog_lines) +# ─── 7. Update version in export_presets.cfg ─────────────────────────────────── +if os.path.exists(EXPORT_PRESETS): + with open(EXPORT_PRESETS, "r", encoding="utf-8") as f: + presets_content = f.read() -new_draft = "## [NEXT]\n\n" + archived_header + archived_entries -if remaining_lines: - new_draft += "\n" + "".join(remaining_lines) + # Replace version in desktop export paths (e.g., v2.1.7 -> v2.1.8) + # Desktop: tekton_armageddon_v2.1.7.exe/zip/x86_64 + # Android: tekton-dash-armageddon-v.2.1.5.apk (different format) + desktop_pattern = r'tekton_armageddon_v\d+\.\d+\.\d+' + presets_content = re.sub(desktop_pattern, f'tekton_armageddon_v{new_version}', presets_content) -with open(CHANGELOG_DRAFT, "w", encoding="utf-8") as f: - f.write(new_draft) + # Also update Android version if needed (different format) + android_pattern = r'tekton-dash-armageddon-v\.\d+\.\d+\.\d+' + presets_content = re.sub(android_pattern, f'tekton-dash-armageddon-v.{new_version}', presets_content) -print(f"[OK] Cleared [NEXT] in {CHANGELOG_DRAFT}, archived as [{new_version}]") -print(f"\nDone! Push to patch-release to deploy v{new_version}.") + with open(EXPORT_PRESETS, "w", encoding="utf-8") as f: + f.write(presets_content) + + print(f"[OK] Updated export paths in {EXPORT_PRESETS} -> v{new_version}") +else: + print(f"[WARN] {EXPORT_PRESETS} not found, skipping export path update") + +# ─── 8. Clear [NEXT] in CHANGELOG_DRAFT.md ─────────────────────────────────── +if not args.local and not args.skip_changelog: + archived_header = f"## [{new_version}] — {today}\n" + archived_entries = "".join(f"- {line}\n" for line in changelog_lines) + + new_draft = "## [NEXT]\n\n" + archived_header + archived_entries + if remaining_lines: + new_draft += "\n" + "".join(remaining_lines) + + with open(CHANGELOG_DRAFT, "w", encoding="utf-8") as f: + f.write(new_draft) + + print(f"[OK] Cleared [NEXT] in {CHANGELOG_DRAFT}, archived as [{new_version}]") +else: + print(f"[INFO] Skipped updating {CHANGELOG_DRAFT} (--local or --skip-changelog mode)") + +print(f"\nDone! Version bumped to v{new_version}.") +if args.local: + print("Local mode: Changelog not cleared. Run without --local to finalize release.")