feat: update 2.1.9

This commit is contained in:
2026-04-29 01:36:49 +08:00
parent 1585b91509
commit 8a2f865ad8
76 changed files with 2628 additions and 39 deletions
+10 -1
View File
@@ -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
+86
View File
@@ -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
Binary file not shown.
@@ -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)
@@ -0,0 +1 @@
uid://1oo0gvamoxgp
@@ -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
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="50px" height="50px" fill-rule="nonzero"><g fill="#ffffff" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.12,5.12)"><path d="M25,3c-11.41,0 -20.79062,8.68078 -21.89062,19.80078l11.19141,5.72852c1.13,-0.95 2.59922,-1.5293 4.19922,-1.5293h0.05078c0.39,-0.6 0.83859,-1.35086 1.30859,-2.13086c0.98,-1.61 2.08016,-3.43008 3.16016,-4.83008c0.24,-4.47 3.95047,-8.03906 8.48047,-8.03906c4.69,0 8.5,3.81 8.5,8.5c0,4.53 -3.56906,8.24047 -8.03906,8.48047c-1.4,1.08 -3.21008,2.18039 -4.83008,3.15039c-0.78,0.48 -1.53086,0.92836 -2.13086,1.31836v0.05078c0,3.59 -2.91,6.5 -6.5,6.5c-3.59,0 -6.5,-2.91 -6.5,-6.5c0,-0.17 0.00953,-0.33 0.01953,-0.5l-8.74023,-4.48047c1.69,10.48 10.7707,18.48047 21.7207,18.48047c12.15,0 22,-9.85 22,-22c0,-12.15 -9.85,-22 -22,-22zM31.5,14c-3.58,0 -6.5,2.92 -6.5,6.5c0,3.58 2.92,6.5 6.5,6.5c3.58,0 6.5,-2.92 6.5,-6.5c0,-3.58 -2.92,-6.5 -6.5,-6.5zM31.5,16c2.49,0 4.5,2.01 4.5,4.5c0,2.49 -2.01,4.5 -4.5,4.5c-2.49,0 -4.5,-2.01 -4.5,-4.5c0,-2.49 2.01,-4.5 4.5,-4.5zM18.5,29c-0.79,0 -1.53969,0.20031 -2.17969,0.57031l3.32031,1.69922c1.23,0.63 1.70984,2.14109 1.08984,3.37109c-0.45,0.86 -1.32047,1.35938 -2.23047,1.35938c-0.39,0 -0.77062,-0.08953 -1.14062,-0.26953l-3.33008,-1.71094c0.26,2.24 2.1607,3.98047 4.4707,3.98047c2.49,0 4.5,-2.01 4.5,-4.5c0,-2.49 -2.01,-4.5 -4.5,-4.5z"></path></g></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -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
+186
View File
@@ -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
@@ -0,0 +1 @@
uid://b4nvgu7h84u53
+29
View File
@@ -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": "" }
@@ -0,0 +1 @@
uid://cxrfuk7agwsov
+85
View File
@@ -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
@@ -0,0 +1 @@
uid://btts11rgropcp
+23
View File
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7
View File
@@ -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"
+93
View File
@@ -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
Binary file not shown.
Binary file not shown.
+29 -6
View File
@@ -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"
]
+422
View File
@@ -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
+115 -2
View File
@@ -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}\""
+11 -2
View File
@@ -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
+121
View File
@@ -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
# =============================================================================
+63 -1
View File
@@ -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
+40
View File
@@ -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
+43
View File
@@ -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"
+16
View File
@@ -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"
+19
View File
@@ -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
+9 -2
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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")
+179
View File
@@ -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"
+69 -1
View File
@@ -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)
# =============================================================================
+176
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://deq26r61rqq51
+11 -7
View File
@@ -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."""
+1 -1
View File
@@ -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 = {}
+133
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://dnpp80dw4ve8l
+72
View File
@@ -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()
@@ -0,0 +1 @@
uid://3k02bu0c2ast
+35
View File
@@ -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))
+1
View File
@@ -0,0 +1 @@
uid://blugche8rky44
+27
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://c3aw5biordna
+12
View File
@@ -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
)
+1
View File
@@ -0,0 +1 @@
uid://by70gfocmnnbj
+14
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://okk0tebly5gr
+17 -4
View File
@@ -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
+1
View File
@@ -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"
# ─────────────────────────────────────────────────────────────
+164
View File
@@ -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"
+1
View File
@@ -0,0 +1 @@
uid://dyr5tlvds11ib
+65
View File
@@ -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
// =============================================================================
+54 -12
View File
@@ -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.")