first commit
This commit is contained in:
364
godot_client/scripts/services/http_client.gd
Normal file
364
godot_client/scripts/services/http_client.gd
Normal file
@@ -0,0 +1,364 @@
|
||||
extends Node
|
||||
## HTTPClient Service
|
||||
##
|
||||
## Singleton service for all HTTP communication with the Flask backend.
|
||||
## Handles authentication, JSON parsing, error handling, and provides
|
||||
## a convenient interface for making API requests.
|
||||
##
|
||||
## Usage:
|
||||
## HTTPClient.http_get("/api/v1/characters", _on_characters_loaded)
|
||||
## HTTPClient.http_post("/api/v1/auth/login", {"email": "...", "password": "..."}, _on_login_complete)
|
||||
|
||||
# API Configuration
|
||||
const API_TIMEOUT := 30.0 # Seconds
|
||||
|
||||
# HTTP Methods
|
||||
enum Method {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
PATCH
|
||||
}
|
||||
|
||||
# Internal state
|
||||
var _auth_token: String = ""
|
||||
var _session_cookie: String = "" # Session cookie for authentication
|
||||
#var _request_queue: Array[Dictionary] = []
|
||||
var _active_requests: Dictionary = {} # request_id -> HTTPRequest node
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
print("[HTTPClient] Service initialized. API Base URL: %s" % Settings.get_api_url())
|
||||
|
||||
|
||||
## Make a GET request
|
||||
##
|
||||
## @param endpoint: API endpoint (e.g., "/api/v1/characters")
|
||||
## @param callback: Function to call with APIResponse when complete
|
||||
## @param error_callback: Optional function to call on error
|
||||
func http_get(endpoint: String, callback: Callable, error_callback: Callable = Callable()) -> void:
|
||||
_make_request(Method.GET, endpoint, {}, callback, error_callback)
|
||||
|
||||
|
||||
## Make a POST request
|
||||
##
|
||||
## @param endpoint: API endpoint
|
||||
## @param data: Dictionary to send as JSON body
|
||||
## @param callback: Function to call with APIResponse when complete
|
||||
## @param error_callback: Optional function to call on error
|
||||
func http_post(endpoint: String, data: Dictionary, callback: Callable, error_callback: Callable = Callable()) -> void:
|
||||
_make_request(Method.POST, endpoint, data, callback, error_callback)
|
||||
|
||||
|
||||
## Make a PUT request
|
||||
##
|
||||
## @param endpoint: API endpoint
|
||||
## @param data: Dictionary to send as JSON body
|
||||
## @param callback: Function to call with APIResponse when complete
|
||||
## @param error_callback: Optional function to call on error
|
||||
func http_put(endpoint: String, data: Dictionary, callback: Callable, error_callback: Callable = Callable()) -> void:
|
||||
_make_request(Method.PUT, endpoint, data, callback, error_callback)
|
||||
|
||||
|
||||
## Make a DELETE request
|
||||
##
|
||||
## @param endpoint: API endpoint
|
||||
## @param callback: Function to call with APIResponse when complete
|
||||
## @param error_callback: Optional function to call on error
|
||||
func http_delete(endpoint: String, callback: Callable, error_callback: Callable = Callable()) -> void:
|
||||
_make_request(Method.DELETE, endpoint, {}, callback, error_callback)
|
||||
|
||||
|
||||
## Make a PATCH request
|
||||
##
|
||||
## @param endpoint: API endpoint
|
||||
## @param data: Dictionary to send as JSON body
|
||||
## @param callback: Function to call with APIResponse when complete
|
||||
## @param error_callback: Optional function to call on error
|
||||
func http_patch(endpoint: String, data: Dictionary, callback: Callable, error_callback: Callable = Callable()) -> void:
|
||||
_make_request(Method.PATCH, endpoint, data, callback, error_callback)
|
||||
|
||||
|
||||
## Set the authentication token for subsequent requests
|
||||
##
|
||||
## @param token: JWT token from login/registration
|
||||
func set_auth_token(token: String) -> void:
|
||||
_auth_token = token
|
||||
print("[HTTPClient] Auth token set")
|
||||
|
||||
|
||||
## Clear the authentication token (logout)
|
||||
func clear_auth_token() -> void:
|
||||
_auth_token = ""
|
||||
print("[HTTPClient] Auth token cleared")
|
||||
|
||||
|
||||
## Get current auth token
|
||||
func get_auth_token() -> String:
|
||||
return _auth_token
|
||||
|
||||
|
||||
## Set the session cookie for subsequent requests
|
||||
##
|
||||
## @param cookie: Session cookie value (e.g., "coc_session=xxx")
|
||||
func set_session_cookie(cookie: String) -> void:
|
||||
_session_cookie = cookie
|
||||
print("[HTTPClient] Session cookie set")
|
||||
|
||||
|
||||
## Clear the session cookie (logout)
|
||||
func clear_session_cookie() -> void:
|
||||
_session_cookie = ""
|
||||
print("[HTTPClient] Session cookie cleared")
|
||||
|
||||
|
||||
## Get current session cookie
|
||||
func get_session_cookie() -> String:
|
||||
return _session_cookie
|
||||
|
||||
|
||||
## Check if authenticated
|
||||
func is_authenticated() -> bool:
|
||||
return not _auth_token.is_empty() or not _session_cookie.is_empty()
|
||||
|
||||
|
||||
## Internal: Make HTTP request
|
||||
func _make_request(
|
||||
method: Method,
|
||||
endpoint: String,
|
||||
data: Dictionary,
|
||||
callback: Callable,
|
||||
error_callback: Callable
|
||||
) -> void:
|
||||
# Create HTTPRequest node
|
||||
var http_request := HTTPRequest.new()
|
||||
add_child(http_request)
|
||||
|
||||
# Generate request ID for tracking
|
||||
var request_id := "%s_%d" % [endpoint.get_file(), Time.get_ticks_msec()]
|
||||
_active_requests[request_id] = http_request
|
||||
|
||||
# Build full URL
|
||||
var url := Settings.get_api_url() + endpoint
|
||||
|
||||
# Build headers
|
||||
var headers := _build_headers()
|
||||
|
||||
# Connect completion signal
|
||||
http_request.request_completed.connect(
|
||||
_on_request_completed.bind(request_id, callback, error_callback)
|
||||
)
|
||||
|
||||
# Set timeout
|
||||
http_request.timeout = API_TIMEOUT
|
||||
|
||||
# Make request based on method
|
||||
var method_int := _method_to_int(method)
|
||||
var body := ""
|
||||
|
||||
if method in [Method.POST, Method.PUT, Method.PATCH]:
|
||||
body = JSON.stringify(data)
|
||||
|
||||
print("[HTTPClient] %s %s" % [_method_to_string(method), url])
|
||||
if not body.is_empty():
|
||||
print("[HTTPClient] Body: %s" % body)
|
||||
|
||||
var error := http_request.request(url, headers, method_int, body)
|
||||
|
||||
if error != OK:
|
||||
push_error("[HTTPClient] Failed to initiate request: %s" % error)
|
||||
_cleanup_request(request_id)
|
||||
if error_callback.is_valid():
|
||||
error_callback.call(_create_error_response("Failed to initiate request", 0))
|
||||
|
||||
|
||||
## Internal: Build request headers
|
||||
func _build_headers() -> PackedStringArray:
|
||||
var headers := PackedStringArray([
|
||||
"Content-Type: application/json",
|
||||
"Accept: application/json"
|
||||
])
|
||||
|
||||
# Add auth token if present (for JWT auth)
|
||||
if not _auth_token.is_empty():
|
||||
headers.append("Authorization: Bearer %s" % _auth_token)
|
||||
|
||||
# Add session cookie if present (for cookie-based auth)
|
||||
if not _session_cookie.is_empty():
|
||||
headers.append("Cookie: %s" % _session_cookie)
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
## Internal: Handle request completion
|
||||
func _on_request_completed(
|
||||
result: int,
|
||||
response_code: int,
|
||||
headers: PackedStringArray,
|
||||
body: PackedByteArray,
|
||||
request_id: String,
|
||||
callback: Callable,
|
||||
error_callback: Callable
|
||||
) -> void:
|
||||
print("[HTTPClient] Request completed: %s (status=%d)" % [request_id, response_code])
|
||||
|
||||
# Extract Set-Cookie header if present
|
||||
_extract_session_cookie(headers)
|
||||
|
||||
# Parse response body
|
||||
var body_string := body.get_string_from_utf8()
|
||||
|
||||
# Handle network errors
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
push_error("[HTTPClient] Network error: %s" % _result_to_string(result))
|
||||
_cleanup_request(request_id)
|
||||
|
||||
if error_callback.is_valid():
|
||||
error_callback.call(_create_error_response(_result_to_string(result), response_code))
|
||||
return
|
||||
|
||||
# Parse JSON response
|
||||
var json := JSON.new()
|
||||
var parse_error := json.parse(body_string)
|
||||
|
||||
if parse_error != OK:
|
||||
push_error("[HTTPClient] Failed to parse JSON: %s" % body_string)
|
||||
_cleanup_request(request_id)
|
||||
|
||||
if error_callback.is_valid():
|
||||
error_callback.call(_create_error_response("Failed to parse JSON response", response_code))
|
||||
return
|
||||
|
||||
# Create APIResponse
|
||||
var api_response: APIResponse = APIResponse.new(json.data)
|
||||
api_response.raw_response = body_string
|
||||
|
||||
# Check for errors
|
||||
if api_response.has_error():
|
||||
print("[HTTPClient] API error: %s" % api_response.get_error_message())
|
||||
|
||||
if error_callback.is_valid():
|
||||
error_callback.call(api_response)
|
||||
elif callback.is_valid():
|
||||
# Call regular callback even with error if no error callback
|
||||
callback.call(api_response)
|
||||
else:
|
||||
print("[HTTPClient] Success: %s" % request_id)
|
||||
|
||||
if callback.is_valid():
|
||||
callback.call(api_response)
|
||||
|
||||
_cleanup_request(request_id)
|
||||
|
||||
|
||||
## Internal: Extract session cookie from response headers
|
||||
func _extract_session_cookie(headers: PackedStringArray) -> void:
|
||||
for header in headers:
|
||||
# Look for Set-Cookie header
|
||||
if header.begins_with("Set-Cookie:") or header.begins_with("set-cookie:"):
|
||||
# Extract cookie value
|
||||
var cookie_string := header.substr(11).strip_edges() # Remove "Set-Cookie:"
|
||||
|
||||
# Look for coc_session cookie
|
||||
if cookie_string.begins_with("coc_session="):
|
||||
# Extract just the cookie name=value part (before semicolon)
|
||||
var cookie_parts := cookie_string.split(";")
|
||||
if cookie_parts.size() > 0:
|
||||
_session_cookie = cookie_parts[0].strip_edges()
|
||||
print("[HTTPClient] Session cookie extracted: %s" % _session_cookie)
|
||||
return
|
||||
|
||||
|
||||
## Internal: Cleanup request resources
|
||||
func _cleanup_request(request_id: String) -> void:
|
||||
if _active_requests.has(request_id):
|
||||
var http_request: HTTPRequest = _active_requests[request_id]
|
||||
http_request.queue_free()
|
||||
_active_requests.erase(request_id)
|
||||
|
||||
|
||||
## Internal: Create error response
|
||||
func _create_error_response(message: String, status_code: int) -> APIResponse:
|
||||
var error_data := {
|
||||
"app": "Code of Conquest",
|
||||
"version": "0.1.0",
|
||||
"status": status_code if status_code > 0 else 500,
|
||||
"timestamp": Time.get_datetime_string_from_system(),
|
||||
"result": null,
|
||||
"error": {
|
||||
"message": message,
|
||||
"code": "NETWORK_ERROR"
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
return APIResponse.new(error_data)
|
||||
|
||||
|
||||
## Internal: Convert Method enum to HTTPClient constant
|
||||
func _method_to_int(method: Method) -> int:
|
||||
match method:
|
||||
Method.GET:
|
||||
return HTTPClient.METHOD_GET
|
||||
Method.POST:
|
||||
return HTTPClient.METHOD_POST
|
||||
Method.PUT:
|
||||
return HTTPClient.METHOD_PUT
|
||||
Method.DELETE:
|
||||
return HTTPClient.METHOD_DELETE
|
||||
Method.PATCH:
|
||||
return HTTPClient.METHOD_PATCH
|
||||
_:
|
||||
return HTTPClient.METHOD_GET
|
||||
|
||||
|
||||
## Internal: Convert Method enum to string
|
||||
func _method_to_string(method: Method) -> String:
|
||||
match method:
|
||||
Method.GET:
|
||||
return "GET"
|
||||
Method.POST:
|
||||
return "POST"
|
||||
Method.PUT:
|
||||
return "PUT"
|
||||
Method.DELETE:
|
||||
return "DELETE"
|
||||
Method.PATCH:
|
||||
return "PATCH"
|
||||
_:
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
## Internal: Convert HTTPRequest result to string
|
||||
func _result_to_string(result: int) -> String:
|
||||
match result:
|
||||
HTTPRequest.RESULT_SUCCESS:
|
||||
return "Success"
|
||||
HTTPRequest.RESULT_CHUNKED_BODY_SIZE_MISMATCH:
|
||||
return "Chunked body size mismatch"
|
||||
HTTPRequest.RESULT_CANT_CONNECT:
|
||||
return "Can't connect to server"
|
||||
HTTPRequest.RESULT_CANT_RESOLVE:
|
||||
return "Can't resolve hostname"
|
||||
HTTPRequest.RESULT_CONNECTION_ERROR:
|
||||
return "Connection error"
|
||||
HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR:
|
||||
return "TLS handshake error"
|
||||
HTTPRequest.RESULT_NO_RESPONSE:
|
||||
return "No response from server"
|
||||
HTTPRequest.RESULT_BODY_SIZE_LIMIT_EXCEEDED:
|
||||
return "Body size limit exceeded"
|
||||
HTTPRequest.RESULT_BODY_DECOMPRESS_FAILED:
|
||||
return "Body decompression failed"
|
||||
HTTPRequest.RESULT_REQUEST_FAILED:
|
||||
return "Request failed"
|
||||
HTTPRequest.RESULT_DOWNLOAD_FILE_CANT_OPEN:
|
||||
return "Can't open download file"
|
||||
HTTPRequest.RESULT_DOWNLOAD_FILE_WRITE_ERROR:
|
||||
return "Download file write error"
|
||||
HTTPRequest.RESULT_REDIRECT_LIMIT_REACHED:
|
||||
return "Redirect limit reached"
|
||||
HTTPRequest.RESULT_TIMEOUT:
|
||||
return "Request timeout"
|
||||
_:
|
||||
return "Unknown error (%d)" % result
|
||||
1
godot_client/scripts/services/http_client.gd.uid
Normal file
1
godot_client/scripts/services/http_client.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://du1woo6w2kr3b
|
||||
204
godot_client/scripts/services/settings.gd
Normal file
204
godot_client/scripts/services/settings.gd
Normal file
@@ -0,0 +1,204 @@
|
||||
extends Node
|
||||
## Settings Service
|
||||
##
|
||||
## Singleton service for application configuration.
|
||||
## Stores URLs, feature flags, and other runtime settings.
|
||||
##
|
||||
## Usage:
|
||||
## Settings.get_api_url()
|
||||
## Settings.get_web_url()
|
||||
## Settings.set_environment("production")
|
||||
|
||||
# Environment types
|
||||
enum Env {
|
||||
DEVELOPMENT,
|
||||
STAGING,
|
||||
PRODUCTION
|
||||
}
|
||||
|
||||
# Current environment
|
||||
var _current_environment: int = Env.DEVELOPMENT
|
||||
|
||||
# URL Configuration
|
||||
var _api_urls := {
|
||||
Env.DEVELOPMENT: "http://localhost:5000",
|
||||
Env.STAGING: "https://staging-api.codeofconquest.com",
|
||||
Env.PRODUCTION: "https://api.codeofconquest.com"
|
||||
}
|
||||
|
||||
var _web_urls := {
|
||||
Env.DEVELOPMENT: "http://localhost:8000", # Flask serves web pages in dev
|
||||
Env.STAGING: "https://staging.codeofconquest.com",
|
||||
Env.PRODUCTION: "https://www.codeofconquest.com"
|
||||
}
|
||||
|
||||
# Feature flags
|
||||
var _features := {
|
||||
"enable_debug_logging": true,
|
||||
"enable_analytics": false,
|
||||
"enable_multiplayer": false,
|
||||
"max_characters_per_user": 5
|
||||
}
|
||||
|
||||
# User preferences (persisted)
|
||||
var _preferences := {
|
||||
"remember_login": true,
|
||||
"auto_save": true,
|
||||
"sound_enabled": true,
|
||||
"music_enabled": true,
|
||||
"sound_volume": 0.8,
|
||||
"music_volume": 0.6
|
||||
}
|
||||
|
||||
# Save file configuration
|
||||
const SETTINGS_FILE_PATH := "user://settings.save"
|
||||
const SETTINGS_VERSION := 1
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_load_settings()
|
||||
_detect_environment()
|
||||
print("[Settings] Service initialized")
|
||||
print("[Settings] Environment: %s" % _environment_to_string(_current_environment))
|
||||
print("[Settings] API URL: %s" % get_api_url())
|
||||
print("[Settings] Web URL: %s" % get_web_url())
|
||||
|
||||
|
||||
## Get current API base URL
|
||||
func get_api_url() -> String:
|
||||
return _api_urls.get(_current_environment, _api_urls[Env.DEVELOPMENT])
|
||||
|
||||
|
||||
## Get current web frontend base URL
|
||||
func get_web_url() -> String:
|
||||
return _web_urls.get(_current_environment, _web_urls[Env.DEVELOPMENT])
|
||||
|
||||
|
||||
## Get current environment
|
||||
func get_environment() -> int:
|
||||
return _current_environment
|
||||
|
||||
|
||||
## Set environment (for testing/switching)
|
||||
func set_environment(env: int) -> void:
|
||||
_current_environment = env
|
||||
print("[Settings] Environment changed to: %s" % _environment_to_string(env))
|
||||
print("[Settings] API URL: %s" % get_api_url())
|
||||
print("[Settings] Web URL: %s" % get_web_url())
|
||||
|
||||
|
||||
## Check if a feature is enabled
|
||||
func is_feature_enabled(feature_name: String) -> bool:
|
||||
return _features.get(feature_name, false)
|
||||
|
||||
|
||||
## Get feature value
|
||||
func get_feature(feature_name: String, default: Variant = null) -> Variant:
|
||||
return _features.get(feature_name, default)
|
||||
|
||||
|
||||
## Set feature flag (for testing/debugging)
|
||||
func set_feature(feature_name: String, value: Variant) -> void:
|
||||
_features[feature_name] = value
|
||||
print("[Settings] Feature updated: %s = %s" % [feature_name, value])
|
||||
|
||||
|
||||
## Get user preference
|
||||
func get_preference(key: String, default: Variant = null) -> Variant:
|
||||
return _preferences.get(key, default)
|
||||
|
||||
|
||||
## Set user preference
|
||||
func set_preference(key: String, value: Variant) -> void:
|
||||
_preferences[key] = value
|
||||
print("[Settings] Preference updated: %s = %s" % [key, value])
|
||||
_save_settings()
|
||||
|
||||
|
||||
## Get all preferences
|
||||
func get_all_preferences() -> Dictionary:
|
||||
return _preferences.duplicate()
|
||||
|
||||
|
||||
## Auto-detect environment based on OS and build flags
|
||||
func _detect_environment() -> void:
|
||||
# Check for --production command line argument
|
||||
var args := OS.get_cmdline_args()
|
||||
if "--production" in args:
|
||||
_current_environment = Env.PRODUCTION
|
||||
return
|
||||
|
||||
if "--staging" in args:
|
||||
_current_environment = Env.STAGING
|
||||
return
|
||||
|
||||
# Default to development
|
||||
_current_environment = Env.DEVELOPMENT
|
||||
|
||||
|
||||
## Save settings to disk
|
||||
func _save_settings() -> void:
|
||||
var save_data := {
|
||||
"version": SETTINGS_VERSION,
|
||||
"preferences": _preferences,
|
||||
"timestamp": Time.get_unix_time_from_system()
|
||||
}
|
||||
|
||||
var file := FileAccess.open(SETTINGS_FILE_PATH, FileAccess.WRITE)
|
||||
if file == null:
|
||||
push_error("[Settings] Failed to open settings file for writing: %s" % FileAccess.get_open_error())
|
||||
return
|
||||
|
||||
file.store_string(JSON.stringify(save_data))
|
||||
file.close()
|
||||
|
||||
print("[Settings] Settings saved to %s" % SETTINGS_FILE_PATH)
|
||||
|
||||
|
||||
## Load settings from disk
|
||||
func _load_settings() -> void:
|
||||
if not FileAccess.file_exists(SETTINGS_FILE_PATH):
|
||||
print("[Settings] No settings file found, using defaults")
|
||||
return
|
||||
|
||||
var file := FileAccess.open(SETTINGS_FILE_PATH, FileAccess.READ)
|
||||
if file == null:
|
||||
push_error("[Settings] Failed to open settings file for reading: %s" % FileAccess.get_open_error())
|
||||
return
|
||||
|
||||
var json_string := file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json := JSON.new()
|
||||
var parse_error := json.parse(json_string)
|
||||
|
||||
if parse_error != OK:
|
||||
push_error("[Settings] Failed to parse settings file")
|
||||
return
|
||||
|
||||
var save_data: Dictionary = json.data
|
||||
|
||||
# Check version
|
||||
if save_data.get("version", 0) != SETTINGS_VERSION:
|
||||
print("[Settings] Settings file version mismatch, using defaults")
|
||||
return
|
||||
|
||||
# Restore preferences
|
||||
var saved_prefs = save_data.get("preferences", {})
|
||||
for key in saved_prefs:
|
||||
_preferences[key] = saved_prefs[key]
|
||||
|
||||
print("[Settings] Settings loaded from %s" % SETTINGS_FILE_PATH)
|
||||
|
||||
|
||||
## Convert environment enum to string
|
||||
func _environment_to_string(env: int) -> String:
|
||||
match env:
|
||||
Env.DEVELOPMENT:
|
||||
return "Development"
|
||||
Env.STAGING:
|
||||
return "Staging"
|
||||
Env.PRODUCTION:
|
||||
return "Production"
|
||||
_:
|
||||
return "Unknown"
|
||||
1
godot_client/scripts/services/settings.gd.uid
Normal file
1
godot_client/scripts/services/settings.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://3s8i3b6v5mde
|
||||
422
godot_client/scripts/services/state_manager.gd
Normal file
422
godot_client/scripts/services/state_manager.gd
Normal file
@@ -0,0 +1,422 @@
|
||||
extends Node
|
||||
## StateManager Service
|
||||
##
|
||||
## Singleton service for managing global application state.
|
||||
## Handles user session, character data, wizard state, navigation, and persistence.
|
||||
##
|
||||
## Usage:
|
||||
## StateManager.set_user_session(user_data)
|
||||
## var user = StateManager.get_current_user()
|
||||
## StateManager.save_state() # Persist to local storage
|
||||
|
||||
# Signals for state changes
|
||||
signal user_logged_in(user_data: Dictionary)
|
||||
signal user_logged_out()
|
||||
signal character_selected(character_id: String)
|
||||
signal character_created(character_data: Dictionary)
|
||||
signal character_deleted(character_id: String)
|
||||
signal characters_updated(characters: Array)
|
||||
|
||||
# Save file configuration
|
||||
const SAVE_FILE_PATH := "user://coc_state.save"
|
||||
const SAVE_VERSION := 1
|
||||
|
||||
# User session state
|
||||
var _current_user: Dictionary = {}
|
||||
var _auth_token: String = ""
|
||||
var _is_authenticated: bool = false
|
||||
|
||||
# Character state
|
||||
var _characters: Array[Dictionary] = []
|
||||
var _selected_character_id: String = ""
|
||||
var _character_limits: Dictionary = {}
|
||||
|
||||
# Character creation wizard state
|
||||
var _wizard_state: Dictionary = {
|
||||
"step": 0, # Current step (0-3)
|
||||
"selected_origin": null, # Selected origin data
|
||||
"selected_class": null, # Selected class data
|
||||
"character_name": "",
|
||||
"character_customization": {}
|
||||
}
|
||||
|
||||
# Navigation state
|
||||
var _current_scene: String = ""
|
||||
var _scene_history: Array[String] = []
|
||||
|
||||
# Settings (deprecated - use Settings service for preferences)
|
||||
# Kept for backward compatibility with save files
|
||||
var _settings: Dictionary = {
|
||||
"remember_login": true,
|
||||
"auto_save": true
|
||||
}
|
||||
|
||||
# Reference to HTTPClient singleton (available after _ready)
|
||||
@onready var http_client: Node = get_node("/root/HTTPClient")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
print("[StateManager] Service initialized")
|
||||
_load_state()
|
||||
|
||||
|
||||
## Set user session data (after login/registration)
|
||||
func set_user_session(user_data: Dictionary, token: String = "") -> void:
|
||||
_current_user = user_data
|
||||
_auth_token = token
|
||||
_is_authenticated = true
|
||||
|
||||
# Update HTTPClient with token (if using JWT auth)
|
||||
if not token.is_empty():
|
||||
http_client.set_auth_token(token)
|
||||
|
||||
print("[StateManager] User session set: %s" % user_data.get("email", "unknown"))
|
||||
|
||||
# Emit signal
|
||||
user_logged_in.emit(user_data)
|
||||
|
||||
# Auto-save if enabled
|
||||
if _settings.get("auto_save", true):
|
||||
save_state()
|
||||
|
||||
|
||||
## Clear user session (logout)
|
||||
func clear_user_session() -> void:
|
||||
_current_user = {}
|
||||
_auth_token = ""
|
||||
_is_authenticated = false
|
||||
_characters = []
|
||||
_selected_character_id = ""
|
||||
|
||||
# Clear HTTPClient token and cookie
|
||||
http_client.clear_auth_token()
|
||||
http_client.clear_session_cookie()
|
||||
|
||||
print("[StateManager] User session cleared")
|
||||
|
||||
# Emit signal
|
||||
user_logged_out.emit()
|
||||
|
||||
# Clear saved state
|
||||
_clear_save_file()
|
||||
|
||||
|
||||
## Get current user data
|
||||
func get_current_user() -> Dictionary:
|
||||
return _current_user
|
||||
|
||||
|
||||
## Get current auth token
|
||||
func get_auth_token() -> String:
|
||||
return _auth_token
|
||||
|
||||
|
||||
## Check if user is authenticated
|
||||
func is_authenticated() -> bool:
|
||||
# Check both token and cookie-based auth
|
||||
return _is_authenticated and (not _auth_token.is_empty() or http_client.is_authenticated())
|
||||
|
||||
|
||||
## Set characters list
|
||||
func set_characters(characters: Array) -> void:
|
||||
_characters.clear()
|
||||
for character in characters:
|
||||
_characters.append(character)
|
||||
|
||||
print("[StateManager] Characters updated: %d total" % _characters.size())
|
||||
|
||||
# Emit signal
|
||||
characters_updated.emit(_characters)
|
||||
|
||||
# Auto-save if enabled
|
||||
if _settings.get("auto_save", true):
|
||||
save_state()
|
||||
|
||||
|
||||
## Get all characters
|
||||
func get_characters() -> Array[Dictionary]:
|
||||
return _characters
|
||||
|
||||
|
||||
## Add character to list
|
||||
func add_character(character_data: Dictionary) -> void:
|
||||
_characters.append(character_data)
|
||||
|
||||
print("[StateManager] Character added: %s" % character_data.get("name", "unknown"))
|
||||
|
||||
# Emit signal
|
||||
character_created.emit(character_data)
|
||||
|
||||
# Auto-save if enabled
|
||||
if _settings.get("auto_save", true):
|
||||
save_state()
|
||||
|
||||
|
||||
## Remove character from list
|
||||
func remove_character(character_id: String) -> void:
|
||||
for i in range(_characters.size()):
|
||||
if _characters[i].get("id") == character_id:
|
||||
_characters.remove_at(i)
|
||||
print("[StateManager] Character removed: %s" % character_id)
|
||||
|
||||
# Clear selection if this was selected
|
||||
if _selected_character_id == character_id:
|
||||
_selected_character_id = ""
|
||||
|
||||
# Emit signal
|
||||
character_deleted.emit(character_id)
|
||||
|
||||
# Auto-save if enabled
|
||||
if _settings.get("auto_save", true):
|
||||
save_state()
|
||||
return
|
||||
|
||||
|
||||
## Get character by ID
|
||||
func get_character(character_id: String) -> Dictionary:
|
||||
for character in _characters:
|
||||
if character.get("id") == character_id:
|
||||
return character
|
||||
return {}
|
||||
|
||||
|
||||
## Set selected character
|
||||
func select_character(character_id: String) -> void:
|
||||
_selected_character_id = character_id
|
||||
print("[StateManager] Character selected: %s" % character_id)
|
||||
|
||||
# Emit signal
|
||||
character_selected.emit(character_id)
|
||||
|
||||
|
||||
## Get selected character
|
||||
func get_selected_character() -> Dictionary:
|
||||
return get_character(_selected_character_id)
|
||||
|
||||
|
||||
## Get selected character ID
|
||||
func get_selected_character_id() -> String:
|
||||
return _selected_character_id
|
||||
|
||||
|
||||
## Set character limits
|
||||
func set_character_limits(limits: Dictionary) -> void:
|
||||
_character_limits = limits
|
||||
|
||||
|
||||
## Get character limits
|
||||
func get_character_limits() -> Dictionary:
|
||||
return _character_limits
|
||||
|
||||
|
||||
## Character Creation Wizard State Management
|
||||
|
||||
## Reset wizard state
|
||||
func reset_wizard() -> void:
|
||||
_wizard_state = {
|
||||
"step": 0,
|
||||
"selected_origin": null,
|
||||
"selected_class": null,
|
||||
"character_name": "",
|
||||
"character_customization": {}
|
||||
}
|
||||
print("[StateManager] Wizard state reset")
|
||||
|
||||
|
||||
## Set wizard step
|
||||
func set_wizard_step(step: int) -> void:
|
||||
_wizard_state["step"] = step
|
||||
|
||||
|
||||
## Get current wizard step
|
||||
func get_wizard_step() -> int:
|
||||
return _wizard_state.get("step", 0)
|
||||
|
||||
|
||||
## Set selected origin
|
||||
func set_wizard_origin(origin_data: Dictionary) -> void:
|
||||
_wizard_state["selected_origin"] = origin_data
|
||||
|
||||
|
||||
## Get selected origin
|
||||
func get_wizard_origin() -> Dictionary:
|
||||
return _wizard_state.get("selected_origin", {})
|
||||
|
||||
|
||||
## Set selected class
|
||||
func set_wizard_class(class_data: Dictionary) -> void:
|
||||
_wizard_state["selected_class"] = class_data
|
||||
|
||||
|
||||
## Get selected class
|
||||
func get_wizard_class() -> Dictionary:
|
||||
return _wizard_state.get("selected_class", {})
|
||||
|
||||
|
||||
## Set character name
|
||||
func set_wizard_name(char_name: String) -> void:
|
||||
_wizard_state["character_name"] = char_name
|
||||
|
||||
|
||||
## Get character name
|
||||
func get_wizard_name() -> String:
|
||||
return _wizard_state.get("character_name", "")
|
||||
|
||||
|
||||
## Set character customization
|
||||
func set_wizard_customization(customization: Dictionary) -> void:
|
||||
_wizard_state["character_customization"] = customization
|
||||
|
||||
|
||||
## Get character customization
|
||||
func get_wizard_customization() -> Dictionary:
|
||||
return _wizard_state.get("character_customization", {})
|
||||
|
||||
|
||||
## Get complete wizard state
|
||||
func get_wizard_state() -> Dictionary:
|
||||
return _wizard_state
|
||||
|
||||
|
||||
## Navigation State Management
|
||||
|
||||
## Set current scene
|
||||
func set_current_scene(scene_path: String) -> void:
|
||||
if not _current_scene.is_empty():
|
||||
_scene_history.append(_current_scene)
|
||||
|
||||
_current_scene = scene_path
|
||||
print("[StateManager] Scene changed: %s" % scene_path)
|
||||
|
||||
|
||||
## Get current scene
|
||||
func get_current_scene() -> String:
|
||||
return _current_scene
|
||||
|
||||
|
||||
## Navigate back
|
||||
func navigate_back() -> String:
|
||||
if _scene_history.is_empty():
|
||||
return ""
|
||||
|
||||
var previous_scene: String = _scene_history.pop_back()
|
||||
_current_scene = previous_scene
|
||||
return previous_scene
|
||||
|
||||
|
||||
## Can navigate back
|
||||
func can_navigate_back() -> bool:
|
||||
return not _scene_history.is_empty()
|
||||
|
||||
|
||||
## Settings Management
|
||||
|
||||
## Get setting value
|
||||
func get_setting(key: String, default: Variant = null) -> Variant:
|
||||
return _settings.get(key, default)
|
||||
|
||||
|
||||
## Set setting value
|
||||
func set_setting(key: String, value: Variant) -> void:
|
||||
_settings[key] = value
|
||||
print("[StateManager] Setting updated: %s = %s" % [key, value])
|
||||
|
||||
# Auto-save if enabled
|
||||
if _settings.get("auto_save", true):
|
||||
save_state()
|
||||
|
||||
|
||||
## Get all settings
|
||||
func get_settings() -> Dictionary:
|
||||
return _settings
|
||||
|
||||
|
||||
## Persistence (Save/Load)
|
||||
|
||||
## Save state to disk
|
||||
func save_state() -> void:
|
||||
var save_data := {
|
||||
"version": SAVE_VERSION,
|
||||
"user": _current_user,
|
||||
"auth_token": _auth_token if _settings.get("remember_login", true) else "",
|
||||
"session_cookie": http_client.get_session_cookie() if _settings.get("remember_login", true) else "",
|
||||
"is_authenticated": _is_authenticated if _settings.get("remember_login", true) else false,
|
||||
"characters": _characters,
|
||||
"selected_character_id": _selected_character_id,
|
||||
"character_limits": _character_limits,
|
||||
"settings": _settings,
|
||||
"timestamp": Time.get_unix_time_from_system()
|
||||
}
|
||||
|
||||
var file := FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE)
|
||||
if file == null:
|
||||
push_error("[StateManager] Failed to open save file for writing: %s" % FileAccess.get_open_error())
|
||||
return
|
||||
|
||||
file.store_string(JSON.stringify(save_data))
|
||||
file.close()
|
||||
|
||||
print("[StateManager] State saved to %s" % SAVE_FILE_PATH)
|
||||
|
||||
|
||||
## Load state from disk
|
||||
func _load_state() -> void:
|
||||
if not FileAccess.file_exists(SAVE_FILE_PATH):
|
||||
print("[StateManager] No save file found, starting fresh")
|
||||
return
|
||||
|
||||
var file := FileAccess.open(SAVE_FILE_PATH, FileAccess.READ)
|
||||
if file == null:
|
||||
push_error("[StateManager] Failed to open save file for reading: %s" % FileAccess.get_open_error())
|
||||
return
|
||||
|
||||
var json_string := file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json := JSON.new()
|
||||
var parse_error := json.parse(json_string)
|
||||
|
||||
if parse_error != OK:
|
||||
push_error("[StateManager] Failed to parse save file")
|
||||
return
|
||||
|
||||
var save_data: Dictionary = json.data
|
||||
|
||||
# Check version
|
||||
if save_data.get("version", 0) != SAVE_VERSION:
|
||||
print("[StateManager] Save file version mismatch, ignoring")
|
||||
return
|
||||
|
||||
# Restore state
|
||||
_current_user = save_data.get("user", {})
|
||||
_auth_token = save_data.get("auth_token", "")
|
||||
_is_authenticated = save_data.get("is_authenticated", false)
|
||||
_characters = []
|
||||
|
||||
# Convert characters array
|
||||
var chars_data = save_data.get("characters", [])
|
||||
for character in chars_data:
|
||||
_characters.append(character)
|
||||
|
||||
_selected_character_id = save_data.get("selected_character_id", "")
|
||||
_character_limits = save_data.get("character_limits", {})
|
||||
_settings = save_data.get("settings", _settings)
|
||||
|
||||
# Update HTTPClient with token if authenticated
|
||||
if _is_authenticated and not _auth_token.is_empty():
|
||||
http_client.set_auth_token(_auth_token)
|
||||
|
||||
# Restore session cookie if present
|
||||
var session_cookie = save_data.get("session_cookie", "")
|
||||
if _is_authenticated and not session_cookie.is_empty():
|
||||
http_client.set_session_cookie(session_cookie)
|
||||
|
||||
print("[StateManager] State loaded from %s" % SAVE_FILE_PATH)
|
||||
print("[StateManager] Authenticated: %s, Characters: %d" % [_is_authenticated, _characters.size()])
|
||||
|
||||
|
||||
## Clear save file
|
||||
func _clear_save_file() -> void:
|
||||
if FileAccess.file_exists(SAVE_FILE_PATH):
|
||||
DirAccess.remove_absolute(SAVE_FILE_PATH)
|
||||
print("[StateManager] Save file cleared")
|
||||
1
godot_client/scripts/services/state_manager.gd.uid
Normal file
1
godot_client/scripts/services/state_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b28q3ngeah5sf
|
||||
Reference in New Issue
Block a user