feat : update backend
This commit is contained in:
@@ -1081,4 +1081,3 @@ func write_tournament_record2_async(p_session : NakamaSession,
|
||||
"score": str(p_score),
|
||||
"subscore": str(p_subscore)
|
||||
}))
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
[ext_resource type="Texture2D" uid="uid://dpkx1a780pvwv" path="res://assets/textures/tile_diamond.png" id="10_sx8rm"]
|
||||
[ext_resource type="BoxMesh" uid="uid://fy4bhoeii40c" path="res://addons/enhanced_gridmap/meshlibrary/tile_safe_zone.tres" id="10_uwjsj"]
|
||||
[ext_resource type="BoxMesh" uid="uid://b5cc3prem52r6" path="res://addons/enhanced_gridmap/meshlibrary/tile_freeze.tres" id="11_pgnbl"]
|
||||
[ext_resource type="BoxMesh" uid="uid://dcjdwbffgtutt" path="res://addons/enhanced_gridmap/meshlibrary/tile_non_walkable.tres" id="11_uwjsj"]
|
||||
[ext_resource type="BoxMesh" path="res://addons/enhanced_gridmap/meshlibrary/tile_non_walkable.tres" id="11_uwjsj"]
|
||||
|
||||
[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_5d0gc"]
|
||||
load_path = "res://.godot/imported/tile_heart.png-deeef50755ca225f028608dfd16900e6.s3tc.ctex"
|
||||
|
||||
@@ -63,13 +63,20 @@ layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("3_font")
|
||||
text = "← BACK"
|
||||
|
||||
[node name="RefreshBtn" type="Button" parent="MainLayout/LeftPanel/LeftVBox/Header"]
|
||||
[node name="RefreshBtn" type="Button" parent="MainLayout/LeftPanel/LeftVBox/Header" unique_id=993543919]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(44, 44)
|
||||
layout_mode = 2
|
||||
tooltip_text = "Refresh Data"
|
||||
text = "⟳"
|
||||
|
||||
[node name="SyncBtn" type="Button" parent="MainLayout/LeftPanel/LeftVBox/Header" unique_id=1452457095]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(120, 44)
|
||||
layout_mode = 2
|
||||
tooltip_text = "Sync your score to the global leaderboard"
|
||||
text = "↑ Sync Score"
|
||||
|
||||
[node name="Title" type="Label" parent="MainLayout/LeftPanel/LeftVBox/Header" unique_id=1037998429]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
@@ -186,21 +193,21 @@ autoplay = &"animation-pack/idle"
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 8
|
||||
|
||||
[node name="InfoMargin" type="MarginContainer" parent="MainLayout/RightPanel/SelectedPlayerInfo"]
|
||||
[node name="InfoMargin" type="MarginContainer" parent="MainLayout/RightPanel/SelectedPlayerInfo" unique_id=882298034]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 20
|
||||
theme_override_constants/margin_right = 20
|
||||
theme_override_constants/margin_top = 15
|
||||
theme_override_constants/margin_right = 20
|
||||
theme_override_constants/margin_bottom = 15
|
||||
|
||||
[node name="InfoVBox" type="VBoxContainer" parent="MainLayout/RightPanel/SelectedPlayerInfo/InfoMargin"]
|
||||
[node name="InfoVBox" type="VBoxContainer" parent="MainLayout/RightPanel/SelectedPlayerInfo/InfoMargin" unique_id=567154378]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="SelectedNameLabel" type="Label" parent="MainLayout/RightPanel/SelectedPlayerInfo/InfoMargin/InfoVBox" unique_id=1940372038]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_colors/font_color = Color(0.69803923, 0.5411765, 0.3647059, 1)
|
||||
theme_override_fonts/font = ExtResource("3_font")
|
||||
theme_override_font_sizes/font_size = 24
|
||||
text = "PLAYER NAME"
|
||||
|
||||
+145
-75
@@ -1,9 +1,9 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://b00eef4tqt2ri"]
|
||||
[gd_scene format=3 uid="uid://b00eef4tqt2ri"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b1lcy1ikdkknq" path="res://scripts/ui/login_screen.gd" id="1"]
|
||||
[ext_resource type="Theme" uid="uid://da337sh5qxi0s" path="res://assets/themes/ui_theme.tres" id="2"]
|
||||
|
||||
[node name="LoginScreen" type="Control"]
|
||||
[node name="LoginScreen" type="Control" unique_id=1394670984]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
@@ -13,7 +13,7 @@ grow_vertical = 2
|
||||
theme = ExtResource("2")
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="Background" type="ColorRect" parent="."]
|
||||
[node name="Background" type="ColorRect" parent="." unique_id=1913867293]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
@@ -22,7 +22,7 @@ grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
color = Color(0.12, 0.1, 0.08, 1)
|
||||
|
||||
[node name="CenterContainer" type="CenterContainer" parent="."]
|
||||
[node name="CenterContainer" type="CenterContainer" parent="." unique_id=706535114]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
@@ -30,177 +30,251 @@ anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="MainPanel" type="PanelContainer" parent="CenterContainer"]
|
||||
custom_minimum_size = Vector2(400, 0)
|
||||
[node name="LayoutVBox" type="VBoxContainer" parent="CenterContainer" unique_id=1110313476]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="CenterContainer/MainPanel"]
|
||||
[node name="HeaderVBox" type="VBoxContainer" parent="CenterContainer/LayoutVBox" unique_id=851120967]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 16
|
||||
theme_override_constants/separation = 2
|
||||
|
||||
[node name="LogoLabel" type="Label" parent="CenterContainer/MainPanel/VBox"]
|
||||
[node name="LogoLabel" type="Label" parent="CenterContainer/LayoutVBox/HeaderVBox" unique_id=73067558]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1)
|
||||
theme_override_font_sizes/font_size = 48
|
||||
theme_override_font_sizes/font_size = 42
|
||||
text = "TEKTON"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="SubtitleLabel" type="Label" parent="CenterContainer/MainPanel/VBox"]
|
||||
[node name="SubtitleLabel" type="Label" parent="CenterContainer/LayoutVBox/HeaderVBox" unique_id=240064697]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Armageddon"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Spacer1" type="Control" parent="CenterContainer/MainPanel/VBox"]
|
||||
custom_minimum_size = Vector2(0, 16)
|
||||
[node name="ServerSelectionSection" type="VBoxContainer" parent="CenterContainer/LayoutVBox" unique_id=580109643]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="GuestButton" type="Button" parent="CenterContainer/MainPanel/VBox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 52)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "▶ PLAY AS GUEST"
|
||||
|
||||
[node name="Spacer2" type="Control" parent="CenterContainer/MainPanel/VBox"]
|
||||
custom_minimum_size = Vector2(0, 8)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="OrLabel" type="Label" parent="CenterContainer/MainPanel/VBox"]
|
||||
[node name="ConnectionModeLabel" type="Label" parent="CenterContainer/LayoutVBox/ServerSelectionSection" unique_id=2002206900]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.69, 0.529, 0.357, 1)
|
||||
text = "─────── or sign in ───────"
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "CONNECTION MODE"
|
||||
|
||||
[node name="ServerOption" type="OptionButton" parent="CenterContainer/LayoutVBox/ServerSelectionSection" unique_id=975117024]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
selected = 3
|
||||
item_count = 4
|
||||
popup/item_0/text = "Nakama - Localhost (Testing)"
|
||||
popup/item_0/id = 0
|
||||
popup/item_1/text = "Nakama - Remote Server (Host IP)"
|
||||
popup/item_1/id = 1
|
||||
popup/item_2/text = "LAN Direct (No Server)"
|
||||
popup/item_2/id = 2
|
||||
popup/item_3/text = "Nakama - Tekton Dash EU"
|
||||
popup/item_3/id = 3
|
||||
|
||||
[node name="ServerIPInput" type="LineEdit" parent="CenterContainer/LayoutVBox/ServerSelectionSection" unique_id=1444265129]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
text = "127.0.0.1"
|
||||
placeholder_text = "Enter Nakama Server IP..."
|
||||
|
||||
[node name="LANSection" type="VBoxContainer" parent="CenterContainer/LayoutVBox/ServerSelectionSection" unique_id=1917634402]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="LanInfo" type="Label" parent="CenterContainer/LayoutVBox/ServerSelectionSection/LANSection" unique_id=75077084]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1)
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "Play over LAN without any server.
|
||||
Firewall may need to allow port 7777."
|
||||
autowrap_mode = 3
|
||||
|
||||
[node name="LANHostBtn" type="Button" parent="CenterContainer/LayoutVBox/ServerSelectionSection/LANSection" unique_id=751026831]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
text = "HOST LAN GAME"
|
||||
|
||||
[node name="LanSeparator" type="Label" parent="CenterContainer/LayoutVBox/ServerSelectionSection/LANSection" unique_id=2088668724]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.5, 0.5, 0.5, 1)
|
||||
theme_override_font_sizes/font_size = 11
|
||||
text = "── or join a friend ──"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="EmailInput" type="LineEdit" parent="CenterContainer/MainPanel/VBox"]
|
||||
[node name="LANIPInput" type="LineEdit" parent="CenterContainer/LayoutVBox/ServerSelectionSection/LANSection" unique_id=1859021984]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
text = "127.0.0.1"
|
||||
placeholder_text = "Host IP (e.g. 192.168.1.10)"
|
||||
|
||||
[node name="LANJoinBtn" type="Button" parent="CenterContainer/LayoutVBox/ServerSelectionSection/LANSection" unique_id=2009389522]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
text = "JOIN LAN GAME"
|
||||
|
||||
[node name="HSeparator" type="HSeparator" parent="CenterContainer/LayoutVBox" unique_id=1424264812]
|
||||
layout_mode = 2
|
||||
[node name="TabContainer" type="TabContainer" parent="CenterContainer/LayoutVBox" unique_id=2135112748]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(420, 0)
|
||||
layout_mode = 2
|
||||
current_tab = 0
|
||||
|
||||
[node name="Login" type="MarginContainer" parent="CenterContainer/LayoutVBox/TabContainer" unique_id=1294712927]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 16
|
||||
theme_override_constants/margin_top = 16
|
||||
theme_override_constants/margin_right = 16
|
||||
theme_override_constants/margin_bottom = 16
|
||||
metadata/_tab_index = 0
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="CenterContainer/LayoutVBox/TabContainer/Login" unique_id=1063599539]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
|
||||
[node name="GuestButton" type="Button" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox" unique_id=2037993426]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 48)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 16
|
||||
text = "â–¶ PLAY AS GUEST"
|
||||
|
||||
[node name="OrLabel" type="Label" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox" unique_id=1387412213]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.69, 0.529, 0.357, 1)
|
||||
text = "─────── or sign in ───────"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="EmailInput" type="LineEdit" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox" unique_id=323436289]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
placeholder_text = "Email"
|
||||
|
||||
[node name="PasswordInput" type="LineEdit" parent="CenterContainer/MainPanel/VBox"]
|
||||
[node name="PasswordInput" type="LineEdit" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox" unique_id=1098627484]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
placeholder_text = "Password"
|
||||
secret = true
|
||||
|
||||
[node name="RememberMe" type="CheckBox" parent="CenterContainer/MainPanel/VBox"]
|
||||
[node name="RememberMe" type="CheckBox" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox" unique_id=1013722117]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
button_pressed = true
|
||||
text = "Remember me"
|
||||
|
||||
[node name="LoginButton" type="Button" parent="CenterContainer/MainPanel/VBox"]
|
||||
[node name="LoginButton" type="Button" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox" unique_id=945681236]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 48)
|
||||
layout_mode = 2
|
||||
text = "Sign In"
|
||||
|
||||
[node name="RegisterLink" type="LinkButton" parent="CenterContainer/MainPanel/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Don't have an account? Register"
|
||||
|
||||
[node name="Spacer3" type="Control" parent="CenterContainer/MainPanel/VBox"]
|
||||
custom_minimum_size = Vector2(0, 8)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SocialLabel" type="Label" parent="CenterContainer/MainPanel/VBox"]
|
||||
[node name="SocialLabel" type="Label" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox" unique_id=840440943]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.69, 0.529, 0.357, 1)
|
||||
text = "─────── or continue with ───────"
|
||||
text = "─────── or continue with ───────"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="SocialButtons" type="HBoxContainer" parent="CenterContainer/MainPanel/VBox"]
|
||||
[node name="SocialButtons" type="HBoxContainer" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox" unique_id=1899762021]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
alignment = 1
|
||||
|
||||
[node name="GoogleButton" type="Button" parent="CenterContainer/MainPanel/VBox/SocialButtons"]
|
||||
[node name="GoogleButton" type="Button" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox/SocialButtons" unique_id=196743645]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(100, 44)
|
||||
layout_mode = 2
|
||||
text = "Google"
|
||||
|
||||
[node name="AppleButton" type="Button" parent="CenterContainer/MainPanel/VBox/SocialButtons"]
|
||||
[node name="AppleButton" type="Button" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox/SocialButtons" unique_id=1174582184]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(100, 44)
|
||||
layout_mode = 2
|
||||
text = "Apple"
|
||||
|
||||
[node name="FacebookButton" type="Button" parent="CenterContainer/MainPanel/VBox/SocialButtons"]
|
||||
[node name="FacebookButton" type="Button" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox/SocialButtons" unique_id=1252096120]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(100, 44)
|
||||
layout_mode = 2
|
||||
text = "Facebook"
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="CenterContainer/MainPanel/VBox"]
|
||||
[node name="StatusLabel" type="Label" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox" unique_id=1679807364]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 0.4, 0.4, 1)
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="LoadingSpinner" type="TextureProgressBar" parent="CenterContainer/MainPanel/VBox"]
|
||||
[node name="LoadingSpinner" type="TextureProgressBar" parent="CenterContainer/LayoutVBox/TabContainer/Login/VBox" unique_id=1985748047]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(40, 40)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
|
||||
[node name="RegistrationPanel" type="PanelContainer" parent="CenterContainer"]
|
||||
unique_name_in_owner = true
|
||||
[node name="Register" type="MarginContainer" parent="CenterContainer/LayoutVBox/TabContainer" unique_id=1373074681]
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(400, 0)
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 16
|
||||
theme_override_constants/margin_top = 16
|
||||
theme_override_constants/margin_right = 16
|
||||
theme_override_constants/margin_bottom = 16
|
||||
metadata/_tab_index = 1
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="CenterContainer/RegistrationPanel"]
|
||||
[node name="VBox" type="VBoxContainer" parent="CenterContainer/LayoutVBox/TabContainer/Register" unique_id=839364109]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="Title" type="Label" parent="CenterContainer/RegistrationPanel/VBox"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1)
|
||||
theme_override_font_sizes/font_size = 24
|
||||
text = "Create Account"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="RegEmailInput" type="LineEdit" parent="CenterContainer/RegistrationPanel/VBox"]
|
||||
[node name="RegEmailInput" type="LineEdit" parent="CenterContainer/LayoutVBox/TabContainer/Register/VBox" unique_id=1906331672]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
placeholder_text = "Email"
|
||||
|
||||
[node name="RegUsernameInput" type="LineEdit" parent="CenterContainer/RegistrationPanel/VBox"]
|
||||
[node name="RegUsernameInput" type="LineEdit" parent="CenterContainer/LayoutVBox/TabContainer/Register/VBox" unique_id=853099369]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
placeholder_text = "Username"
|
||||
|
||||
[node name="RegPasswordInput" type="LineEdit" parent="CenterContainer/RegistrationPanel/VBox"]
|
||||
[node name="RegPasswordInput" type="LineEdit" parent="CenterContainer/LayoutVBox/TabContainer/Register/VBox" unique_id=1603528515]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
placeholder_text = "Password"
|
||||
secret = true
|
||||
|
||||
[node name="RegConfirmPasswordInput" type="LineEdit" parent="CenterContainer/RegistrationPanel/VBox"]
|
||||
[node name="RegConfirmPasswordInput" type="LineEdit" parent="CenterContainer/LayoutVBox/TabContainer/Register/VBox" unique_id=830465730]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
placeholder_text = "Confirm Password"
|
||||
secret = true
|
||||
|
||||
[node name="PasswordStrength" type="ProgressBar" parent="CenterContainer/RegistrationPanel/VBox"]
|
||||
[node name="PasswordStrength" type="ProgressBar" parent="CenterContainer/LayoutVBox/TabContainer/Register/VBox" unique_id=2023454025]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 12)
|
||||
layout_mode = 2
|
||||
max_value = 4.0
|
||||
show_percentage = false
|
||||
|
||||
[node name="PasswordHint" type="Label" parent="CenterContainer/RegistrationPanel/VBox"]
|
||||
[node name="PasswordHint" type="Label" parent="CenterContainer/LayoutVBox/TabContainer/Register/VBox" unique_id=1402181329]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.69, 0.529, 0.357, 1)
|
||||
@@ -208,43 +282,38 @@ theme_override_font_sizes/font_size = 11
|
||||
text = "Min 8 characters, include number and symbol"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="RegCaptchaContainer" type="VBoxContainer" parent="CenterContainer/RegistrationPanel/VBox"]
|
||||
[node name="RegCaptchaContainer" type="VBoxContainer" parent="CenterContainer/LayoutVBox/TabContainer/Register/VBox" unique_id=583603175]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="RegCaptchaQuestion" type="Label" parent="CenterContainer/RegistrationPanel/VBox/RegCaptchaContainer"]
|
||||
[node name="RegCaptchaQuestion" type="Label" parent="CenterContainer/LayoutVBox/TabContainer/Register/VBox/RegCaptchaContainer" unique_id=1792478300]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1)
|
||||
text = "Security Check: 5 + 7 = ?"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="RegCaptchaInput" type="LineEdit" parent="CenterContainer/RegistrationPanel/VBox/RegCaptchaContainer"]
|
||||
[node name="RegCaptchaInput" type="LineEdit" parent="CenterContainer/LayoutVBox/TabContainer/Register/VBox/RegCaptchaContainer" unique_id=1405718888]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
placeholder_text = "Answer"
|
||||
alignment = 1
|
||||
|
||||
[node name="RegisterButton" type="Button" parent="CenterContainer/RegistrationPanel/VBox"]
|
||||
[node name="RegisterButton" type="Button" parent="CenterContainer/LayoutVBox/TabContainer/Register/VBox" unique_id=2040733064]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 48)
|
||||
layout_mode = 2
|
||||
text = "Create Account"
|
||||
|
||||
[node name="BackToLoginLink" type="LinkButton" parent="CenterContainer/RegistrationPanel/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Already have an account? Sign In"
|
||||
|
||||
[node name="RegStatusLabel" type="Label" parent="CenterContainer/RegistrationPanel/VBox"]
|
||||
[node name="RegStatusLabel" type="Label" parent="CenterContainer/LayoutVBox/TabContainer/Register/VBox" unique_id=1096594976]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 0.4, 0.4, 1)
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="VersionLabel" type="Label" parent="."]
|
||||
[node name="VersionLabel" type="Label" parent="." unique_id=481275614]
|
||||
layout_mode = 1
|
||||
anchors_preset = 3
|
||||
anchor_left = 1.0
|
||||
@@ -261,3 +330,4 @@ theme_override_colors/font_color = Color(0.6, 0.6, 0.6, 0.6)
|
||||
theme_override_font_sizes/font_size = 11
|
||||
text = "v0.9.0"
|
||||
horizontal_alignment = 2
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ theme_override_colors/font_color = Color(0.57, 0.32090998, 0.1767, 1)
|
||||
theme_override_fonts/font = ExtResource("3_e13i6")
|
||||
text = "High Score: 0"
|
||||
|
||||
[node name="AccountSection" type="VBoxContainer" parent="MainLayout/LeftPanel/LeftVBox" unique_id=1730058560]
|
||||
[node name="AccountSection" type="HBoxContainer" parent="MainLayout/LeftPanel/LeftVBox" unique_id=1267378790]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
@@ -242,8 +242,8 @@ text = "Link Email (Keep Progress)"
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 0.4, 0.4, 1)
|
||||
theme_override_fonts/font = ExtResource("3_e13i6")
|
||||
theme_override_colors/font_color = Color(1.0, 0.4, 0.4, 1.0)
|
||||
text = "Server Admin Panel"
|
||||
|
||||
[node name="LogoutBtn" type="Button" parent="MainLayout/LeftPanel/LeftVBox/AccountSection" unique_id=1741557407]
|
||||
@@ -271,7 +271,7 @@ unique_name_in_owner = true
|
||||
own_world_3d = true
|
||||
transparent_bg = true
|
||||
handle_input_locally = false
|
||||
size = Vector2i(746, 742)
|
||||
size = Vector2i(746, 720)
|
||||
render_target_update_mode = 4
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="MainLayout/RightPanel/PreviewViewport" unique_id=1213356390]
|
||||
@@ -328,3 +328,66 @@ offset_top = 4.0
|
||||
offset_right = 316.0
|
||||
offset_bottom = 216.0
|
||||
columns = 3
|
||||
|
||||
[node name="AccountSettingsDialog" type="AcceptDialog" parent="." unique_id=987000001]
|
||||
unique_name_in_owner = true
|
||||
title = "Account Settings"
|
||||
size = Vector2i(360, 480)
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="AccountSettingsDialog" unique_id=987000002]
|
||||
offset_left = 8.0
|
||||
offset_top = 8.0
|
||||
offset_right = 352.0
|
||||
offset_bottom = 431.0
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="CredTitle" type="Label" parent="AccountSettingsDialog/VBox" unique_id=987000003]
|
||||
layout_mode = 2
|
||||
text = "Update Credentials"
|
||||
|
||||
[node name="OldPassInput" type="LineEdit" parent="AccountSettingsDialog/VBox" unique_id=987000004]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
placeholder_text = "Current Password"
|
||||
secret = true
|
||||
|
||||
[node name="NewEmailInput" type="LineEdit" parent="AccountSettingsDialog/VBox" unique_id=987000005]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
placeholder_text = "New Email"
|
||||
|
||||
[node name="NewPassInput" type="LineEdit" parent="AccountSettingsDialog/VBox" unique_id=987000006]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
placeholder_text = "New Password"
|
||||
secret = true
|
||||
|
||||
[node name="SubmitCredBtn" type="Button" parent="AccountSettingsDialog/VBox" unique_id=987000007]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Submit Credentials"
|
||||
|
||||
[node name="HSeparator1" type="HSeparator" parent="AccountSettingsDialog/VBox" unique_id=987000008]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TzLabel" type="Label" parent="AccountSettingsDialog/VBox" unique_id=987000009]
|
||||
layout_mode = 2
|
||||
text = "Timezone"
|
||||
|
||||
[node name="TzDropdown" type="OptionButton" parent="AccountSettingsDialog/VBox" unique_id=987000010]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SaveTzBtn" type="Button" parent="AccountSettingsDialog/VBox" unique_id=987000011]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Save Timezone"
|
||||
|
||||
[node name="HSeparator2" type="HSeparator" parent="AccountSettingsDialog/VBox" unique_id=987000012]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ResetStatsBtn" type="Button" parent="AccountSettingsDialog/VBox" unique_id=987000013]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 0, 0, 1)
|
||||
text = "RESET ALL GAME STATS"
|
||||
|
||||
@@ -86,6 +86,11 @@ func load_profile() -> Dictionary:
|
||||
is_profile_loaded = true
|
||||
emit_signal("profile_loaded", profile)
|
||||
print("[UserProfileManager] Profile loaded: ", profile.display_name)
|
||||
|
||||
# Auto-sync existing score to native Nakama leaderboard in background
|
||||
if stats.get("high_score", 0) > 0 and NakamaManager.session and not AuthManager.is_guest:
|
||||
_submit_to_leaderboard.call_deferred()
|
||||
|
||||
return profile
|
||||
|
||||
func load_stats() -> Dictionary:
|
||||
@@ -135,15 +140,16 @@ func update_display_name(new_name: String) -> bool:
|
||||
emit_signal("profile_update_failed", "Name can only contain letters, numbers, and spaces")
|
||||
return false
|
||||
|
||||
# Allow guest name updates locally
|
||||
if not NakamaManager.session or AuthManager.is_guest:
|
||||
# Offline fallback (no session)
|
||||
if not NakamaManager.session:
|
||||
profile["display_name"] = new_name
|
||||
emit_signal("profile_updated")
|
||||
return true
|
||||
|
||||
var formatted_username = new_name.replace(" ", "_").to_lower()
|
||||
var result: NakamaAsyncResult = await NakamaManager.client.update_account_async(
|
||||
NakamaManager.session,
|
||||
null, # username (don't change)
|
||||
formatted_username, # username (sync to display name)
|
||||
new_name # display_name
|
||||
)
|
||||
|
||||
@@ -168,10 +174,24 @@ func update_avatar(avatar_index: int) -> bool:
|
||||
profile["avatar_index"] = avatar_index
|
||||
profile["avatar_url"] = AVATARS[avatar_index]
|
||||
|
||||
|
||||
# Update native avatar URL for the leaderboard
|
||||
var avatar_url = AVATARS[avatar_index]
|
||||
var result = await NakamaManager.client.update_account_async(
|
||||
NakamaManager.session, null, null, avatar_url
|
||||
)
|
||||
if result.is_exception():
|
||||
emit_signal("profile_update_failed", "Failed to update native avatar")
|
||||
return false
|
||||
|
||||
var success := await _save_profile_data()
|
||||
if success:
|
||||
emit_signal("avatar_changed", AVATARS[avatar_index])
|
||||
emit_signal("avatar_changed", avatar_url)
|
||||
emit_signal("profile_updated")
|
||||
|
||||
# Immediately update leaderboard with new avatar
|
||||
if stats.get("high_score", 0) > 0:
|
||||
_submit_to_leaderboard.call_deferred()
|
||||
|
||||
return success
|
||||
|
||||
@@ -255,6 +275,34 @@ func record_game_result(won: bool, score: int) -> void:
|
||||
stats["high_score"] = score
|
||||
|
||||
await update_stats(stats)
|
||||
|
||||
# Also submit to Nakama native leaderboard so global_high_score is populated
|
||||
await _submit_to_leaderboard()
|
||||
|
||||
func _submit_to_leaderboard() -> void:
|
||||
"""Submits the current high_score via server RPC (required for authoritative leaderboards)."""
|
||||
if not NakamaManager.session:
|
||||
return
|
||||
if stats.get("high_score", 0) <= 0:
|
||||
return
|
||||
|
||||
var payload = JSON.stringify({
|
||||
"score": int(stats.get("high_score", 0)),
|
||||
"games_played": int(stats.get("games_played", 0)),
|
||||
"games_won": int(stats.get("games_won", 0)),
|
||||
"avatar_url": profile.get("avatar_url", "")
|
||||
})
|
||||
|
||||
var result = await NakamaManager.client.rpc_async(
|
||||
NakamaManager.session,
|
||||
"submit_score",
|
||||
payload
|
||||
)
|
||||
|
||||
if result.is_exception():
|
||||
push_warning("[UserProfileManager] Leaderboard RPC failed: ", result.get_exception().message)
|
||||
else:
|
||||
print("[UserProfileManager] Leaderboard score submitted: ", stats.get("high_score", 0))
|
||||
|
||||
# =============================================================================
|
||||
# Getters
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
extends Control
|
||||
## Leaderboard panel — full-scene layout.
|
||||
## Leaderboard panel — reads from Nakama native leaderboard (global_high_score).
|
||||
## Left: sortable leaderboard list.
|
||||
## Right: 3D SubViewport character preview of the selected/top-ranked player.
|
||||
|
||||
@@ -10,6 +10,7 @@ signal closed
|
||||
# -------------------------------------------------------------------------
|
||||
@onready var back_btn := %BackBtn as Button
|
||||
@onready var refresh_btn := %RefreshBtn as Button
|
||||
@onready var sync_btn := %SyncBtn as Button
|
||||
@onready var sort_score_btn := %SortScoreBtn as Button
|
||||
@onready var sort_win_rate_btn := %SortWinRateBtn as Button
|
||||
@onready var sort_games_btn := %SortGamesBtn as Button
|
||||
@@ -41,6 +42,7 @@ const AVATAR_TO_CHAR: Array[String] = ["Pip", "Gatot", "Dabro", "Copper"]
|
||||
func _ready() -> void:
|
||||
back_btn.pressed.connect(_on_close_pressed)
|
||||
refresh_btn.pressed.connect(_fetch_leaderboard_data)
|
||||
sync_btn.pressed.connect(_on_sync_pressed)
|
||||
sort_score_btn.pressed.connect(func(): _sort_by("high_score"))
|
||||
sort_win_rate_btn.pressed.connect(func(): _sort_by("win_rate"))
|
||||
sort_games_btn.pressed.connect(func(): _sort_by("games_played"))
|
||||
@@ -52,12 +54,31 @@ func _ready() -> void:
|
||||
# -------------------------------------------------------------------------
|
||||
func show_panel() -> void:
|
||||
show()
|
||||
status_label.text = "Syncing scores..."
|
||||
# Bulk-sync all users' storage stats to native leaderboard (server-side operation)
|
||||
if NakamaManager.session:
|
||||
var sync_result = await NakamaManager.client.rpc_async(NakamaManager.session, "sync_leaderboard", "{}")
|
||||
if sync_result.is_exception():
|
||||
push_error("[Leaderboard] sync_leaderboard RPC failed: ", sync_result.get_exception().message)
|
||||
else:
|
||||
print("[Leaderboard] Server sync finished: ", sync_result.payload)
|
||||
_fetch_leaderboard_data()
|
||||
|
||||
func _on_close_pressed() -> void:
|
||||
hide()
|
||||
emit_signal("closed")
|
||||
|
||||
func _on_sync_pressed() -> void:
|
||||
"""Push the current player's stored stats up to the native Nakama leaderboard."""
|
||||
if not NakamaManager.session or AuthManager.is_guest:
|
||||
status_label.text = "Must be logged in to sync"
|
||||
return
|
||||
status_label.text = "Syncing your score..."
|
||||
await UserProfileManager._submit_to_leaderboard()
|
||||
status_label.text = "Synced! Refreshing..."
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
_fetch_leaderboard_data()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Data
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -66,11 +87,59 @@ func _fetch_leaderboard_data() -> void:
|
||||
status_label.text = "Not connected to Nakama"
|
||||
return
|
||||
|
||||
status_label.text = "Fetching Global Records..."
|
||||
status_label.text = "Fetching Leaderboard..."
|
||||
for child in leaderboard_list.get_children():
|
||||
child.queue_free()
|
||||
|
||||
# Calls the updated RPC that returns ONLY native global_high_score records
|
||||
# Try native Nakama leaderboard first (fastest, ranked already)
|
||||
var native_data = await _fetch_native_leaderboard()
|
||||
|
||||
if native_data.size() > 0:
|
||||
leaderboard_data = native_data
|
||||
_calculate_win_rates()
|
||||
status_label.text = ""
|
||||
_sort_by(current_sort_key)
|
||||
if leaderboard_data.size() > 0:
|
||||
_show_entry_preview(0)
|
||||
else:
|
||||
# Fallback: try the server RPC (reads same native leaderboard)
|
||||
await _fetch_via_rpc()
|
||||
|
||||
func _fetch_native_leaderboard() -> Array:
|
||||
"""Use the Nakama client API to list native leaderboard records directly."""
|
||||
var result = await NakamaManager.client.list_leaderboard_records_async(
|
||||
NakamaManager.session,
|
||||
"global_high_score",
|
||||
[], # no specific owner filter
|
||||
null, # expiry = null (no filter)
|
||||
100 # limit
|
||||
)
|
||||
|
||||
if result.is_exception():
|
||||
push_warning("[Leaderboard] Native API failed: ", result.get_exception().message)
|
||||
return []
|
||||
|
||||
var data: Array = []
|
||||
for record in result.records:
|
||||
var meta: Dictionary = {}
|
||||
if record.metadata and not record.metadata.is_empty():
|
||||
var parsed = JSON.parse_string(record.metadata)
|
||||
if parsed is Dictionary:
|
||||
meta = parsed
|
||||
|
||||
data.append({
|
||||
"user_id": record.owner_id,
|
||||
"display_name": record.username if (record.username and not record.username.is_empty()) else "Unknown",
|
||||
"avatar_url": meta.get("avatar_url", ""),
|
||||
"high_score": int(record.score),
|
||||
"games_played": int(meta.get("games_played", 0)),
|
||||
"games_won": int(meta.get("games_won", 0)),
|
||||
"rank": int(record.rank)
|
||||
})
|
||||
return data
|
||||
|
||||
func _fetch_via_rpc() -> void:
|
||||
"""Fallback: call server RPC which reads the same native leaderboard."""
|
||||
var result = await NakamaManager.client.rpc_async(NakamaManager.session, "get_leaderboard_stats", "{}")
|
||||
|
||||
if result.is_exception():
|
||||
@@ -78,20 +147,19 @@ func _fetch_leaderboard_data() -> void:
|
||||
push_error("[Leaderboard] RPC failed: ", result.get_exception().message)
|
||||
return
|
||||
|
||||
var json = JSON.new()
|
||||
var error = json.parse(result.payload)
|
||||
if error == OK:
|
||||
var json := JSON.new()
|
||||
if json.parse(result.payload) == OK:
|
||||
var data = json.get_data()
|
||||
if data.has("leaderboard"):
|
||||
if data.has("leaderboard") and data.leaderboard.size() > 0:
|
||||
leaderboard_data = data.leaderboard
|
||||
_calculate_win_rates()
|
||||
status_label.text = ""
|
||||
_sort_by(current_sort_key)
|
||||
# Show top player's character in 3D preview
|
||||
if leaderboard_data.size() > 0:
|
||||
_show_entry_preview(0)
|
||||
else:
|
||||
status_label.text = "No records found"
|
||||
# No records exist yet — show a helpful hint
|
||||
status_label.text = "No scores recorded yet.\nPlay a match to appear here!"
|
||||
else:
|
||||
status_label.text = "Error parsing server data"
|
||||
|
||||
@@ -122,7 +190,7 @@ func _populate_list() -> void:
|
||||
child.queue_free()
|
||||
|
||||
if leaderboard_data.size() == 0:
|
||||
status_label.text = "No players found"
|
||||
status_label.text = "No players found.\nPlay a match to appear here!"
|
||||
return
|
||||
|
||||
for i in range(leaderboard_data.size()):
|
||||
@@ -143,7 +211,7 @@ func _create_leaderboard_item(rank: int, entry: Dictionary, index: int) -> void:
|
||||
item.add_theme_stylebox_override("panel", style)
|
||||
|
||||
var hbox = HBoxContainer.new()
|
||||
hbox.theme_override_constants.separation = 16
|
||||
hbox.add_theme_constant_override("separation", 16)
|
||||
item.add_child(hbox)
|
||||
|
||||
# Rank
|
||||
|
||||
+36
-134
@@ -7,7 +7,6 @@ extends Control
|
||||
@onready var password_input := %PasswordInput as LineEdit
|
||||
@onready var remember_me := %RememberMe as CheckBox
|
||||
@onready var login_button := %LoginButton as Button
|
||||
@onready var register_link := %RegisterLink as LinkButton
|
||||
@onready var google_button := %GoogleButton as Button
|
||||
@onready var apple_button := %AppleButton as Button
|
||||
@onready var facebook_button := %FacebookButton as Button
|
||||
@@ -15,7 +14,7 @@ extends Control
|
||||
@onready var loading_spinner := %LoadingSpinner as TextureProgressBar
|
||||
|
||||
# Registration panel elements
|
||||
@onready var registration_panel := %RegistrationPanel as PanelContainer
|
||||
@onready var tab_container := %TabContainer as TabContainer
|
||||
@onready var reg_email_input := %RegEmailInput as LineEdit
|
||||
@onready var reg_username_input := %RegUsernameInput as LineEdit
|
||||
@onready var reg_password_input := %RegPasswordInput as LineEdit
|
||||
@@ -25,25 +24,33 @@ extends Control
|
||||
@onready var reg_captcha_question := %RegCaptchaQuestion as Label
|
||||
@onready var reg_captcha_input := %RegCaptchaInput as LineEdit
|
||||
@onready var register_button := %RegisterButton as Button
|
||||
@onready var back_to_login_link := %BackToLoginLink as LinkButton
|
||||
@onready var reg_status_label := %RegStatusLabel as Label
|
||||
|
||||
var current_captcha_answer: int = 0
|
||||
|
||||
# Main panel reference
|
||||
@onready var main_panel := $CenterContainer/MainPanel as PanelContainer
|
||||
|
||||
var is_loading: bool = false
|
||||
|
||||
# Server Selection Controls
|
||||
var server_option: OptionButton
|
||||
var server_ip_input: LineEdit
|
||||
var lan_section: VBoxContainer # LAN-specific controls
|
||||
@onready var server_option := %ServerOption as OptionButton
|
||||
@onready var server_ip_input := %ServerIPInput as LineEdit
|
||||
@onready var lan_section := %LANSection as VBoxContainer
|
||||
@onready var lan_host_btn := %LANHostBtn as Button
|
||||
@onready var lan_ip := %LANIPInput as LineEdit
|
||||
@onready var lan_join_btn := %LANJoinBtn as Button
|
||||
|
||||
func _ready() -> void:
|
||||
_connect_signals()
|
||||
_setup_ui()
|
||||
_setup_server_config_ui()
|
||||
|
||||
# Initialize connection mode view
|
||||
if NakamaManager.nakama_host == "localhost":
|
||||
server_option.selected = 0
|
||||
elif NakamaManager.nakama_host == "tektondash.vps.webdock.cloud":
|
||||
server_option.selected = 3
|
||||
else:
|
||||
server_option.selected = 1
|
||||
server_ip_input.text = NakamaManager.nakama_host if NakamaManager.nakama_host != "localhost" else "127.0.0.1"
|
||||
_on_server_option_selected(server_option.selected)
|
||||
|
||||
# Check if already authenticated
|
||||
if AuthManager.is_logged_in():
|
||||
@@ -53,7 +60,6 @@ func _connect_signals() -> void:
|
||||
# Login buttons
|
||||
guest_button.pressed.connect(_on_guest_pressed)
|
||||
login_button.pressed.connect(_on_login_pressed)
|
||||
register_link.pressed.connect(_show_registration)
|
||||
|
||||
# Social buttons
|
||||
google_button.pressed.connect(_on_google_pressed)
|
||||
@@ -62,7 +68,6 @@ func _connect_signals() -> void:
|
||||
|
||||
# Registration buttons
|
||||
register_button.pressed.connect(_on_register_pressed)
|
||||
back_to_login_link.pressed.connect(_show_login)
|
||||
|
||||
# Password strength checker
|
||||
reg_password_input.text_changed.connect(_check_password_strength)
|
||||
@@ -73,6 +78,13 @@ func _connect_signals() -> void:
|
||||
AuthManager.auth_failed.connect(_on_auth_failed)
|
||||
AuthManager.session_restored.connect(_on_session_restored)
|
||||
|
||||
tab_container.tab_changed.connect(_on_tab_changed)
|
||||
server_option.item_selected.connect(_on_server_option_selected)
|
||||
server_ip_input.text_submitted.connect(_on_server_ip_submitted)
|
||||
server_ip_input.focus_exited.connect(func(): _on_server_ip_submitted(server_ip_input.text))
|
||||
lan_host_btn.pressed.connect(_on_lan_host_pressed)
|
||||
lan_join_btn.pressed.connect(func(): _on_lan_join_pressed(lan_ip.text))
|
||||
|
||||
# Enter key to submit
|
||||
password_input.text_submitted.connect(func(_t): _on_login_pressed())
|
||||
reg_confirm_password_input.text_submitted.connect(func(_t): _on_register_pressed())
|
||||
@@ -81,8 +93,7 @@ func _setup_ui() -> void:
|
||||
status_label.text = ""
|
||||
reg_status_label.text = ""
|
||||
loading_spinner.visible = false
|
||||
registration_panel.visible = false
|
||||
main_panel.visible = true
|
||||
tab_container.current_tab = 0
|
||||
|
||||
# Hide social buttons on platforms where they're not supported
|
||||
_configure_social_buttons()
|
||||
@@ -102,12 +113,14 @@ func _configure_social_buttons() -> void:
|
||||
# Panel Switching
|
||||
# =============================================================================
|
||||
|
||||
func _show_registration() -> void:
|
||||
main_panel.visible = false
|
||||
registration_panel.visible = true
|
||||
reg_status_label.text = ""
|
||||
_generate_captcha()
|
||||
reg_email_input.grab_focus()
|
||||
func _on_tab_changed(tab: int) -> void:
|
||||
if tab == 0:
|
||||
status_label.text = ""
|
||||
email_input.grab_focus()
|
||||
elif tab == 1:
|
||||
reg_status_label.text = ""
|
||||
_generate_captcha()
|
||||
reg_email_input.grab_focus()
|
||||
|
||||
func _generate_captcha() -> void:
|
||||
var num1 := randi_range(1, 10)
|
||||
@@ -116,12 +129,6 @@ func _generate_captcha() -> void:
|
||||
reg_captcha_question.text = "Security Check: %d + %d = ?" % [num1, num2]
|
||||
reg_captcha_input.text = ""
|
||||
|
||||
func _show_login() -> void:
|
||||
registration_panel.visible = false
|
||||
main_panel.visible = true
|
||||
status_label.text = ""
|
||||
email_input.grab_focus()
|
||||
|
||||
# =============================================================================
|
||||
# Login Handlers
|
||||
# =============================================================================
|
||||
@@ -183,112 +190,7 @@ func _on_facebook_pressed() -> void:
|
||||
# When you have the access token from Facebook SDK:
|
||||
# AuthManager.login_with_facebook(access_token)
|
||||
|
||||
func _setup_server_config_ui() -> void:
|
||||
"""Inject server configuration controls into MainPanel."""
|
||||
if not main_panel: return
|
||||
|
||||
var vbox = main_panel.get_node_or_null("VBox")
|
||||
if not vbox: return
|
||||
|
||||
# Find where to insert (before GuestButton)
|
||||
var insert_pos = 3 # Default position
|
||||
if guest_button:
|
||||
insert_pos = guest_button.get_index()
|
||||
|
||||
# Create Server Section
|
||||
var server_section = VBoxContainer.new()
|
||||
server_section.name = "ServerSelectionSection"
|
||||
server_section.add_theme_constant_override("separation", 10)
|
||||
vbox.add_child(server_section)
|
||||
vbox.move_child(server_section, insert_pos)
|
||||
|
||||
# Server Label
|
||||
var label = Label.new()
|
||||
label.text = "CONNECTION MODE"
|
||||
label.add_theme_color_override("font_color", Color(0.69, 0.529, 0.357, 1))
|
||||
label.add_theme_font_size_override("font_size", 13)
|
||||
server_section.add_child(label)
|
||||
|
||||
# Server OptionButton
|
||||
server_option = OptionButton.new()
|
||||
server_option.name = "ServerOption"
|
||||
server_option.custom_minimum_size = Vector2(0, 44)
|
||||
server_option.add_item("Nakama - Localhost (Testing)")
|
||||
server_option.add_item("Nakama - Remote Server (Host IP)")
|
||||
server_option.add_item("LAN Direct (No Server)")
|
||||
server_option.add_item("Nakama - Tekton Dash EU")
|
||||
|
||||
# Set initial state based on NakamaManager
|
||||
if NakamaManager.nakama_host == "localhost":
|
||||
server_option.selected = 0
|
||||
elif NakamaManager.nakama_host == "tektondash.vps.webdock.cloud":
|
||||
server_option.selected = 3
|
||||
else:
|
||||
server_option.selected = 1
|
||||
|
||||
server_option.item_selected.connect(_on_server_option_selected)
|
||||
server_section.add_child(server_option)
|
||||
|
||||
# Nakama Server IP Input
|
||||
server_ip_input = LineEdit.new()
|
||||
server_ip_input.name = "ServerIPInput"
|
||||
server_ip_input.custom_minimum_size = Vector2(0, 44)
|
||||
server_ip_input.placeholder_text = "Enter Nakama Server IP..."
|
||||
server_ip_input.text = NakamaManager.nakama_host if NakamaManager.nakama_host != "localhost" else "127.0.0.1"
|
||||
server_ip_input.visible = server_option.selected == 1
|
||||
server_ip_input.text_submitted.connect(_on_server_ip_submitted)
|
||||
server_ip_input.focus_exited.connect(func(): _on_server_ip_submitted(server_ip_input.text))
|
||||
server_section.add_child(server_ip_input)
|
||||
|
||||
# --- LAN Section ---
|
||||
lan_section = VBoxContainer.new()
|
||||
lan_section.name = "LANSection"
|
||||
lan_section.add_theme_constant_override("separation", 8)
|
||||
lan_section.visible = false
|
||||
server_section.add_child(lan_section)
|
||||
|
||||
var lan_info = Label.new()
|
||||
lan_info.text = "Play over LAN without any server.\nFirewall may need to allow port 7777."
|
||||
lan_info.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7, 1))
|
||||
lan_info.add_theme_font_size_override("font_size", 12)
|
||||
lan_info.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
lan_section.add_child(lan_info)
|
||||
|
||||
# Host LAN button
|
||||
var lan_host_btn = Button.new()
|
||||
lan_host_btn.name = "LANHostBtn"
|
||||
lan_host_btn.text = "HOST LAN GAME"
|
||||
lan_host_btn.custom_minimum_size = Vector2(0, 44)
|
||||
lan_host_btn.pressed.connect(_on_lan_host_pressed)
|
||||
lan_section.add_child(lan_host_btn)
|
||||
|
||||
var lan_sep = Label.new()
|
||||
lan_sep.text = "── or join a friend ──"
|
||||
lan_sep.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
lan_sep.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5, 1))
|
||||
lan_sep.add_theme_font_size_override("font_size", 11)
|
||||
lan_section.add_child(lan_sep)
|
||||
|
||||
# LAN Host IP input
|
||||
var lan_ip = LineEdit.new()
|
||||
lan_ip.name = "LANIPInput"
|
||||
lan_ip.custom_minimum_size = Vector2(0, 44)
|
||||
lan_ip.placeholder_text = "Host IP (e.g. 192.168.1.10)"
|
||||
lan_ip.text = "127.0.0.1"
|
||||
lan_section.add_child(lan_ip)
|
||||
|
||||
# Join LAN button
|
||||
var lan_join_btn = Button.new()
|
||||
lan_join_btn.name = "LANJoinBtn"
|
||||
lan_join_btn.text = "JOIN LAN GAME"
|
||||
lan_join_btn.custom_minimum_size = Vector2(0, 44)
|
||||
lan_join_btn.pressed.connect(func(): _on_lan_join_pressed(lan_ip.text))
|
||||
lan_section.add_child(lan_join_btn)
|
||||
|
||||
# Add a separator after the section
|
||||
var separator = HSeparator.new()
|
||||
vbox.add_child(separator)
|
||||
vbox.move_child(separator, insert_pos + 1)
|
||||
|
||||
|
||||
func _on_server_option_selected(index: int) -> void:
|
||||
if index == 0:
|
||||
@@ -322,7 +224,7 @@ func _on_lan_host_pressed() -> void:
|
||||
player_name = "Host"
|
||||
LobbyManager.local_player_name = player_name
|
||||
|
||||
var ok = await LobbyManager.create_room_lan()
|
||||
var ok = LobbyManager.create_room_lan()
|
||||
if ok:
|
||||
_go_to_lobby()
|
||||
else:
|
||||
@@ -490,7 +392,7 @@ func _on_auth_failed(error: String) -> void:
|
||||
loading_spinner.visible = false
|
||||
_set_inputs_enabled(true)
|
||||
|
||||
if registration_panel.visible:
|
||||
if tab_container.current_tab == 1:
|
||||
_show_reg_error(error)
|
||||
else:
|
||||
_show_error(error)
|
||||
|
||||
@@ -25,6 +25,16 @@ signal profile_updated
|
||||
@onready var avatar_popup := %AvatarSelectionPopup as PopupPanel
|
||||
@onready var avatar_grid := %GridContainer as GridContainer
|
||||
|
||||
# Account Settings refs
|
||||
@onready var acc_settings_dialog := %AccountSettingsDialog as AcceptDialog
|
||||
@onready var old_pass_input := %OldPassInput as LineEdit
|
||||
@onready var new_email_input := %NewEmailInput as LineEdit
|
||||
@onready var new_pass_input := %NewPassInput as LineEdit
|
||||
@onready var submit_cred_btn := %SubmitCredBtn as Button
|
||||
@onready var tz_dropdown := %TzDropdown as OptionButton
|
||||
@onready var save_tz_btn := %SaveTzBtn as Button
|
||||
@onready var reset_stats_btn := %ResetStatsBtn as Button
|
||||
|
||||
# Loadout refs
|
||||
@onready var char_left_btn := %CharLeftBtn as Button
|
||||
@onready var char_right_btn := %CharRightBtn as Button
|
||||
@@ -55,6 +65,7 @@ func _ready() -> void:
|
||||
_connect_signals()
|
||||
_load_profile_data()
|
||||
_setup_avatar_grid()
|
||||
_setup_account_settings_ui()
|
||||
_load_loadout()
|
||||
_setup_3d_preview()
|
||||
|
||||
@@ -71,6 +82,17 @@ func _connect_signals() -> void:
|
||||
|
||||
UserProfileManager.profile_updated.connect(_on_profile_updated)
|
||||
UserProfileManager.profile_update_failed.connect(_on_profile_update_failed)
|
||||
|
||||
# Dynamically inject Account Settings button
|
||||
var acc_settings_btn = Button.new()
|
||||
acc_settings_btn.text = "Account Settings"
|
||||
acc_settings_btn.custom_minimum_size = Vector2(0, 44)
|
||||
acc_settings_btn.add_theme_font_override("font", load("res://assets/fonts/Nougat-ExtraBlack.ttf"))
|
||||
acc_settings_btn.pressed.connect(_open_account_settings)
|
||||
# Insert it before Logout button
|
||||
var logout_idx = logout_btn.get_index()
|
||||
logout_btn.get_parent().add_child(acc_settings_btn)
|
||||
logout_btn.get_parent().move_child(acc_settings_btn, logout_idx)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Profile
|
||||
@@ -279,6 +301,63 @@ func _on_link_account_pressed() -> void:
|
||||
dialog.queue_free()
|
||||
)
|
||||
|
||||
func _setup_account_settings_ui() -> void:
|
||||
# Populate Timezone dropdown
|
||||
tz_dropdown.clear()
|
||||
for i in range(-12, 15):
|
||||
var prefix = "+" if i >= 0 else ""
|
||||
tz_dropdown.add_item("GMT " + prefix + str(i))
|
||||
|
||||
# Connect buttons
|
||||
submit_cred_btn.pressed.connect(func():
|
||||
status_label.text = "Updating credentials..."
|
||||
var payload = {
|
||||
"current_password": old_pass_input.text,
|
||||
"new_email": new_email_input.text,
|
||||
"new_password": new_pass_input.text
|
||||
}
|
||||
var result = await NakamaManager.client.rpc_async(NakamaManager.session, "change_credentials", JSON.stringify(payload))
|
||||
if result.is_exception():
|
||||
status_label.text = "Failed: " + result.get_exception().message
|
||||
else:
|
||||
status_label.text = "Credentials updated successfully!"
|
||||
acc_settings_dialog.hide()
|
||||
)
|
||||
|
||||
save_tz_btn.pressed.connect(func():
|
||||
var selected_text = tz_dropdown.get_item_text(tz_dropdown.selected)
|
||||
var res = await NakamaManager.client.update_account_async(NakamaManager.session, null, null, null, null, null, selected_text)
|
||||
if res.is_exception():
|
||||
status_label.text = "TZ Failed: " + res.get_exception().message
|
||||
else:
|
||||
status_label.text = "Timezone saved!"
|
||||
)
|
||||
|
||||
reset_stats_btn.pressed.connect(func():
|
||||
var conf = ConfirmationDialog.new()
|
||||
conf.dialog_text = "Are you SURE you want to irreversibly wipe all your stats to 0?"
|
||||
add_child(conf)
|
||||
conf.popup_centered()
|
||||
conf.confirmed.connect(func():
|
||||
var r = await NakamaManager.client.rpc_async(NakamaManager.session, "reset_stats", "{}")
|
||||
if not r.is_exception():
|
||||
UserProfileManager.stats = {
|
||||
"games_played": 0, "games_won": 0, "games_lost": 0, "total_score": 0, "high_score": 0, "play_time_minutes": 0
|
||||
}
|
||||
_load_profile_data()
|
||||
status_label.text = "Stats wiped completely."
|
||||
conf.queue_free()
|
||||
acc_settings_dialog.hide()
|
||||
)
|
||||
)
|
||||
|
||||
func _open_account_settings() -> void:
|
||||
old_pass_input.visible = not AuthManager.is_guest
|
||||
old_pass_input.text = ""
|
||||
new_email_input.text = ""
|
||||
new_pass_input.text = ""
|
||||
acc_settings_dialog.popup_centered()
|
||||
|
||||
func _on_logout_pressed() -> void:
|
||||
AuthManager.logout()
|
||||
get_tree().change_scene_to_file("res://scenes/ui/login_screen.tscn")
|
||||
@@ -301,6 +380,10 @@ func show_panel() -> void:
|
||||
_load_loadout()
|
||||
_check_admin_visibility()
|
||||
show()
|
||||
|
||||
if AuthManager.is_guest:
|
||||
_on_link_account_pressed()
|
||||
status_label.text = "Please link an email to save your progress permanently!"
|
||||
|
||||
func _check_admin_visibility() -> void:
|
||||
admin_panel_btn.hide()
|
||||
|
||||
@@ -28,10 +28,15 @@ function InitModule(ctx, logger, nk, initializer) {
|
||||
initializer.registerRpc("admin_update_stats", rpcAdminUpdateStats);
|
||||
initializer.registerRpc("admin_delete_stats", rpcAdminDeleteStats);
|
||||
initializer.registerRpc("admin_sync_leaderboard", rpcAdminSyncLeaderboard);
|
||||
// Client-accessible score submission (authoritative leaderboard requires server-side writes)
|
||||
initializer.registerRpc("submit_score", rpcSubmitScore);
|
||||
initializer.registerRpc("sync_leaderboard", rpcSyncLeaderboard);
|
||||
initializer.registerRpc("change_credentials", rpcChangeCredentials);
|
||||
initializer.registerRpc("reset_stats", rpcResetStats);
|
||||
|
||||
// Create default native leaderboard
|
||||
// id: "global_high_score", authoritative: true, sort: "desc", operator: "best", reset: None
|
||||
nk.leaderboardCreate("global_high_score", true, "desc", "best", null, {});
|
||||
try { nk.leaderboardCreate("global_high_score", true, "desc", "best", null, {}); } catch(e) {}
|
||||
|
||||
logger.info("Tekton admin module loaded");
|
||||
}
|
||||
@@ -477,6 +482,144 @@ function rpcGetLeaderboardStats(ctx, logger, nk, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
// Any authenticated user can submit their own score (server writes on their behalf)
|
||||
function rpcSubmitScore(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
try {
|
||||
var request = JSON.parse(payload || "{}");
|
||||
var score = parseInt(request.score) || 0;
|
||||
var account = nk.accountGetId(ctx.userId);
|
||||
var metadata = {
|
||||
games_played: request.games_played || 0,
|
||||
games_won: request.games_won || 0,
|
||||
avatar_url: account.user.avatarUrl || request.avatar_url || ""
|
||||
};
|
||||
nk.leaderboardRecordWrite(
|
||||
"global_high_score",
|
||||
ctx.userId,
|
||||
account.user.username,
|
||||
score,
|
||||
0,
|
||||
metadata
|
||||
);
|
||||
logger.info("Score submitted for user " + ctx.userId + ": " + score);
|
||||
return JSON.stringify({ success: true });
|
||||
} catch (e) {
|
||||
logger.error("Failed to submit score for " + ctx.userId + ": " + e);
|
||||
throw new Error("Failed to submit score: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
// Any authenticated user can trigger a bulk sync (reads all public stats, populates leaderboard)
|
||||
function rpcSyncLeaderboard(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
try {
|
||||
var result = nk.storageList(null, "stats", 100, "");
|
||||
var statsObjects = result.objects || [];
|
||||
var userGroup = {};
|
||||
|
||||
for (var i = 0; i < statsObjects.length; i++) {
|
||||
var obj = statsObjects[i];
|
||||
var userId = obj.userId;
|
||||
var value;
|
||||
try {
|
||||
// obj.value may already be an object or a string depending on Nakama version
|
||||
value = (typeof obj.value === "string") ? JSON.parse(obj.value) : obj.value;
|
||||
} catch (e) { continue; }
|
||||
if (!value) continue;
|
||||
|
||||
if (!userGroup[userId]) {
|
||||
userGroup[userId] = {
|
||||
high_score: value.high_score || 0,
|
||||
games_played: value.games_played || 0,
|
||||
games_won: value.games_won || 0,
|
||||
avatar_url: value.avatar_url || ""
|
||||
};
|
||||
} else {
|
||||
userGroup[userId].high_score = Math.max(userGroup[userId].high_score, value.high_score || 0);
|
||||
userGroup[userId].games_played = Math.max(userGroup[userId].games_played, value.games_played || 0);
|
||||
userGroup[userId].games_won = Math.max(userGroup[userId].games_won, value.games_won || 0);
|
||||
}
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
var debugLogs = [];
|
||||
for (var uid in userGroup) {
|
||||
try {
|
||||
var stats = userGroup[uid];
|
||||
var account = nk.accountGetId(uid);
|
||||
var meta = {
|
||||
games_played: stats.games_played || 0,
|
||||
games_won: stats.games_won || 0,
|
||||
avatar_url: stats.avatar_url || account.user.avatarUrl || ""
|
||||
};
|
||||
nk.leaderboardRecordWrite("global_high_score", uid, account.user.username, stats.high_score, 0, meta);
|
||||
count++;
|
||||
} catch (inner) {
|
||||
debugLogs.push("Error user " + uid + ": " + inner);
|
||||
logger.error("Failed to sync record for " + uid + ": " + inner);
|
||||
}
|
||||
}
|
||||
logger.info("Synced " + count + " records to leaderboard by user " + ctx.userId);
|
||||
return JSON.stringify({ success: true, synced: count, objects_found: statsObjects.length, debug: debugLogs });
|
||||
} catch (e) {
|
||||
logger.error("Leaderboard sync failed: " + e);
|
||||
throw new Error("Sync failed: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
// Change Email / Password securely
|
||||
function rpcChangeCredentials(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
var req = {};
|
||||
try { req = JSON.parse(payload || "{}"); } catch (e) {}
|
||||
|
||||
var account = nk.accountGetId(ctx.userId);
|
||||
|
||||
// If not a guest (has email), verify current password and unlink
|
||||
if (account.email) {
|
||||
if (!req.current_password) throw new Error("Current password required");
|
||||
try {
|
||||
nk.authenticateEmail(account.email, req.current_password, false);
|
||||
} catch (e) {
|
||||
throw new Error("Incorrect current password.");
|
||||
}
|
||||
nk.unlinkEmail(ctx.userId, account.email, req.current_password);
|
||||
}
|
||||
|
||||
try {
|
||||
nk.linkEmail(ctx.userId, req.new_email, req.new_password);
|
||||
} catch (e) {
|
||||
// Safe rollback
|
||||
if (account.email) nk.linkEmail(ctx.userId, account.email, req.current_password);
|
||||
throw new Error("Failed to set new credentials: " + e.message);
|
||||
}
|
||||
|
||||
return JSON.stringify({ success: true });
|
||||
}
|
||||
|
||||
// Reset Game Stats
|
||||
function rpcResetStats(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
var account = nk.accountGetId(ctx.userId);
|
||||
|
||||
// Delete native leaderboard rank
|
||||
try { nk.leaderboardRecordDelete("global_high_score", ctx.userId); } catch (e) {}
|
||||
|
||||
// Wipe storage stats
|
||||
var zeros = { games_played: 0, games_won: 0, high_score: 0, total_kills: 0, total_deaths: 0 };
|
||||
nk.storageWrite([{
|
||||
collection: "stats",
|
||||
key: "game_stats",
|
||||
userId: ctx.userId,
|
||||
value: zeros,
|
||||
permissionRead: 2,
|
||||
permissionWrite: 1
|
||||
}]);
|
||||
|
||||
return JSON.stringify({ success: true });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Admin User Management RPCs
|
||||
// =============================================================================
|
||||
@@ -680,7 +823,7 @@ function rpcAdminSyncLeaderboard(ctx, logger, nk, payload) {
|
||||
avatar_url: stats.avatar_url || account.user.avatarUrl || ""
|
||||
};
|
||||
|
||||
nk.leaderboardRecordWrite("global_high_score", userId, account.user.username, stats.high_score, 0, JSON.stringify(metadata));
|
||||
nk.leaderboardRecordWrite("global_high_score", userId, account.user.username, stats.high_score, 0, metadata);
|
||||
count++;
|
||||
} catch (inner) {
|
||||
logger.error("Failed to sync merged record for " + userId + ": " + inner);
|
||||
|
||||
@@ -1,699 +0,0 @@
|
||||
/**
|
||||
* Tekton Nakama Server Runtime Module
|
||||
*
|
||||
* This module provides secure admin operations via RPC calls.
|
||||
* Deploy this to your Nakama server's runtime directory.
|
||||
*
|
||||
* For TypeScript modules, compile to JavaScript and place in:
|
||||
* - data/modules/ (for Nakama Docker)
|
||||
* - Or configure in nakama config.yml
|
||||
*/
|
||||
|
||||
// Initialize RPC endpoints
|
||||
function InitModule(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
initializer: nkruntime.Initializer
|
||||
) {
|
||||
// Admin RPCs
|
||||
initializer.registerRpc("admin_kick_player", rpcAdminKickPlayer);
|
||||
initializer.registerRpc("admin_ban_player", rpcAdminBanPlayer);
|
||||
initializer.registerRpc("admin_unban_player", rpcAdminUnbanPlayer);
|
||||
initializer.registerRpc("admin_get_ban_list", rpcAdminGetBanList);
|
||||
initializer.registerRpc("admin_get_server_stats", rpcAdminGetServerStats);
|
||||
initializer.registerRpc("admin_get_player_list", rpcAdminGetPlayerList);
|
||||
initializer.registerRpc("admin_end_match", rpcAdminEndMatch);
|
||||
initializer.registerRpc("admin_set_user_role", rpcAdminSetUserRole);
|
||||
initializer.registerRpc("admin_list_users", rpcAdminListUsers);
|
||||
initializer.registerRpc("admin_delete_users", rpcAdminDeleteUsers);
|
||||
|
||||
// User management RPCs
|
||||
initializer.registerRpc("get_user_profile", rpcGetUserProfile);
|
||||
initializer.registerRpc("update_user_profile", rpcUpdateUserProfile);
|
||||
|
||||
// Leaderboard RPCs
|
||||
initializer.registerRpc("get_leaderboard_stats", rpcGetLeaderboardStats);
|
||||
|
||||
logger.info("Tekton admin module loaded");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Authorization Helpers
|
||||
// =============================================================================
|
||||
|
||||
interface UserMetadata {
|
||||
role?: string;
|
||||
banned?: boolean;
|
||||
ban_reason?: string;
|
||||
ban_expires?: string;
|
||||
}
|
||||
|
||||
const ADMIN_ROLES = ["admin", "moderator", "owner"];
|
||||
|
||||
function isAdmin(ctx: nkruntime.Context, nk: nkruntime.Nakama): boolean {
|
||||
if (!ctx.userId) return false;
|
||||
|
||||
try {
|
||||
const account = nk.accountGetId(ctx.userId);
|
||||
const metadata = JSON.parse(account.user.metadata || "{}") as UserMetadata;
|
||||
return ADMIN_ROLES.includes(metadata.role || "");
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isMatchHost(
|
||||
ctx: nkruntime.Context,
|
||||
nk: nkruntime.Nakama,
|
||||
matchId: string
|
||||
): boolean {
|
||||
if (!ctx.userId || !matchId) return false;
|
||||
|
||||
try {
|
||||
// Get match state to check host
|
||||
const match = nk.matchGet(matchId);
|
||||
if (!match) return false;
|
||||
|
||||
// The first user to join (presence) is typically the host
|
||||
// This logic may need adjustment based on your match handler
|
||||
const state = JSON.parse(match.state || "{}");
|
||||
return state.hostUserId === ctx.userId;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function requireAdmin(ctx: nkruntime.Context, nk: nkruntime.Nakama): void {
|
||||
if (!isAdmin(ctx, nk)) {
|
||||
throw new Error("Admin privileges required");
|
||||
}
|
||||
}
|
||||
|
||||
function requireAdminOrHost(
|
||||
ctx: nkruntime.Context,
|
||||
nk: nkruntime.Nakama,
|
||||
matchId: string
|
||||
): void {
|
||||
if (!isAdmin(ctx, nk) && !isMatchHost(ctx, nk, matchId)) {
|
||||
throw new Error("Admin or host privileges required");
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Admin RPCs
|
||||
// =============================================================================
|
||||
|
||||
interface KickPlayerRequest {
|
||||
match_id: string;
|
||||
user_id: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
function rpcAdminKickPlayer(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
payload: string
|
||||
): string {
|
||||
const request = JSON.parse(payload) as KickPlayerRequest;
|
||||
|
||||
requireAdminOrHost(ctx, nk, request.match_id);
|
||||
|
||||
// Can't kick yourself
|
||||
if (request.user_id === ctx.userId) {
|
||||
throw new Error("Cannot kick yourself");
|
||||
}
|
||||
|
||||
try {
|
||||
// Signal the match to kick the player
|
||||
nk.matchSignal(request.match_id, JSON.stringify({
|
||||
action: "kick",
|
||||
user_id: request.user_id,
|
||||
reason: request.reason || "Kicked by admin"
|
||||
}));
|
||||
|
||||
logger.info(`Player ${request.user_id} kicked from match ${request.match_id} by ${ctx.userId}`);
|
||||
|
||||
return JSON.stringify({ success: true });
|
||||
} catch (e) {
|
||||
logger.error(`Failed to kick player: ${e}`);
|
||||
throw new Error("Failed to kick player");
|
||||
}
|
||||
}
|
||||
|
||||
interface BanPlayerRequest {
|
||||
user_id: string;
|
||||
reason?: string;
|
||||
duration_hours?: number; // 0 = permanent
|
||||
match_id?: string; // Optional: also kick from current match
|
||||
}
|
||||
|
||||
function rpcAdminBanPlayer(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
payload: string
|
||||
): string {
|
||||
const request = JSON.parse(payload) as BanPlayerRequest;
|
||||
|
||||
// Only full admins can ban (not just match hosts)
|
||||
requireAdmin(ctx, nk);
|
||||
|
||||
if (request.user_id === ctx.userId) {
|
||||
throw new Error("Cannot ban yourself");
|
||||
}
|
||||
|
||||
try {
|
||||
// Get target user's account
|
||||
const targetAccount = nk.accountGetId(request.user_id);
|
||||
const metadata = JSON.parse(targetAccount.user.metadata || "{}") as UserMetadata;
|
||||
|
||||
// Don't allow banning other admins
|
||||
if (ADMIN_ROLES.includes(metadata.role || "")) {
|
||||
throw new Error("Cannot ban an admin");
|
||||
}
|
||||
|
||||
// Set ban in metadata
|
||||
const banExpires = request.duration_hours && request.duration_hours > 0
|
||||
? new Date(Date.now() + request.duration_hours * 60 * 60 * 1000).toISOString()
|
||||
: null;
|
||||
|
||||
metadata.banned = true;
|
||||
metadata.ban_reason = request.reason || "Banned by admin";
|
||||
if (banExpires) {
|
||||
metadata.ban_expires = banExpires;
|
||||
}
|
||||
|
||||
nk.accountUpdateId(request.user_id, null, null, null, null, null, null, JSON.stringify(metadata));
|
||||
|
||||
// Also kick from current match if specified
|
||||
if (request.match_id) {
|
||||
nk.matchSignal(request.match_id, JSON.stringify({
|
||||
action: "kick",
|
||||
user_id: request.user_id,
|
||||
reason: "Banned: " + (request.reason || "")
|
||||
}));
|
||||
}
|
||||
|
||||
// Store in ban list (for quick lookup)
|
||||
const banRecord = {
|
||||
user_id: request.user_id,
|
||||
username: targetAccount.user.username,
|
||||
banned_by: ctx.userId,
|
||||
banned_at: new Date().toISOString(),
|
||||
reason: request.reason,
|
||||
expires: banExpires
|
||||
};
|
||||
|
||||
nk.storageWrite([{
|
||||
collection: "bans",
|
||||
key: request.user_id,
|
||||
userId: "00000000-0000-0000-0000-000000000000", // System-owned
|
||||
value: banRecord,
|
||||
permissionRead: 2, // Public read
|
||||
permissionWrite: 0 // No one can write (except server)
|
||||
}]);
|
||||
|
||||
logger.warn(`Player ${request.user_id} banned by ${ctx.userId}. Reason: ${request.reason}`);
|
||||
|
||||
return JSON.stringify({ success: true, ban: banRecord });
|
||||
} catch (e) {
|
||||
logger.error(`Failed to ban player: ${e}`);
|
||||
throw new Error(`Failed to ban player: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
interface UnbanPlayerRequest {
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
function rpcAdminUnbanPlayer(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
payload: string
|
||||
): string {
|
||||
const request = JSON.parse(payload) as UnbanPlayerRequest;
|
||||
|
||||
requireAdmin(ctx, nk);
|
||||
|
||||
try {
|
||||
// Get target user's account
|
||||
const targetAccount = nk.accountGetId(request.user_id);
|
||||
const metadata = JSON.parse(targetAccount.user.metadata || "{}") as UserMetadata;
|
||||
|
||||
// Remove ban
|
||||
delete metadata.banned;
|
||||
delete metadata.ban_reason;
|
||||
delete metadata.ban_expires;
|
||||
|
||||
nk.accountUpdateId(request.user_id, null, null, null, null, null, null, JSON.stringify(metadata));
|
||||
|
||||
// Remove from ban list
|
||||
nk.storageDelete([{
|
||||
collection: "bans",
|
||||
key: request.user_id,
|
||||
userId: "00000000-0000-0000-0000-000000000000"
|
||||
}]);
|
||||
|
||||
logger.info(`Player ${request.user_id} unbanned by ${ctx.userId}`);
|
||||
|
||||
return JSON.stringify({ success: true });
|
||||
} catch (e) {
|
||||
logger.error(`Failed to unban player: ${e}`);
|
||||
throw new Error("Failed to unban player");
|
||||
}
|
||||
}
|
||||
|
||||
function rpcAdminGetBanList(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
payload: string
|
||||
): string {
|
||||
requireAdmin(ctx, nk);
|
||||
|
||||
try {
|
||||
const result = nk.storageList(
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
"bans",
|
||||
100,
|
||||
""
|
||||
);
|
||||
|
||||
const bans = result.objects?.map(obj => obj.value) || [];
|
||||
|
||||
return JSON.stringify({ bans });
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get ban list: ${e}`);
|
||||
return JSON.stringify({ bans: [] });
|
||||
}
|
||||
}
|
||||
|
||||
interface GetServerStatsRequest {
|
||||
match_id?: string;
|
||||
}
|
||||
|
||||
function rpcAdminGetServerStats(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
payload: string
|
||||
): string {
|
||||
const request = JSON.parse(payload || "{}") as GetServerStatsRequest;
|
||||
|
||||
if (request.match_id) {
|
||||
requireAdminOrHost(ctx, nk, request.match_id);
|
||||
} else {
|
||||
requireAdmin(ctx, nk);
|
||||
}
|
||||
|
||||
try {
|
||||
// Get server-wide stats
|
||||
const matches = nk.matchList(100, true, null, null, null, null);
|
||||
const activeMatchCount = matches?.length || 0;
|
||||
|
||||
let totalPlayers = 0;
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
totalPlayers += match.size || 0;
|
||||
}
|
||||
}
|
||||
|
||||
const stats = {
|
||||
active_matches: activeMatchCount,
|
||||
total_players: totalPlayers,
|
||||
server_time: new Date().toISOString()
|
||||
};
|
||||
|
||||
// If specific match requested, include match details
|
||||
if (request.match_id) {
|
||||
try {
|
||||
const match = nk.matchGet(request.match_id);
|
||||
if (match) {
|
||||
(stats as any).match = {
|
||||
id: match.matchId,
|
||||
size: match.size,
|
||||
tick_rate: match.tickRate,
|
||||
authoritative: match.authoritative
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// Match not found
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(stats);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get server stats: ${e}`);
|
||||
throw new Error("Failed to get server stats");
|
||||
}
|
||||
}
|
||||
|
||||
interface GetPlayerListRequest {
|
||||
match_id: string;
|
||||
}
|
||||
|
||||
function rpcAdminGetPlayerList(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
payload: string
|
||||
): string {
|
||||
const request = JSON.parse(payload) as GetPlayerListRequest;
|
||||
|
||||
requireAdminOrHost(ctx, nk, request.match_id);
|
||||
|
||||
try {
|
||||
const match = nk.matchGet(request.match_id);
|
||||
if (!match) {
|
||||
throw new Error("Match not found");
|
||||
}
|
||||
|
||||
// Get player details
|
||||
const players: any[] = [];
|
||||
|
||||
// Note: In actual implementation, you'd need to track presences
|
||||
// This is a simplified version - adjust based on your match handler
|
||||
|
||||
return JSON.stringify({ players });
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get player list: ${e}`);
|
||||
throw new Error("Failed to get player list");
|
||||
}
|
||||
}
|
||||
|
||||
interface EndMatchRequest {
|
||||
match_id: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
function rpcAdminEndMatch(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
payload: string
|
||||
): string {
|
||||
const request = JSON.parse(payload) as EndMatchRequest;
|
||||
|
||||
requireAdminOrHost(ctx, nk, request.match_id);
|
||||
|
||||
try {
|
||||
// Signal match to end
|
||||
nk.matchSignal(request.match_id, JSON.stringify({
|
||||
action: "end_match",
|
||||
reason: request.reason || "Ended by admin"
|
||||
}));
|
||||
|
||||
logger.info(`Match ${request.match_id} ended by ${ctx.userId}`);
|
||||
|
||||
return JSON.stringify({ success: true });
|
||||
} catch (e) {
|
||||
logger.error(`Failed to end match: ${e}`);
|
||||
throw new Error("Failed to end match");
|
||||
}
|
||||
}
|
||||
|
||||
interface SetUserRoleRequest {
|
||||
user_id: string;
|
||||
role: string; // "player", "moderator", "admin"
|
||||
}
|
||||
|
||||
function rpcAdminSetUserRole(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
payload: string
|
||||
): string {
|
||||
const request = JSON.parse(payload) as SetUserRoleRequest;
|
||||
|
||||
// Only owner/super-admin can set roles
|
||||
const callerAccount = nk.accountGetId(ctx.userId!);
|
||||
const callerMetadata = JSON.parse(callerAccount.user.metadata || "{}");
|
||||
|
||||
if (callerMetadata.role !== "owner") {
|
||||
throw new Error("Only owners can modify user roles");
|
||||
}
|
||||
|
||||
const validRoles = ["player", "moderator", "admin"];
|
||||
if (!validRoles.includes(request.role)) {
|
||||
throw new Error("Invalid role");
|
||||
}
|
||||
|
||||
try {
|
||||
const targetAccount = nk.accountGetId(request.user_id);
|
||||
const metadata = JSON.parse(targetAccount.user.metadata || "{}");
|
||||
|
||||
metadata.role = request.role;
|
||||
|
||||
nk.accountUpdateId(request.user_id, null, null, null, null, null, null, JSON.stringify(metadata));
|
||||
|
||||
logger.info(`User ${request.user_id} role set to ${request.role} by ${ctx.userId}`);
|
||||
|
||||
return JSON.stringify({ success: true, role: request.role });
|
||||
} catch (e) {
|
||||
logger.error(`Failed to set user role: ${e}`);
|
||||
throw new Error("Failed to set user role");
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// User Profile RPCs
|
||||
// =============================================================================
|
||||
|
||||
function rpcGetUserProfile(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
payload: string
|
||||
): string {
|
||||
const request = JSON.parse(payload || "{}");
|
||||
const targetUserId = request.user_id || ctx.userId;
|
||||
|
||||
try {
|
||||
const account = nk.accountGetId(targetUserId!);
|
||||
const metadata = JSON.parse(account.user.metadata || "{}");
|
||||
|
||||
// Check if banned
|
||||
if (metadata.banned && targetUserId === ctx.userId) {
|
||||
// Check if ban expired
|
||||
if (metadata.ban_expires) {
|
||||
const expiresAt = new Date(metadata.ban_expires);
|
||||
if (expiresAt <= new Date()) {
|
||||
// Ban expired, remove it
|
||||
delete metadata.banned;
|
||||
delete metadata.ban_reason;
|
||||
delete metadata.ban_expires;
|
||||
nk.accountUpdateId(targetUserId!, null, null, null, null, null, null, JSON.stringify(metadata));
|
||||
} else {
|
||||
throw new Error(`Account banned until ${metadata.ban_expires}. Reason: ${metadata.ban_reason}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Account permanently banned. Reason: ${metadata.ban_reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
user_id: account.user.id,
|
||||
username: account.user.username,
|
||||
display_name: account.user.displayName,
|
||||
avatar_url: account.user.avatarUrl,
|
||||
create_time: account.user.createTime,
|
||||
role: metadata.role || "player"
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateProfileRequest {
|
||||
display_name?: string;
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
function rpcUpdateUserProfile(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
payload: string
|
||||
): string {
|
||||
if (!ctx.userId) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
const request = JSON.parse(payload) as UpdateProfileRequest;
|
||||
|
||||
try {
|
||||
nk.accountUpdateId(
|
||||
ctx.userId,
|
||||
null, // username
|
||||
request.display_name || null,
|
||||
null, // timezone
|
||||
null, // location
|
||||
null, // lang
|
||||
request.avatar_url || null,
|
||||
null // metadata
|
||||
);
|
||||
|
||||
return JSON.stringify({ success: true });
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update profile: ${e}`);
|
||||
throw new Error("Failed to update profile");
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Leaderboard RPCs
|
||||
// =============================================================================
|
||||
|
||||
function rpcGetLeaderboardStats(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
payload: string
|
||||
): string {
|
||||
try {
|
||||
// Query the "stats" collection to get all user stats
|
||||
// Warning: For a large production game this should be indexed using Nakama's actual Leaderboard system,
|
||||
// but this works for listing all users' custom storage stats.
|
||||
const limit = 100;
|
||||
const result = nk.storageList(null, "stats", limit, "");
|
||||
|
||||
const statsObjects = result.objects || [];
|
||||
const leaderboardData: any[] = [];
|
||||
|
||||
for (const obj of statsObjects) {
|
||||
try {
|
||||
const stats = JSON.parse(obj.value);
|
||||
const userId = obj.userId;
|
||||
|
||||
// Get the user's profile to retrieve their display name
|
||||
let displayName = "Unknown";
|
||||
let avatarUrl = "";
|
||||
try {
|
||||
const account = nk.accountGetId(userId);
|
||||
displayName = account.user.displayName || account.user.username;
|
||||
avatarUrl = account.user.avatarUrl;
|
||||
} catch (e) {
|
||||
// Ignore if account fetch fails
|
||||
}
|
||||
|
||||
leaderboardData.push({
|
||||
user_id: userId,
|
||||
display_name: displayName,
|
||||
avatar_url: avatarUrl,
|
||||
games_played: stats.games_played || 0,
|
||||
games_won: stats.games_won || 0,
|
||||
high_score: stats.high_score || 0
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`Error parsing stats for object: ${obj.key}`);
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({ leaderboard: leaderboardData });
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get leaderboard stats: ${e}`);
|
||||
throw new Error("Failed to get leaderboard stats");
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Admin User Management RPCs
|
||||
// =============================================================================
|
||||
|
||||
function rpcAdminListUsers(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
payload: string
|
||||
): string {
|
||||
requireAdmin(ctx, nk);
|
||||
|
||||
try {
|
||||
const users: any[] = [];
|
||||
let cursor = "";
|
||||
for (let page = 0; page < 10; page++) {
|
||||
const result = nk.usersList("", "", 100, cursor);
|
||||
if (!result || !result.users || result.users.length === 0) break;
|
||||
for (const u of result.users) {
|
||||
let metadata: UserMetadata = {};
|
||||
try { metadata = JSON.parse(u.metadata || "{}"); } catch(e) {}
|
||||
users.push({
|
||||
user_id: u.id,
|
||||
username: u.username || "",
|
||||
display_name: u.displayName || u.username || "",
|
||||
create_time: u.createTime,
|
||||
role: metadata.role || "player",
|
||||
banned: metadata.banned || false,
|
||||
ban_reason: metadata.ban_reason || ""
|
||||
});
|
||||
}
|
||||
if (!result.cursor) break;
|
||||
cursor = result.cursor;
|
||||
}
|
||||
return JSON.stringify({ users, count: users.length });
|
||||
} catch (e) {
|
||||
logger.error(`Failed to list users: ${e}`);
|
||||
throw new Error(`Failed to list users: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
interface DeleteUsersRequest {
|
||||
user_ids: string[];
|
||||
}
|
||||
|
||||
function rpcAdminDeleteUsers(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
payload: string
|
||||
): string {
|
||||
requireAdmin(ctx, nk);
|
||||
|
||||
const request = JSON.parse(payload) as DeleteUsersRequest;
|
||||
const userIds = request.user_ids || [];
|
||||
|
||||
if (userIds.length === 0) {
|
||||
throw new Error("No user IDs provided");
|
||||
}
|
||||
|
||||
for (const uid of userIds) {
|
||||
if (uid === ctx.userId) {
|
||||
throw new Error("Cannot delete your own account");
|
||||
}
|
||||
}
|
||||
|
||||
const deleted: string[] = [];
|
||||
const failed: { user_id: string; reason: string }[] = [];
|
||||
|
||||
for (const uid of userIds) {
|
||||
try {
|
||||
const account = nk.accountGetId(uid);
|
||||
const meta = JSON.parse(account.user.metadata || "{}") as UserMetadata;
|
||||
if (ADMIN_ROLES.includes(meta.role || "")) {
|
||||
failed.push({ user_id: uid, reason: "Cannot delete admin account" });
|
||||
continue;
|
||||
}
|
||||
nk.accountDeleteId(uid, false);
|
||||
deleted.push(uid);
|
||||
logger.warn(`User ${uid} deleted by ${ctx.userId}`);
|
||||
} catch (e) {
|
||||
failed.push({ user_id: uid, reason: `${e}` });
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({ success: true, deleted, failed });
|
||||
}
|
||||
|
||||
// Before login hook to check ban status
|
||||
function beforeAuthenticateEmail(
|
||||
ctx: nkruntime.Context,
|
||||
logger: nkruntime.Logger,
|
||||
nk: nkruntime.Nakama,
|
||||
data: nkruntime.AuthenticateEmailRequest
|
||||
): nkruntime.AuthenticateEmailRequest {
|
||||
// Can't check ban before auth, so we check in afterAuthenticate
|
||||
return data;
|
||||
}
|
||||
Reference in New Issue
Block a user