feat: update 2.1.9
This commit is contained in:
+10
-1
@@ -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
|
||||
|
||||
@@ -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.
Binary file not shown.
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
# =============================================================================
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://deq26r61rqq51
|
||||
@@ -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."""
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dnpp80dw4ve8l
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -0,0 +1 @@
|
||||
uid://blugche8rky44
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://c3aw5biordna
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
uid://by70gfocmnnbj
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://okk0tebly5gr
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
uid://dyr5tlvds11ib
|
||||
@@ -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
|
||||
// =============================================================================
|
||||
|
||||
@@ -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.")
|
||||
|
||||
Reference in New Issue
Block a user