Editor/SetupWindow.cs
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Sandbox;
using Editor;
/// <summary>
/// Setup window for entering Network Storage credentials.
/// Uses Paint for layout/chrome but real LineEdit widgets for text inputs,
/// giving native text selection, copy/paste, drag, and unlimited key length.
///
/// Access via Editor menu → Network Storage → Setup, or the Setup button in the Sync Tool.
/// </summary>
[Dock( "Editor", "Network Storage Setup", "key" )]
public class SetupWindow : DockWindow
{
// ── Real input widgets (native text selection, copy/paste, any length) ──
private LineEdit _projectIdInput;
private LineEdit _publicKeyInput;
private LineEdit _secretKeyInput;
private LineEdit _baseUrlInput;
private LineEdit _cdnUrlInput;
private LineEdit _dataFolderInput;
// ── UI state ──
private string _status = "";
private string _statusColor = "white";
private bool _secretKeyMasked = true;
private List<ButtonRect> _buttons = new();
private Vector2 _mousePos;
// ── Test results ──
private string _testProjectId;
private string _testSecretKey;
private string _testPublicKey;
private string _testProjectTitle;
private string _testPermissions;
private struct ButtonRect
{
public Rect Rect;
public string Id;
public Action OnClick;
}
public SetupWindow()
{
Title = "Network Storage Setup";
Size = new Vector2( 520, 720 );
MinimumSize = new Vector2( 400, 580 );
SyncToolConfig.Load();
CreateInputWidgets();
}
[Menu( "Editor", "Network Storage/Setup" )]
public static void OpenWindow()
{
var window = new SetupWindow();
window.Show();
}
// ──────────────────────────────────────────────────────
// Create real LineEdit widgets (children of the window)
// ──────────────────────────────────────────────────────
private void CreateInputWidgets()
{
_projectIdInput = CreateLineEdit( SyncToolConfig.ProjectId, "From sbox.cool dashboard" );
_publicKeyInput = CreateLineEdit( SyncToolConfig.PublicApiKey, "Public key — used by game client" );
_secretKeyInput = CreateLineEdit( SyncToolConfig.SecretKey, "Secret key — editor only, NEVER ships" );
_baseUrlInput = CreateLineEdit( SyncToolConfig.BaseUrl, "Default: https://api.sboxcool.com" );
_cdnUrlInput = CreateLineEdit( SyncToolConfig.CdnUrl, "Optional: storage.sboxcool.com" );
_dataFolderInput = CreateLineEdit( SyncToolConfig.DataFolder, "Subfolder under Editor/ (default: Network Storage)" );
}
private LineEdit CreateLineEdit( string value, string placeholder )
{
var input = new LineEdit( this );
input.Text = value ?? "";
input.PlaceholderText = placeholder;
input.Visible = true;
return input;
}
/// <summary>
/// Position a LineEdit at the given rect. Called during OnPaint so they track layout.
/// </summary>
private void PositionInput( LineEdit input, float x, float y, float w, float h )
{
input.Position = new Vector2( x, y );
input.Size = new Vector2( w, h );
}
// ──────────────────────────────────────────────────────
// Rendering (Paint for chrome, real widgets for inputs)
// ──────────────────────────────────────────────────────
protected override void OnPaint()
{
base.OnPaint();
_buttons.Clear();
var y = 38f;
var pad = 20f;
var w = Width - pad * 2;
var fieldH = 28f;
// ── Title ──
Paint.SetDefaultFont( size: 14, weight: 700 );
Paint.SetPen( Color.White );
Paint.DrawText( new Rect( pad, y, w, 24 ), "Network Storage Setup", TextFlag.LeftCenter );
y += 32;
// ── Description ──
Paint.SetDefaultFont( size: 9 );
Paint.SetPen( Color.White.WithAlpha( 0.5f ) );
Paint.DrawText( new Rect( pad, y, w, 14 ), "Credentials are stored in Editor/ config files (gitignored, never published)", TextFlag.LeftCenter );
y += 22;
DrawSeparator( ref y, w, pad );
// ── Input fields (label + real LineEdit widget) ──
DrawFieldLabel( ref y, pad, w, "Project ID" );
PositionInput( _projectIdInput, pad, y, w, fieldH );
y += fieldH + 6;
DrawFieldLabel( ref y, pad, w, "Public API Key" );
PositionInput( _publicKeyInput, pad, y, w, fieldH );
y += fieldH + 2;
// Public key prefix hint
var pubKey = _publicKeyInput.Text?.Trim() ?? "";
if ( !string.IsNullOrEmpty( pubKey ) && !pubKey.StartsWith( "sbox_ns_" ) )
{
Paint.SetDefaultFont( size: 8 );
Paint.SetPen( Color.Orange.WithAlpha( 0.7f ) );
Paint.DrawText( new Rect( pad, y, w, 12 ), "Standard public keys start with sbox_ns_", TextFlag.LeftCenter );
y += 14;
}
y += 4;
// Secret Key — with show/hide toggle
var toggleW = 48f;
var toggleGap = 6f;
var fieldInputW = w - toggleW - toggleGap;
// Label
DrawFieldLabel( ref y, pad, w, "Secret Key" );
if ( _secretKeyMasked )
{
// Hide the real input, draw a masked box with dots
_secretKeyInput.Visible = false;
var maskedRect = new Rect( pad, y, fieldInputW, fieldH );
var maskedHovered = maskedRect.IsInside( _mousePos );
Paint.SetBrush( Color.White.WithAlpha( maskedHovered ? 0.08f : 0.04f ) );
Paint.SetPen( Color.White.WithAlpha( maskedHovered ? 0.2f : 0.1f ) );
Paint.DrawRect( maskedRect, 3 );
Paint.SetDefaultFont( size: 12 );
var secText = _secretKeyInput.Text?.Trim() ?? "";
if ( string.IsNullOrEmpty( secText ) )
{
Paint.SetPen( Color.White.WithAlpha( 0.25f ) );
Paint.SetDefaultFont( size: 10 );
Paint.DrawText( new Rect( pad + 8, y, fieldInputW - 16, fieldH ), "Secret key — editor only, NEVER ships", TextFlag.LeftCenter );
}
else
{
Paint.SetPen( Color.White.WithAlpha( 0.6f ) );
var dots = new string( '\u2022', Math.Min( secText.Length, 40 ) );
Paint.DrawText( new Rect( pad + 8, y, fieldInputW - 16, fieldH ), dots, TextFlag.LeftCenter );
}
}
else
{
// Show the real input
_secretKeyInput.Visible = true;
PositionInput( _secretKeyInput, pad, y, fieldInputW, fieldH );
}
// Show/Hide toggle button
var toggleRect = new Rect( pad + fieldInputW + toggleGap, y, toggleW, fieldH );
var toggleHovered = toggleRect.IsInside( _mousePos );
var toggleColor = Color.Cyan;
Paint.SetBrush( toggleColor.WithAlpha( toggleHovered ? 0.2f : 0.08f ) );
Paint.SetPen( toggleColor.WithAlpha( toggleHovered ? 0.5f : 0.2f ) );
Paint.DrawRect( toggleRect, 3 );
Paint.SetDefaultFont( size: 9, weight: 600 );
Paint.SetPen( toggleColor.WithAlpha( toggleHovered ? 1f : 0.7f ) );
Paint.DrawText( toggleRect, _secretKeyMasked ? "Show" : "Hide", TextFlag.Center );
_buttons.Add( new ButtonRect
{
Rect = toggleRect,
Id = "toggle_secret",
OnClick = () =>
{
_secretKeyMasked = !_secretKeyMasked;
Update();
}
} );
y += fieldH + 2;
// Secret key prefix hint
var secKey = _secretKeyInput.Text?.Trim() ?? "";
if ( !string.IsNullOrEmpty( secKey ) && !secKey.StartsWith( "sbox_sk_" ) )
{
Paint.SetDefaultFont( size: 8 );
Paint.SetPen( Color.Orange.WithAlpha( 0.7f ) );
Paint.DrawText( new Rect( pad, y, w, 12 ), "Standard secret keys start with sbox_sk_", TextFlag.LeftCenter );
y += 14;
}
y += 4;
DrawFieldLabel( ref y, pad, w, "Base URL" );
PositionInput( _baseUrlInput, pad, y, w, fieldH );
y += fieldH + 6;
DrawFieldLabel( ref y, pad, w, "CDN URL" );
PositionInput( _cdnUrlInput, pad, y, w, fieldH );
y += fieldH + 6;
DrawFieldLabel( ref y, pad, w, "Editor Data Folder" );
PositionInput( _dataFolderInput, pad, y, w, fieldH );
y += fieldH + 8;
DrawSeparator( ref y, w, pad );
// ── Data Source Preference ──
Paint.SetDefaultFont( size: 10, weight: 600 );
Paint.SetPen( Color.White.WithAlpha( 0.7f ) );
Paint.DrawText( new Rect( pad, y, w, 18 ), "Data Source: API Only", TextFlag.LeftCenter );
y += 24;
// ── Save button ──
var saveBtnH = 34f;
var saveBtnRect = new Rect( pad, y, w, saveBtnH );
var saveHovered = saveBtnRect.IsInside( _mousePos );
Paint.SetBrush( Color.Green.WithAlpha( saveHovered ? 0.25f : 0.15f ) );
Paint.SetPen( Color.Green.WithAlpha( saveHovered ? 0.6f : 0.3f ) );
Paint.DrawRect( saveBtnRect, 4 );
Paint.SetDefaultFont( size: 11, weight: 700 );
Paint.SetPen( Color.Green.WithAlpha( saveHovered ? 1f : 0.8f ) );
Paint.DrawText( saveBtnRect, "Save Configuration", TextFlag.Center );
_buttons.Add( new ButtonRect { Rect = saveBtnRect, Id = "save", OnClick = SaveConfig } );
y += saveBtnH + 8;
// ── Test Connection button ──
var testBtnH = 28f;
var testBtnRect = new Rect( pad, y, w, testBtnH );
var testHovered = testBtnRect.IsInside( _mousePos );
Paint.SetBrush( Color.White.WithAlpha( testHovered ? 0.1f : 0.04f ) );
Paint.SetPen( Color.White.WithAlpha( testHovered ? 0.3f : 0.15f ) );
Paint.DrawRect( testBtnRect, 4 );
Paint.SetDefaultFont( size: 10 );
Paint.SetPen( Color.White.WithAlpha( testHovered ? 0.9f : 0.6f ) );
Paint.DrawText( testBtnRect, "Test Connection", TextFlag.Center );
_buttons.Add( new ButtonRect { Rect = testBtnRect, Id = "test", OnClick = () => _ = TestConnection() } );
y += testBtnH + 8;
// ── Test results ──
if ( _testProjectId != null )
{
DrawTestResult( ref y, pad, w, "Project ID", _testProjectId, _testProjectTitle );
DrawTestResult( ref y, pad, w, "Secret Key", _testSecretKey );
DrawTestResult( ref y, pad, w, "Public Key", _testPublicKey );
if ( !string.IsNullOrEmpty( _testPermissions ) )
{
y += 4;
Paint.SetDefaultFont( size: 8, weight: 600 );
Paint.SetPen( Color.White.WithAlpha( 0.5f ) );
Paint.DrawText( new Rect( pad, y, w, 12 ), "Key Permissions:", TextFlag.LeftCenter );
y += 14;
Paint.SetDefaultFont( size: 8 );
Paint.SetPen( Color.Cyan.WithAlpha( 0.7f ) );
Paint.DrawText( new Rect( pad + 8, y, w - 8, 12 ), _testPermissions, TextFlag.LeftCenter );
y += 14;
}
y += 4;
}
// ── Status ──
if ( !string.IsNullOrEmpty( _status ) )
{
Paint.SetDefaultFont( size: 9 );
var col = _statusColor == "green" ? Color.Green : _statusColor == "red" ? Color.Red : Color.White.WithAlpha( 0.6f );
Paint.SetPen( col );
Paint.DrawText( new Rect( pad, y, w, 16 ), _status, TextFlag.LeftCenter );
y += 22;
}
// ── File location ──
Paint.SetDefaultFont( size: 8 );
Paint.SetPen( Color.White.WithAlpha( 0.25f ) );
Paint.DrawText( new Rect( pad, y, w, 14 ), $"Saved to: {SyncToolConfig.EnvFilePath}", TextFlag.LeftCenter );
}
private void DrawFieldLabel( ref float y, float pad, float w, string label )
{
Paint.SetDefaultFont( size: 10, weight: 600 );
Paint.SetPen( Color.White.WithAlpha( 0.8f ) );
Paint.DrawText( new Rect( pad, y, w, 16 ), label, TextFlag.LeftCenter );
y += 18;
}
private void DrawSeparator( ref float y, float w, float pad )
{
Paint.SetPen( Color.White.WithAlpha( 0.08f ) );
Paint.DrawLine( new Vector2( pad, y ), new Vector2( pad + w, y ) );
y += 10;
}
private void DrawTestResult( ref float y, float pad, float w, string label, string result, string extra = null )
{
if ( result == null ) return;
var isOk = result.StartsWith( "Valid" ) || result.StartsWith( "Found" );
var color = isOk ? Color.Green : Color.Red;
Paint.SetDefaultFont( size: 9, weight: 600 );
Paint.SetPen( color.WithAlpha( 0.8f ) );
Paint.DrawText( new Rect( pad, y, 16, 15 ), isOk ? "+" : "x", TextFlag.Center );
Paint.SetDefaultFont( size: 9 );
Paint.SetPen( Color.White.WithAlpha( 0.7f ) );
Paint.DrawText( new Rect( pad + 18, y, 80, 15 ), label, TextFlag.LeftCenter );
var display = !string.IsNullOrEmpty( extra ) ? $"{result} — {extra}" : result;
Paint.SetPen( color.WithAlpha( 0.7f ) );
Paint.DrawText( new Rect( pad + 100, y, w - 100, 15 ), display, TextFlag.LeftCenter );
y += 17;
}
// ──────────────────────────────────────────────────────
// Input handling
// ──────────────────────────────────────────────────────
protected override void OnMousePress( MouseEvent e )
{
base.OnMousePress( e );
foreach ( var btn in _buttons )
{
if ( btn.Rect.IsInside( e.LocalPosition ) )
{
btn.OnClick?.Invoke();
return;
}
}
}
protected override void OnMouseMove( MouseEvent e )
{
base.OnMouseMove( e );
_mousePos = e.LocalPosition;
Update();
}
// ──────────────────────────────────────────────────────
// Actions
// ──────────────────────────────────────────────────────
private void SaveConfig()
{
var projectId = _projectIdInput.Text?.Trim() ?? "";
var publicKey = _publicKeyInput.Text?.Trim() ?? "";
var secretKey = _secretKeyInput.Text?.Trim() ?? "";
var baseUrl = _baseUrlInput.Text?.Trim() ?? "";
var cdnUrl = _cdnUrlInput.Text?.Trim() ?? "";
var dataFolder = _dataFolderInput.Text?.Trim() ?? "";
if ( string.IsNullOrEmpty( projectId ) )
{
_status = "Project ID is required";
_statusColor = "red";
Update();
return;
}
SyncToolConfig.Save( secretKey, publicKey, projectId, baseUrl, SyncToolConfig.DataSourceMode.ApiOnly, dataFolder, cdnUrl );
// Warn about non-standard prefixes but still save
var warnings = "";
if ( !string.IsNullOrEmpty( publicKey ) && !publicKey.StartsWith( "sbox_ns_" ) )
warnings += " (public key: non-standard prefix)";
if ( !string.IsNullOrEmpty( secretKey ) && !secretKey.StartsWith( "sbox_sk_" ) )
warnings += " (secret key: non-standard prefix)";
if ( !string.IsNullOrEmpty( warnings ) )
{
_status = $"Saved with warnings:{warnings}";
_statusColor = "green";
}
else
{
_status = "Configuration saved successfully";
_statusColor = "green";
}
Update();
}
private async Task TestConnection()
{
SaveConfig();
var projectId = _projectIdInput.Text?.Trim() ?? "";
var secretKey = _secretKeyInput.Text?.Trim() ?? "";
var publicKey = _publicKeyInput.Text?.Trim() ?? "";
if ( string.IsNullOrEmpty( projectId ) || string.IsNullOrEmpty( secretKey ) )
{
_status = "Need at least Project ID + Secret Key to test";
_statusColor = "red";
_testProjectId = null;
Update();
return;
}
_status = "Testing credentials...";
_statusColor = "white";
_testProjectId = null;
_testSecretKey = null;
_testPublicKey = null;
_testProjectTitle = null;
_testPermissions = null;
Update();
var resp = await SyncToolApi.Validate( publicKey );
if ( !resp.HasValue )
{
var errDetail = SyncToolApi.LastErrorMessage ?? "Could not reach server";
_status = $"Validation failed: {errDetail}";
_statusColor = "red";
_testProjectId = SyncToolApi.LastErrorCode ?? "Connection failed";
_testSecretKey = SyncToolApi.LastErrorCode ?? "Connection failed";
_testPublicKey = SyncToolApi.LastErrorCode ?? "Connection failed";
Log.Warning( $"[SyncTool] Validate returned null — LastError: {SyncToolApi.LastErrorCode} — {SyncToolApi.LastErrorMessage}" );
Update();
return;
}
var result = resp.Value;
// Check for KEY_UPGRADE_REQUIRED error
if ( SyncToolApi.LastErrorCode == "KEY_UPGRADE_REQUIRED" ||
(result.TryGetProperty( "error", out var errEl ) && errEl.GetString() == "KEY_UPGRADE_REQUIRED") )
{
_status = "Secret key uses an old format. Generate a new key at sbox.cool.";
_statusColor = "red";
_testProjectId = "Not checked";
_testSecretKey = "Old format — regenerate on dashboard";
_testPublicKey = "Not checked";
Update();
return;
}
if ( result.TryGetProperty( "checks", out var checks ) )
{
_testProjectId = GetCheckMessage( checks, "projectId" );
_testSecretKey = GetCheckMessage( checks, "secretKey" );
_testPublicKey = GetCheckMessage( checks, "publicKey" );
}
if ( result.TryGetProperty( "project", out var proj ) && proj.ValueKind == JsonValueKind.Object )
{
_testProjectTitle = proj.TryGetProperty( "title", out var t ) ? t.GetString() : null;
}
// Parse permissions if present
if ( result.TryGetProperty( "permissions", out var perms ) && perms.ValueKind == JsonValueKind.Object )
{
var parts = new List<string>();
var labels = new Dictionary<string, string>
{
{ "endpoints", "Endpoints" },
{ "queries", "Queries" },
{ "collections", "Collections" },
{ "workflows", "Workflows" },
{ "game_values", "Game Values" },
{ "rate_limits", "Rate Limits" },
{ "settings", "Settings" }
};
foreach ( var kv in labels )
{
if ( perms.TryGetProperty( kv.Key, out var val ) )
{
var access = val.GetString() ?? "rw";
var label = FormatPermissionLabel( access );
parts.Add( $"{kv.Value}: {label}" );
}
}
_testPermissions = string.Join( " | ", parts );
}
var allOk = result.TryGetProperty( "ok", out var okEl ) && okEl.ValueKind == JsonValueKind.True;
if ( allOk )
{
_status = "All credentials validated";
_statusColor = "green";
}
else
{
// Build a meaningful error message from the check results
var failedChecks = new List<string>();
if ( _testProjectId != null && _testProjectId != "Found" ) failedChecks.Add( $"Project: {_testProjectId}" );
if ( _testSecretKey != null && _testSecretKey != "Valid" ) failedChecks.Add( $"Secret key: {_testSecretKey}" );
if ( _testPublicKey != null && _testPublicKey != "Valid" && _testPublicKey != "Not provided" ) failedChecks.Add( $"Public key: {_testPublicKey}" );
_status = failedChecks.Count > 0
? $"Failed: {string.Join( " | ", failedChecks )}"
: "Validation failed — check console for details";
_statusColor = "red";
}
Update();
}
private static string FormatPermissionLabel( string access )
{
if ( string.IsNullOrWhiteSpace( access ) || access == "none" ) return "-";
return access.ToUpperInvariant();
}
private static string GetCheckMessage( JsonElement checks, string key )
{
if ( !checks.TryGetProperty( key, out var check ) ) return "Not checked";
var msg = check.TryGetProperty( "message", out var m ) ? m.GetString() : "Unknown";
return msg ?? "Unknown";
}
}