feat: sync live profile customization
This commit is contained in:
parent
d7c887db21
commit
a7837c3711
9 changed files with 842 additions and 49 deletions
|
|
@ -41,6 +41,21 @@ static func valid_apply_request(data: Variant) -> bool:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func valid_appearance_preview_request(data: Variant) -> bool:
|
||||||
|
return (
|
||||||
|
typeof(data) == TYPE_DICTIONARY
|
||||||
|
and valid_request_id(data.get("request_id"))
|
||||||
|
and typeof(data.get("session_id")) == TYPE_STRING
|
||||||
|
and not str(data.get("session_id", "")).is_empty()
|
||||||
|
and typeof(data.get("appearance")) == TYPE_DICTIONARY
|
||||||
|
and valid_snapshot(data["appearance"])
|
||||||
|
and NetworkIdentityCrypto.valid_fingerprint(
|
||||||
|
data.get("sender_fingerprint")
|
||||||
|
)
|
||||||
|
and typeof(data.get("sender_signature")) == TYPE_PACKED_BYTE_ARRAY
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
static func signature_fields(data: Dictionary) -> Array:
|
static func signature_fields(data: Dictionary) -> Array:
|
||||||
var appearance: Dictionary = data.get("appearance", {})
|
var appearance: Dictionary = data.get("appearance", {})
|
||||||
var result: Array = [
|
var result: Array = [
|
||||||
|
|
@ -54,3 +69,16 @@ static func signature_fields(data: Dictionary) -> Array:
|
||||||
)
|
)
|
||||||
result.append(bool(data.get("use_anyway", false)))
|
result.append(bool(data.get("use_anyway", false)))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
static func appearance_preview_signature_fields(data: Dictionary) -> Array:
|
||||||
|
var appearance: Dictionary = data.get("appearance", {})
|
||||||
|
var result: Array = [
|
||||||
|
str(data.get("session_id", "")),
|
||||||
|
str(data.get("request_id", "")),
|
||||||
|
str(data.get("sender_fingerprint", "")),
|
||||||
|
]
|
||||||
|
result.append_array(
|
||||||
|
CharacterCustomizationCatalog.appearance_signature_values(appearance)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,47 @@ func get_identity_fingerprint() -> String:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func preview_appearance(appearance: Dictionary) -> bool:
|
||||||
|
if not CharacterCustomizationCatalog.validate_snapshot(appearance):
|
||||||
|
return false
|
||||||
|
var snapshot := CharacterCustomizationCatalog.sanitized_snapshot(appearance)
|
||||||
|
var local_peer_id: int = (
|
||||||
|
_session.get_local_peer_id() if _session != null else 1
|
||||||
|
)
|
||||||
|
_apply_to_avatar(local_peer_id, snapshot)
|
||||||
|
if _session == null or not _session.is_gameplay_session_active():
|
||||||
|
return true
|
||||||
|
_session.apply_canonical_profile(
|
||||||
|
local_peer_id,
|
||||||
|
_preferences.display_name,
|
||||||
|
snapshot,
|
||||||
|
)
|
||||||
|
if _session.is_host():
|
||||||
|
_broadcast_appearance_preview_to_supported_peers(
|
||||||
|
local_peer_id,
|
||||||
|
snapshot,
|
||||||
|
)
|
||||||
|
elif _session.supports_server_capability(
|
||||||
|
NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY
|
||||||
|
):
|
||||||
|
var request: Dictionary = {
|
||||||
|
"request_id": _new_id(),
|
||||||
|
"session_id": _session.get_session_id(),
|
||||||
|
"appearance": snapshot.duplicate(true),
|
||||||
|
"sender_fingerprint": _session.get_local_identity_fingerprint(),
|
||||||
|
}
|
||||||
|
request["sender_signature"] = _session.sign_local_action(
|
||||||
|
"appearance_preview",
|
||||||
|
NetworkProfileProtocol.appearance_preview_signature_fields(request),
|
||||||
|
)
|
||||||
|
submit_appearance_preview.rpc_id(1, request)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func restore_persisted_appearance() -> void:
|
||||||
|
preview_appearance(_appearance_store.get_snapshot())
|
||||||
|
|
||||||
|
|
||||||
func request_name_check(display_name: String) -> String:
|
func request_name_check(display_name: String) -> String:
|
||||||
var request_id := _new_id()
|
var request_id := _new_id()
|
||||||
_latest_check_id = request_id
|
_latest_check_id = request_id
|
||||||
|
|
@ -200,6 +241,40 @@ func submit_profile_apply(data: Dictionary) -> void:
|
||||||
_process_apply_request(sender_id, data)
|
_process_apply_request(sender_id, data)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
|
||||||
|
func submit_appearance_preview(data: Dictionary) -> void:
|
||||||
|
var sender_id := multiplayer.get_remote_sender_id()
|
||||||
|
if (
|
||||||
|
not _session.is_host()
|
||||||
|
or not _session.is_authenticated_peer(sender_id)
|
||||||
|
or not NetworkProfileProtocol.valid_appearance_preview_request(data)
|
||||||
|
or str(data["session_id"]) != _session.get_session_id()
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var record := _session.get_peer_record(sender_id)
|
||||||
|
if (
|
||||||
|
record == null
|
||||||
|
or record.identity_fingerprint != str(data["sender_fingerprint"])
|
||||||
|
or not _session.verify_peer_action(
|
||||||
|
sender_id,
|
||||||
|
"appearance_preview",
|
||||||
|
NetworkProfileProtocol.appearance_preview_signature_fields(data),
|
||||||
|
data["sender_signature"],
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var appearance := CharacterCustomizationCatalog.sanitized_snapshot(
|
||||||
|
data["appearance"]
|
||||||
|
)
|
||||||
|
_session.apply_canonical_profile(
|
||||||
|
sender_id,
|
||||||
|
record.display_name,
|
||||||
|
appearance,
|
||||||
|
)
|
||||||
|
_apply_to_avatar(sender_id, appearance)
|
||||||
|
_broadcast_appearance_preview_to_supported_peers(sender_id, appearance)
|
||||||
|
|
||||||
|
|
||||||
func _process_apply_request(peer_id: int, data: Dictionary) -> void:
|
func _process_apply_request(peer_id: int, data: Dictionary) -> void:
|
||||||
var display_name: String = str(data["display_name"]).strip_edges()
|
var display_name: String = str(data["display_name"]).strip_edges()
|
||||||
var conflict: bool = _name_conflicts(peer_id, display_name)
|
var conflict: bool = _name_conflicts(peer_id, display_name)
|
||||||
|
|
@ -256,7 +331,7 @@ func confirm_profile_saved(request_id: String) -> void:
|
||||||
_apply_to_avatar(sender_id, appearance)
|
_apply_to_avatar(sender_id, appearance)
|
||||||
broadcast_profile_snapshot.rpc(
|
broadcast_profile_snapshot.rpc(
|
||||||
sender_id,
|
sender_id,
|
||||||
name,
|
display_name,
|
||||||
appearance,
|
appearance,
|
||||||
pending.get("authorization", {}),
|
pending.get("authorization", {}),
|
||||||
)
|
)
|
||||||
|
|
@ -344,6 +419,8 @@ func _apply_local_result(
|
||||||
_pending_voice_ids.erase(request_id)
|
_pending_voice_ids.erase(request_id)
|
||||||
_pending_speech_speed_ids.erase(request_id)
|
_pending_speech_speed_ids.erase(request_id)
|
||||||
_pending_call_ids.erase(request_id)
|
_pending_call_ids.erase(request_id)
|
||||||
|
if _session != null:
|
||||||
|
_session.set_local_appearance_snapshot(_appearance_store.get_snapshot())
|
||||||
if _session != null and _session.is_gameplay_session_active():
|
if _session != null and _session.is_gameplay_session_active():
|
||||||
if _session.is_host():
|
if _session.is_host():
|
||||||
_session.apply_canonical_profile(
|
_session.apply_canonical_profile(
|
||||||
|
|
@ -408,6 +485,48 @@ func broadcast_profile_snapshot(
|
||||||
profile_snapshot_changed.emit(peer_id, display_name, appearance.duplicate(true))
|
profile_snapshot_changed.emit(peer_id, display_name, appearance.duplicate(true))
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
|
||||||
|
func broadcast_appearance_preview(
|
||||||
|
peer_id: int,
|
||||||
|
appearance: Dictionary,
|
||||||
|
) -> void:
|
||||||
|
if not CharacterCustomizationCatalog.validate_snapshot(appearance):
|
||||||
|
return
|
||||||
|
var record := _session.get_peer_record(peer_id)
|
||||||
|
if record == null:
|
||||||
|
return
|
||||||
|
var snapshot := CharacterCustomizationCatalog.sanitized_snapshot(appearance)
|
||||||
|
_session.apply_canonical_profile(peer_id, record.display_name, snapshot)
|
||||||
|
_apply_to_avatar(peer_id, snapshot)
|
||||||
|
profile_snapshot_changed.emit(
|
||||||
|
peer_id,
|
||||||
|
record.display_name,
|
||||||
|
snapshot.duplicate(true),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _broadcast_appearance_preview_to_supported_peers(
|
||||||
|
peer_id: int,
|
||||||
|
appearance: Dictionary,
|
||||||
|
) -> void:
|
||||||
|
if _session == null or not _session.is_host():
|
||||||
|
return
|
||||||
|
for recipient_id: int in _session.get_authenticated_peer_ids():
|
||||||
|
if (
|
||||||
|
recipient_id == _session.get_local_peer_id()
|
||||||
|
or not _session.peer_supports_capability(
|
||||||
|
recipient_id,
|
||||||
|
NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
broadcast_appearance_preview.rpc_id(
|
||||||
|
recipient_id,
|
||||||
|
peer_id,
|
||||||
|
appearance,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
|
func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
|
||||||
if not _session.is_host():
|
if not _session.is_host():
|
||||||
return
|
return
|
||||||
|
|
@ -422,9 +541,19 @@ func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
|
||||||
record.appearance_snapshot,
|
record.appearance_snapshot,
|
||||||
record.profile_authorization,
|
record.profile_authorization,
|
||||||
)
|
)
|
||||||
|
if _session.peer_supports_capability(
|
||||||
|
peer_id,
|
||||||
|
NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY,
|
||||||
|
):
|
||||||
|
broadcast_appearance_preview.rpc_id(
|
||||||
|
peer_id,
|
||||||
|
existing_id,
|
||||||
|
record.appearance_snapshot,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _on_join_authenticated() -> void:
|
func _on_join_authenticated() -> void:
|
||||||
|
_session.set_local_appearance_snapshot(_appearance_store.get_snapshot())
|
||||||
_apply_to_avatar(
|
_apply_to_avatar(
|
||||||
_session.get_local_peer_id(), _appearance_store.get_snapshot()
|
_session.get_local_peer_id(), _appearance_store.get_snapshot()
|
||||||
)
|
)
|
||||||
|
|
@ -474,6 +603,8 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
_host_pending_apply.clear()
|
_host_pending_apply.clear()
|
||||||
_latest_check_id = ""
|
_latest_check_id = ""
|
||||||
_latest_check_name = ""
|
_latest_check_name = ""
|
||||||
|
_session.set_local_appearance_snapshot(_appearance_store.get_snapshot())
|
||||||
|
_apply_to_avatar(1, _appearance_store.get_snapshot())
|
||||||
|
|
||||||
|
|
||||||
func _emit_conflict_result(data: Dictionary) -> void:
|
func _emit_conflict_result(data: Dictionary) -> void:
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ const WORLD_WEATHER_CAPABILITY: String = "world_weather_v1"
|
||||||
const FISH_QUALITY_CAPABILITY: String = "fish_quality_v1"
|
const FISH_QUALITY_CAPABILITY: String = "fish_quality_v1"
|
||||||
const JOBS_CAPABILITY: String = "jobs_v1"
|
const JOBS_CAPABILITY: String = "jobs_v1"
|
||||||
const WORLD_SPAWN_CAPABILITY: String = "world_spawn_envelope_v1"
|
const WORLD_SPAWN_CAPABILITY: String = "world_spawn_envelope_v1"
|
||||||
|
const APPEARANCE_PREVIEW_CAPABILITY: String = "appearance_preview_v1"
|
||||||
|
|
||||||
enum RejectionCode {
|
enum RejectionCode {
|
||||||
NONE,
|
NONE,
|
||||||
|
|
@ -174,6 +175,7 @@ static func make_client_hello(
|
||||||
WORLD_WEATHER_CAPABILITY,
|
WORLD_WEATHER_CAPABILITY,
|
||||||
JOBS_CAPABILITY,
|
JOBS_CAPABILITY,
|
||||||
WORLD_SPAWN_CAPABILITY,
|
WORLD_SPAWN_CAPABILITY,
|
||||||
|
APPEARANCE_PREVIEW_CAPABILITY,
|
||||||
]),
|
]),
|
||||||
"cosmetic_snapshot": cosmetic_snapshot,
|
"cosmetic_snapshot": cosmetic_snapshot,
|
||||||
"identity_fingerprint": identity_fingerprint,
|
"identity_fingerprint": identity_fingerprint,
|
||||||
|
|
@ -303,6 +305,7 @@ static func make_server_hello(
|
||||||
WORLD_WEATHER_CAPABILITY,
|
WORLD_WEATHER_CAPABILITY,
|
||||||
JOBS_CAPABILITY,
|
JOBS_CAPABILITY,
|
||||||
WORLD_SPAWN_CAPABILITY,
|
WORLD_SPAWN_CAPABILITY,
|
||||||
|
APPEARANCE_PREVIEW_CAPABILITY,
|
||||||
"chat_v1",
|
"chat_v1",
|
||||||
"mail_v1",
|
"mail_v1",
|
||||||
"profile_v1",
|
"profile_v1",
|
||||||
|
|
|
||||||
|
|
@ -559,6 +559,7 @@ func supports_server_capability(capability: StringName) -> bool:
|
||||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_SPAWN_CAPABILITY,
|
NetworkProtocol.WORLD_SPAWN_CAPABILITY,
|
||||||
|
NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY,
|
||||||
"chat_v1",
|
"chat_v1",
|
||||||
"mail_v1",
|
"mail_v1",
|
||||||
"profile_v1",
|
"profile_v1",
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,18 @@ func _validate_fur_color_ui(snapshot: Dictionary) -> void:
|
||||||
))
|
))
|
||||||
var picker_style := picker.get_theme_stylebox("normal") as StyleBoxFlat
|
var picker_style := picker.get_theme_stylebox("normal") as StyleBoxFlat
|
||||||
assert(picker_style != null and picker_style.corner_radius_top_left == 14)
|
assert(picker_style != null and picker_style.corner_radius_top_left == 14)
|
||||||
assert(picker_popup.min_size == ProfilePage.FUR_COLOR_PICKER_POPUP_SIZE)
|
var picker_window_size: Vector2i = profile_page.get_window().size
|
||||||
|
var expected_picker_size := Vector2i(
|
||||||
|
mini(
|
||||||
|
ProfilePage.FUR_COLOR_PICKER_POPUP_SIZE.x,
|
||||||
|
maxi(picker_window_size.x - 32, 420),
|
||||||
|
),
|
||||||
|
mini(
|
||||||
|
ProfilePage.FUR_COLOR_PICKER_POPUP_SIZE.y,
|
||||||
|
maxi(picker_window_size.y - 32, 340),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert(picker_popup.min_size == expected_picker_size)
|
||||||
assert(picker_popup.theme.default_font == UtilityPageStyle.TuffyFont)
|
assert(picker_popup.theme.default_font == UtilityPageStyle.TuffyFont)
|
||||||
var popup_style := picker_popup.get_theme_stylebox(
|
var popup_style := picker_popup.get_theme_stylebox(
|
||||||
"panel"
|
"panel"
|
||||||
|
|
@ -341,11 +352,17 @@ func _validate_fur_color_ui(snapshot: Dictionary) -> void:
|
||||||
assert(popup_style.corner_radius_top_left == 20)
|
assert(popup_style.corner_radius_top_left == 20)
|
||||||
assert(
|
assert(
|
||||||
picker_control.get_theme_constant("sv_width")
|
picker_control.get_theme_constant("sv_width")
|
||||||
== ProfilePage.FUR_COLOR_PICKER_SV_SIZE.x
|
== mini(
|
||||||
|
ProfilePage.FUR_COLOR_PICKER_SV_SIZE.x,
|
||||||
|
expected_picker_size.x - 110,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
assert(
|
assert(
|
||||||
picker_control.get_theme_constant("sv_height")
|
picker_control.get_theme_constant("sv_height")
|
||||||
== ProfilePage.FUR_COLOR_PICKER_SV_SIZE.y
|
== mini(
|
||||||
|
ProfilePage.FUR_COLOR_PICKER_SV_SIZE.y,
|
||||||
|
expected_picker_size.y - 180,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
assert(not picker_control.edit_alpha)
|
assert(not picker_control.edit_alpha)
|
||||||
assert(not picker_control.edit_intensity)
|
assert(not picker_control.edit_intensity)
|
||||||
|
|
|
||||||
175
tests/profile_multiplayer_validation.gd
Normal file
175
tests/profile_multiplayer_validation.gd
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||||
|
const TEST_PORT: int = 18144
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var arguments: PackedStringArray = OS.get_cmdline_user_args()
|
||||||
|
if arguments.has("host"):
|
||||||
|
await _run_host()
|
||||||
|
return
|
||||||
|
if arguments.has("client"):
|
||||||
|
await _run_client()
|
||||||
|
return
|
||||||
|
push_error("Profile multiplayer validation needs host or client mode.")
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _run_host() -> void:
|
||||||
|
var main: Node = await _create_initialized_main()
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
|
assert(session.start_private_host(TEST_PORT))
|
||||||
|
assert(save_manager.initialize_new_game())
|
||||||
|
main.call("_enter_gameplay")
|
||||||
|
for _frame: int in 4:
|
||||||
|
await physics_frame
|
||||||
|
assert(session.set_host_open(true))
|
||||||
|
var remote_peer_id: int = await _wait_for_remote_peer(session)
|
||||||
|
assert(remote_peer_id > 1)
|
||||||
|
var record := session.get_peer_record(remote_peer_id)
|
||||||
|
assert(record != null)
|
||||||
|
assert(
|
||||||
|
NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY
|
||||||
|
in record.capability_flags
|
||||||
|
)
|
||||||
|
var original: Dictionary = record.appearance_snapshot.duplicate(true)
|
||||||
|
var changed: Dictionary = _changed_appearance(original)
|
||||||
|
assert(changed != original)
|
||||||
|
await _wait_for_remote_appearance(session, remote_peer_id, changed)
|
||||||
|
await _wait_for_remote_appearance(session, remote_peer_id, original)
|
||||||
|
await _wait_for_remote_appearance(session, remote_peer_id, changed)
|
||||||
|
var avatar := main.get_node("%PlayerSpawnService").get_avatar(
|
||||||
|
remote_peer_id
|
||||||
|
) as Player
|
||||||
|
assert(avatar != null)
|
||||||
|
assert(avatar.appearance_snapshot == changed)
|
||||||
|
assert(record.display_name != "NetworkProfileService")
|
||||||
|
var disconnect_deadline: int = Time.get_ticks_msec() + 8000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < disconnect_deadline
|
||||||
|
and session.is_authenticated_peer(remote_peer_id)
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(not session.is_authenticated_peer(remote_peer_id))
|
||||||
|
print("Profile multiplayer host validation: PASS")
|
||||||
|
await _session_cleanup(main, session)
|
||||||
|
|
||||||
|
|
||||||
|
func _run_client() -> void:
|
||||||
|
var main: Node = await _create_initialized_main()
|
||||||
|
main.call("_on_title_join_game_requested", "127.0.0.1:%d" % TEST_PORT)
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
var join_deadline: int = Time.get_ticks_msec() + 20000
|
||||||
|
while Time.get_ticks_msec() < join_deadline:
|
||||||
|
await process_frame
|
||||||
|
if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
|
||||||
|
main.call("_confirm_server_trust")
|
||||||
|
if session.is_joined_client() and bool(main.get("_gameplay_started")):
|
||||||
|
break
|
||||||
|
assert(session.is_joined_client())
|
||||||
|
assert(session.supports_server_capability(
|
||||||
|
NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY
|
||||||
|
))
|
||||||
|
var service := main.get_node(
|
||||||
|
"%NetworkProfileService"
|
||||||
|
) as NetworkProfileService
|
||||||
|
var original: Dictionary = service.get_persisted_appearance()
|
||||||
|
var changed: Dictionary = _changed_appearance(original)
|
||||||
|
assert(service.preview_appearance(changed))
|
||||||
|
await create_timer(0.75).timeout
|
||||||
|
service.restore_persisted_appearance()
|
||||||
|
await create_timer(0.75).timeout
|
||||||
|
assert(service.preview_appearance(changed))
|
||||||
|
await create_timer(0.75).timeout
|
||||||
|
var apply_results: Array = []
|
||||||
|
service.apply_finished.connect(func(accepted: bool, message: String) -> void:
|
||||||
|
apply_results.append([accepted, message])
|
||||||
|
)
|
||||||
|
assert(service.apply_profile(
|
||||||
|
service.get_persisted_name(),
|
||||||
|
changed,
|
||||||
|
true,
|
||||||
|
service.get_persisted_voice_id(),
|
||||||
|
service.get_persisted_speech_speed_id(),
|
||||||
|
service.get_persisted_call_id(),
|
||||||
|
))
|
||||||
|
var apply_deadline: int = Time.get_ticks_msec() + 8000
|
||||||
|
while Time.get_ticks_msec() < apply_deadline and apply_results.is_empty():
|
||||||
|
await process_frame
|
||||||
|
assert(not apply_results.is_empty())
|
||||||
|
assert(bool(apply_results[0][0]))
|
||||||
|
assert(service.get_persisted_appearance() == changed)
|
||||||
|
assert(Dictionary(session.get("_local_appearance_snapshot")) == changed)
|
||||||
|
await create_timer(0.75).timeout
|
||||||
|
print("Profile multiplayer client validation: PASS")
|
||||||
|
await _session_cleanup(main, session)
|
||||||
|
|
||||||
|
|
||||||
|
func _changed_appearance(original: Dictionary) -> Dictionary:
|
||||||
|
var changed := original.duplicate(true)
|
||||||
|
for category_id: String in CharacterCustomizationCatalog.CATEGORY_IDS:
|
||||||
|
if category_id in CharacterCustomizationCatalog.FUR_STYLE_IDS:
|
||||||
|
continue
|
||||||
|
var options: Array = CharacterCustomizationCatalog.options_for(category_id)
|
||||||
|
for option: Dictionary in options:
|
||||||
|
var option_id: Variant = option.get("id")
|
||||||
|
if option_id != changed.get(category_id):
|
||||||
|
changed[category_id] = option_id
|
||||||
|
if CharacterCustomizationCatalog.validate_snapshot(changed):
|
||||||
|
return changed
|
||||||
|
changed[category_id] = original.get(category_id)
|
||||||
|
assert(false, "No alternate appearance option is available.")
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_for_remote_appearance(
|
||||||
|
session: NetworkSession,
|
||||||
|
peer_id: int,
|
||||||
|
expected: Dictionary,
|
||||||
|
) -> void:
|
||||||
|
var deadline: int = Time.get_ticks_msec() + 8000
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
await process_frame
|
||||||
|
var record := session.get_peer_record(peer_id)
|
||||||
|
if record != null and record.appearance_snapshot == expected:
|
||||||
|
return
|
||||||
|
assert(false, "Timed out waiting for a remote appearance update.")
|
||||||
|
|
||||||
|
|
||||||
|
func _create_initialized_main() -> Node:
|
||||||
|
root.size = Vector2i(1280, 720)
|
||||||
|
var main: Node = MainScene.instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
if not bool(main.get("_application_initialized")):
|
||||||
|
main.call("_activate_selected_data_path", "", true)
|
||||||
|
for _frame: int in 8:
|
||||||
|
await process_frame
|
||||||
|
assert(bool(main.get("_application_initialized")))
|
||||||
|
return main
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_for_remote_peer(session: NetworkSession) -> int:
|
||||||
|
var deadline: int = Time.get_ticks_msec() + 20000
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
await process_frame
|
||||||
|
for peer_id: int in session.get_authenticated_peer_ids():
|
||||||
|
if peer_id != session.get_local_peer_id():
|
||||||
|
return peer_id
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
func _session_cleanup(main: Node, session: NetworkSession) -> void:
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
await create_timer(0.1).timeout
|
||||||
|
quit()
|
||||||
1
tests/profile_multiplayer_validation.gd.uid
Normal file
1
tests/profile_multiplayer_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://qvk8y0fd7o2j
|
||||||
|
|
@ -2,6 +2,7 @@ class_name ProfilePage
|
||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
const CHECK_DEBOUNCE_SECONDS: float = 0.4
|
const CHECK_DEBOUNCE_SECONDS: float = 0.4
|
||||||
|
const APPEARANCE_PREVIEW_INTERVAL_SECONDS: float = 0.08
|
||||||
const OPTION_GRID_COLUMNS: int = 6
|
const OPTION_GRID_COLUMNS: int = 6
|
||||||
const FUR_PALETTE_GRID_COLUMNS: int = 10
|
const FUR_PALETTE_GRID_COLUMNS: int = 10
|
||||||
const FUR_PATTERN_GRID_COLUMNS: int = 4
|
const FUR_PATTERN_GRID_COLUMNS: int = 4
|
||||||
|
|
@ -27,6 +28,13 @@ const VoiceProfilesType = preload(
|
||||||
)
|
)
|
||||||
const VOICE_CATEGORY_ID: String = "voice"
|
const VOICE_CATEGORY_ID: String = "voice"
|
||||||
|
|
||||||
|
enum ControllerZone {
|
||||||
|
ACCOUNT,
|
||||||
|
CATEGORIES,
|
||||||
|
OPTIONS,
|
||||||
|
COLOR_PICKER,
|
||||||
|
}
|
||||||
|
|
||||||
var _service: NetworkProfileService
|
var _service: NetworkProfileService
|
||||||
var _experience: PlayerExperience
|
var _experience: PlayerExperience
|
||||||
var _draft_name: String = ""
|
var _draft_name: String = ""
|
||||||
|
|
@ -49,14 +57,18 @@ var _suggestions: HBoxContainer
|
||||||
var _category_list: VBoxContainer
|
var _category_list: VBoxContainer
|
||||||
var _option_list: VBoxContainer
|
var _option_list: VBoxContainer
|
||||||
var _preview: ProfilePreview
|
var _preview: ProfilePreview
|
||||||
|
var _customize_button: Button
|
||||||
var _apply_button: Button
|
var _apply_button: Button
|
||||||
var _revert_button: Button
|
var _revert_button: Button
|
||||||
|
var _defaults_button: Button
|
||||||
|
var _reset_view_button: Button
|
||||||
var _discard_confirmation: PanelContainer
|
var _discard_confirmation: PanelContainer
|
||||||
var _confirmation_label: Label
|
var _confirmation_label: Label
|
||||||
var _confirmation_confirm: Button
|
var _confirmation_confirm: Button
|
||||||
var _keep_editing_button: Button
|
var _keep_editing_button: Button
|
||||||
var _confirmation_action: String = ""
|
var _confirmation_action: String = ""
|
||||||
var _debounce: Timer
|
var _debounce: Timer
|
||||||
|
var _appearance_preview_timer: Timer
|
||||||
var _experience_level: Label
|
var _experience_level: Label
|
||||||
var _experience_progress: ProgressBar
|
var _experience_progress: ProgressBar
|
||||||
var _experience_value: Label
|
var _experience_value: Label
|
||||||
|
|
@ -76,6 +88,13 @@ var _fur_color_channel_buttons: Dictionary[String, Button] = {}
|
||||||
var _fur_color_channel_swatches: Dictionary[String, Panel] = {}
|
var _fur_color_channel_swatches: Dictionary[String, Panel] = {}
|
||||||
var _fur_custom_color_display: Panel
|
var _fur_custom_color_display: Panel
|
||||||
var _fur_custom_color_label: Label
|
var _fur_custom_color_label: Label
|
||||||
|
var _controller_mapping_manager: ControllerMappingManagerType
|
||||||
|
var _controller_zone: ControllerZone = ControllerZone.ACCOUNT
|
||||||
|
var _controller_option_depth: int = 0
|
||||||
|
var _active_color_picker_button: ColorPickerButton
|
||||||
|
var _active_color_picker_return_depth: int = 0
|
||||||
|
var _profile_active: bool = false
|
||||||
|
var _profile_interactive: bool = false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -86,10 +105,17 @@ func _ready() -> void:
|
||||||
_debounce.wait_time = CHECK_DEBOUNCE_SECONDS
|
_debounce.wait_time = CHECK_DEBOUNCE_SECONDS
|
||||||
_debounce.timeout.connect(_request_conflict_check)
|
_debounce.timeout.connect(_request_conflict_check)
|
||||||
add_child(_debounce)
|
add_child(_debounce)
|
||||||
|
_appearance_preview_timer = Timer.new()
|
||||||
|
_appearance_preview_timer.one_shot = true
|
||||||
|
_appearance_preview_timer.wait_time = APPEARANCE_PREVIEW_INTERVAL_SECONDS
|
||||||
|
_appearance_preview_timer.timeout.connect(_publish_draft_appearance)
|
||||||
|
add_child(_appearance_preview_timer)
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
_cancel_feature_preview_requests()
|
_cancel_feature_preview_requests()
|
||||||
|
if _service != null:
|
||||||
|
_service.restore_persisted_appearance()
|
||||||
|
|
||||||
|
|
||||||
func setup(
|
func setup(
|
||||||
|
|
@ -127,6 +153,7 @@ func setup(
|
||||||
func setup_controller_mapping(
|
func setup_controller_mapping(
|
||||||
mapping_manager: ControllerMappingManagerType,
|
mapping_manager: ControllerMappingManagerType,
|
||||||
) -> void:
|
) -> void:
|
||||||
|
_controller_mapping_manager = mapping_manager
|
||||||
if _preview != null:
|
if _preview != null:
|
||||||
_preview.setup_controller_mapping(mapping_manager)
|
_preview.setup_controller_mapping(mapping_manager)
|
||||||
|
|
||||||
|
|
@ -137,6 +164,7 @@ func set_world_pixel_size(pixel_size: int) -> void:
|
||||||
|
|
||||||
|
|
||||||
func activate() -> void:
|
func activate() -> void:
|
||||||
|
_profile_active = true
|
||||||
visible = true
|
visible = true
|
||||||
if _service != null:
|
if _service != null:
|
||||||
_load_persisted()
|
_load_persisted()
|
||||||
|
|
@ -145,8 +173,12 @@ func activate() -> void:
|
||||||
|
|
||||||
|
|
||||||
func deactivate() -> void:
|
func deactivate() -> void:
|
||||||
|
_profile_active = false
|
||||||
visible = false
|
visible = false
|
||||||
_debounce.stop()
|
_debounce.stop()
|
||||||
|
_appearance_preview_timer.stop()
|
||||||
|
if _service != null:
|
||||||
|
_service.restore_persisted_appearance()
|
||||||
_cancel_feature_preview_requests()
|
_cancel_feature_preview_requests()
|
||||||
if _voice_preview_tween != null and _voice_preview_tween.is_valid():
|
if _voice_preview_tween != null and _voice_preview_tween.is_valid():
|
||||||
_voice_preview_tween.kill()
|
_voice_preview_tween.kill()
|
||||||
|
|
@ -154,16 +186,24 @@ func deactivate() -> void:
|
||||||
|
|
||||||
|
|
||||||
func set_interactive(interactive: bool) -> void:
|
func set_interactive(interactive: bool) -> void:
|
||||||
|
_profile_interactive = interactive and _profile_active
|
||||||
mouse_filter = (
|
mouse_filter = (
|
||||||
Control.MOUSE_FILTER_PASS if interactive else Control.MOUSE_FILTER_IGNORE
|
Control.MOUSE_FILTER_PASS
|
||||||
|
if _profile_interactive else Control.MOUSE_FILTER_IGNORE
|
||||||
)
|
)
|
||||||
|
_apply_controller_zone_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func reset_controller_zone() -> void:
|
||||||
|
_controller_zone = ControllerZone.ACCOUNT
|
||||||
|
_controller_option_depth = 0
|
||||||
|
_apply_controller_zone_focus()
|
||||||
|
call_deferred("_focus_controller_zone")
|
||||||
|
|
||||||
|
|
||||||
func consume_escape() -> bool:
|
func consume_escape() -> bool:
|
||||||
if _discard_confirmation.visible:
|
if _discard_confirmation.visible:
|
||||||
_discard_confirmation.visible = false
|
_close_discard_confirmation()
|
||||||
_confirmation_action = ""
|
|
||||||
_name_edit.grab_focus()
|
|
||||||
return true
|
return true
|
||||||
if _name_edit.has_focus():
|
if _name_edit.has_focus():
|
||||||
_name_edit.release_focus()
|
_name_edit.release_focus()
|
||||||
|
|
@ -174,6 +214,257 @@ func consume_escape() -> bool:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func handle_controller_input(event: InputEvent) -> bool:
|
||||||
|
if not _profile_active or not _profile_interactive:
|
||||||
|
return false
|
||||||
|
if event.is_action_pressed("ui_cancel"):
|
||||||
|
if _discard_confirmation.visible:
|
||||||
|
_close_discard_confirmation()
|
||||||
|
return true
|
||||||
|
match _controller_zone:
|
||||||
|
ControllerZone.COLOR_PICKER:
|
||||||
|
_close_controller_color_picker()
|
||||||
|
ControllerZone.OPTIONS:
|
||||||
|
if _controller_option_depth > 0:
|
||||||
|
_controller_option_depth -= 1
|
||||||
|
_apply_controller_zone_focus()
|
||||||
|
call_deferred("_focus_controller_zone")
|
||||||
|
else:
|
||||||
|
_controller_zone = ControllerZone.CATEGORIES
|
||||||
|
_apply_controller_zone_focus()
|
||||||
|
call_deferred("_focus_controller_zone")
|
||||||
|
ControllerZone.CATEGORIES:
|
||||||
|
_controller_zone = ControllerZone.ACCOUNT
|
||||||
|
_apply_controller_zone_focus()
|
||||||
|
call_deferred("_focus_controller_zone")
|
||||||
|
ControllerZone.ACCOUNT:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
if _controller_zone == ControllerZone.ACCOUNT:
|
||||||
|
return false
|
||||||
|
if (
|
||||||
|
_controller_zone == ControllerZone.CATEGORIES
|
||||||
|
and event.is_action_pressed("ui_accept")
|
||||||
|
):
|
||||||
|
var focused_category := get_viewport().gui_get_focus_owner() as Button
|
||||||
|
if focused_category != null and _category_list.is_ancestor_of(
|
||||||
|
focused_category
|
||||||
|
):
|
||||||
|
focused_category.pressed.emit()
|
||||||
|
_controller_zone = ControllerZone.OPTIONS
|
||||||
|
_controller_option_depth = 0
|
||||||
|
_apply_controller_zone_focus.call_deferred()
|
||||||
|
_focus_controller_zone.call_deferred()
|
||||||
|
return true
|
||||||
|
if (
|
||||||
|
_controller_zone == ControllerZone.OPTIONS
|
||||||
|
and event.is_action_pressed("ui_accept")
|
||||||
|
):
|
||||||
|
var groups: Array = _controller_option_groups()
|
||||||
|
if _controller_option_depth < groups.size() - 1:
|
||||||
|
var focused_option := get_viewport().gui_get_focus_owner() as BaseButton
|
||||||
|
_controller_option_depth += 1
|
||||||
|
if focused_option != null:
|
||||||
|
focused_option.pressed.emit()
|
||||||
|
_apply_controller_zone_focus.call_deferred()
|
||||||
|
_focus_controller_zone.call_deferred()
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if (
|
||||||
|
not _profile_active
|
||||||
|
or not _profile_interactive
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var right_stick := Vector2(
|
||||||
|
_controller_mapping_manager.get_role_axis(
|
||||||
|
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
|
||||||
|
)
|
||||||
|
if _controller_mapping_manager != null
|
||||||
|
else Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
|
||||||
|
_controller_mapping_manager.get_role_axis(
|
||||||
|
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
|
||||||
|
)
|
||||||
|
if _controller_mapping_manager != null
|
||||||
|
else Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
|
||||||
|
)
|
||||||
|
if right_stick.length() < 0.16:
|
||||||
|
return
|
||||||
|
if _controller_zone == ControllerZone.COLOR_PICKER:
|
||||||
|
_adjust_controller_color_gamut(right_stick, delta)
|
||||||
|
else:
|
||||||
|
_preview.apply_controller_orbit(right_stick, delta)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_controller_zone_focus() -> void:
|
||||||
|
if not is_node_ready():
|
||||||
|
return
|
||||||
|
var account_controls: Array[Control] = _account_controller_controls()
|
||||||
|
var category_controls: Array[Control] = _controls_under(_category_list)
|
||||||
|
var option_groups: Array = _controller_option_groups()
|
||||||
|
var all_option_controls: Array[Control] = []
|
||||||
|
for group_value: Variant in option_groups:
|
||||||
|
var group := group_value as Array
|
||||||
|
for item: Variant in group:
|
||||||
|
var control := item as Control
|
||||||
|
if control != null and control not in all_option_controls:
|
||||||
|
all_option_controls.append(control)
|
||||||
|
for control: Control in account_controls + category_controls + all_option_controls:
|
||||||
|
control.focus_mode = Control.FOCUS_NONE
|
||||||
|
for control: Control in [_confirmation_confirm, _keep_editing_button]:
|
||||||
|
if control != null:
|
||||||
|
control.focus_mode = Control.FOCUS_NONE
|
||||||
|
if _preview != null:
|
||||||
|
_preview.focus_mode = Control.FOCUS_NONE
|
||||||
|
if _reset_view_button != null:
|
||||||
|
_reset_view_button.focus_mode = Control.FOCUS_NONE
|
||||||
|
if not _profile_interactive:
|
||||||
|
return
|
||||||
|
if _discard_confirmation != null and _discard_confirmation.visible:
|
||||||
|
var confirmation_controls: Array[Control] = [
|
||||||
|
_confirmation_confirm,
|
||||||
|
_keep_editing_button,
|
||||||
|
]
|
||||||
|
for control: Control in confirmation_controls:
|
||||||
|
control.focus_mode = Control.FOCUS_ALL
|
||||||
|
ControllerFocusNavigation.configure_spatial_neighbors(
|
||||||
|
confirmation_controls
|
||||||
|
)
|
||||||
|
return
|
||||||
|
var active_controls: Array[Control] = []
|
||||||
|
match _controller_zone:
|
||||||
|
ControllerZone.ACCOUNT:
|
||||||
|
active_controls = account_controls
|
||||||
|
ControllerZone.CATEGORIES:
|
||||||
|
active_controls = category_controls
|
||||||
|
ControllerZone.OPTIONS:
|
||||||
|
if not option_groups.is_empty():
|
||||||
|
_controller_option_depth = clampi(
|
||||||
|
_controller_option_depth,
|
||||||
|
0,
|
||||||
|
option_groups.size() - 1,
|
||||||
|
)
|
||||||
|
for item: Variant in option_groups[_controller_option_depth]:
|
||||||
|
var control := item as Control
|
||||||
|
if control != null:
|
||||||
|
active_controls.append(control)
|
||||||
|
ControllerZone.COLOR_PICKER:
|
||||||
|
active_controls = _color_picker_controller_controls()
|
||||||
|
for control: Control in active_controls:
|
||||||
|
var button := control as BaseButton
|
||||||
|
control.focus_mode = (
|
||||||
|
Control.FOCUS_ALL
|
||||||
|
if button == null or not button.disabled
|
||||||
|
else Control.FOCUS_NONE
|
||||||
|
)
|
||||||
|
ControllerFocusNavigation.configure_spatial_neighbors(active_controls)
|
||||||
|
|
||||||
|
|
||||||
|
func _focus_controller_zone() -> void:
|
||||||
|
if not _profile_interactive:
|
||||||
|
return
|
||||||
|
var controls: Array[Control] = []
|
||||||
|
match _controller_zone:
|
||||||
|
ControllerZone.ACCOUNT:
|
||||||
|
controls = _account_controller_controls()
|
||||||
|
ControllerZone.CATEGORIES:
|
||||||
|
controls = _controls_under(_category_list)
|
||||||
|
ControllerZone.OPTIONS:
|
||||||
|
var groups: Array = _controller_option_groups()
|
||||||
|
if not groups.is_empty():
|
||||||
|
for item: Variant in groups[clampi(
|
||||||
|
_controller_option_depth, 0, groups.size() - 1
|
||||||
|
)]:
|
||||||
|
var option_control := item as Control
|
||||||
|
if option_control != null:
|
||||||
|
controls.append(option_control)
|
||||||
|
ControllerZone.COLOR_PICKER:
|
||||||
|
controls = _color_picker_controller_controls()
|
||||||
|
for control: Control in controls:
|
||||||
|
var button := control as BaseButton
|
||||||
|
if button != null and button.button_pressed:
|
||||||
|
button.grab_focus()
|
||||||
|
return
|
||||||
|
for control: Control in controls:
|
||||||
|
if control.focus_mode != Control.FOCUS_NONE:
|
||||||
|
control.grab_focus()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
func _account_controller_controls() -> Array[Control]:
|
||||||
|
var controls: Array[Control] = []
|
||||||
|
for control: Control in [
|
||||||
|
_name_edit,
|
||||||
|
_customize_button,
|
||||||
|
_apply_button,
|
||||||
|
_revert_button,
|
||||||
|
_defaults_button,
|
||||||
|
]:
|
||||||
|
if control != null and control.is_visible_in_tree():
|
||||||
|
controls.append(control)
|
||||||
|
for control: Control in _controls_under(_suggestions):
|
||||||
|
if control not in controls:
|
||||||
|
controls.append(control)
|
||||||
|
return controls
|
||||||
|
|
||||||
|
|
||||||
|
func _controller_option_groups() -> Array:
|
||||||
|
var groups: Array = []
|
||||||
|
if _option_list == null:
|
||||||
|
return groups
|
||||||
|
if _category_id != "fur_pattern":
|
||||||
|
groups.append(_controls_under(_option_list))
|
||||||
|
return groups
|
||||||
|
var section_tabs := _option_list.find_child(
|
||||||
|
"FurSectionTabs", true, false
|
||||||
|
) as Node
|
||||||
|
groups.append(_controls_under(section_tabs))
|
||||||
|
if _active_fur_section == FUR_SECTION_PATTERNS:
|
||||||
|
groups.append(_controls_under(_option_list.find_child(
|
||||||
|
"FurPatternPartTabs", true, false
|
||||||
|
)))
|
||||||
|
groups.append(_controls_under(_option_list.find_child(
|
||||||
|
"FurPatternGrid", true, false
|
||||||
|
)))
|
||||||
|
else:
|
||||||
|
groups.append(_controls_under(_option_list.find_child(
|
||||||
|
"FurColorChannelGrid", true, false
|
||||||
|
)))
|
||||||
|
var palette_controls: Array[Control] = _controls_under(
|
||||||
|
_option_list.find_child("FurPaletteGrid", true, false)
|
||||||
|
)
|
||||||
|
var custom_picker := _option_list.find_child(
|
||||||
|
"FurCustomColorPicker", true, false
|
||||||
|
) as Control
|
||||||
|
if custom_picker != null:
|
||||||
|
palette_controls.append(custom_picker)
|
||||||
|
groups.append(palette_controls)
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
func _controls_under(root: Node) -> Array[Control]:
|
||||||
|
var controls: Array[Control] = []
|
||||||
|
if root == null:
|
||||||
|
return controls
|
||||||
|
_collect_controller_controls(root, controls)
|
||||||
|
return controls
|
||||||
|
|
||||||
|
|
||||||
|
func _collect_controller_controls(
|
||||||
|
root: Node,
|
||||||
|
output: Array[Control],
|
||||||
|
) -> void:
|
||||||
|
for child: Node in root.get_children():
|
||||||
|
var control := child as Control
|
||||||
|
if control != null and not control.is_visible_in_tree():
|
||||||
|
continue
|
||||||
|
if control is BaseButton or control is Slider or control is LineEdit:
|
||||||
|
output.append(control)
|
||||||
|
_collect_controller_controls(child, output)
|
||||||
|
|
||||||
|
|
||||||
func has_unsaved_changes() -> bool:
|
func has_unsaved_changes() -> bool:
|
||||||
return _dirty
|
return _dirty
|
||||||
|
|
||||||
|
|
@ -295,6 +586,12 @@ func _build_ui() -> void:
|
||||||
actions.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
actions.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||||
actions.add_theme_constant_override("separation", 7)
|
actions.add_theme_constant_override("separation", 7)
|
||||||
account_row.add_child(actions)
|
account_row.add_child(actions)
|
||||||
|
_customize_button = Button.new()
|
||||||
|
_customize_button.text = "customize"
|
||||||
|
_customize_button.custom_minimum_size.x = 96.0
|
||||||
|
UtilityPageStyle.apply_compact_ocean_button(_customize_button)
|
||||||
|
_customize_button.pressed.connect(_enter_controller_customization)
|
||||||
|
actions.add_child(_customize_button)
|
||||||
_apply_button = Button.new()
|
_apply_button = Button.new()
|
||||||
_apply_button.text = "apply"
|
_apply_button.text = "apply"
|
||||||
_apply_button.custom_minimum_size.x = 72.0
|
_apply_button.custom_minimum_size.x = 72.0
|
||||||
|
|
@ -307,12 +604,12 @@ func _build_ui() -> void:
|
||||||
UtilityPageStyle.apply_compact_ocean_button(_revert_button)
|
UtilityPageStyle.apply_compact_ocean_button(_revert_button)
|
||||||
_revert_button.pressed.connect(_revert)
|
_revert_button.pressed.connect(_revert)
|
||||||
actions.add_child(_revert_button)
|
actions.add_child(_revert_button)
|
||||||
var defaults_button := Button.new()
|
_defaults_button = Button.new()
|
||||||
defaults_button.text = "defaults"
|
_defaults_button.text = "defaults"
|
||||||
defaults_button.custom_minimum_size.x = 82.0
|
_defaults_button.custom_minimum_size.x = 82.0
|
||||||
UtilityPageStyle.apply_compact_ocean_button(defaults_button)
|
UtilityPageStyle.apply_compact_ocean_button(_defaults_button)
|
||||||
defaults_button.pressed.connect(_show_confirmation.bind("defaults"))
|
_defaults_button.pressed.connect(_show_confirmation.bind("defaults"))
|
||||||
actions.add_child(defaults_button)
|
actions.add_child(_defaults_button)
|
||||||
|
|
||||||
var body_panel := PanelContainer.new()
|
var body_panel := PanelContainer.new()
|
||||||
body_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
body_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
|
@ -377,16 +674,17 @@ func _build_ui() -> void:
|
||||||
_preview.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
_preview.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
preview_layer.add_child(_preview)
|
preview_layer.add_child(_preview)
|
||||||
_preview.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
_preview.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
var reset_view := Button.new()
|
_reset_view_button = Button.new()
|
||||||
reset_view.text = "↶"
|
_reset_view_button.text = "↶"
|
||||||
reset_view.tooltip_text = "reset view"
|
_reset_view_button.tooltip_text = "reset view"
|
||||||
reset_view.custom_minimum_size = Vector2(36.0, 36.0)
|
_reset_view_button.custom_minimum_size = Vector2(36.0, 36.0)
|
||||||
reset_view.position = Vector2(-8.0, -8.0)
|
_reset_view_button.position = Vector2(-8.0, -8.0)
|
||||||
reset_view.z_index = 2
|
_reset_view_button.z_index = 2
|
||||||
reset_view.pressed.connect(_preview.reset_view)
|
_reset_view_button.focus_mode = Control.FOCUS_NONE
|
||||||
UtilityPageStyle.apply_compact_ocean_button(reset_view)
|
_reset_view_button.pressed.connect(_preview.reset_view)
|
||||||
reset_view.add_theme_font_size_override("font_size", 22)
|
UtilityPageStyle.apply_compact_ocean_button(_reset_view_button)
|
||||||
preview_layer.add_child(reset_view)
|
_reset_view_button.add_theme_font_size_override("font_size", 22)
|
||||||
|
preview_layer.add_child(_reset_view_button)
|
||||||
|
|
||||||
_discard_confirmation = PanelContainer.new()
|
_discard_confirmation = PanelContainer.new()
|
||||||
_discard_confirmation.visible = false
|
_discard_confirmation.visible = false
|
||||||
|
|
@ -414,16 +712,22 @@ func _build_ui() -> void:
|
||||||
confirm_buttons.add_child(_confirmation_confirm)
|
confirm_buttons.add_child(_confirmation_confirm)
|
||||||
_keep_editing_button = Button.new()
|
_keep_editing_button = Button.new()
|
||||||
_keep_editing_button.text = "keep editing"
|
_keep_editing_button.text = "keep editing"
|
||||||
_keep_editing_button.pressed.connect(func() -> void:
|
_keep_editing_button.pressed.connect(_close_discard_confirmation)
|
||||||
_discard_confirmation.visible = false
|
|
||||||
_name_edit.grab_focus()
|
|
||||||
)
|
|
||||||
UtilityPageStyle.apply_ocean_button(_keep_editing_button)
|
UtilityPageStyle.apply_ocean_button(_keep_editing_button)
|
||||||
confirm_buttons.add_child(_keep_editing_button)
|
confirm_buttons.add_child(_keep_editing_button)
|
||||||
|
|
||||||
_build_categories()
|
_build_categories()
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_controller_customization() -> void:
|
||||||
|
if not _profile_active or not _profile_interactive:
|
||||||
|
return
|
||||||
|
_controller_zone = ControllerZone.CATEGORIES
|
||||||
|
_controller_option_depth = 0
|
||||||
|
_apply_controller_zone_focus()
|
||||||
|
call_deferred("_focus_controller_zone")
|
||||||
|
|
||||||
|
|
||||||
func _build_categories() -> void:
|
func _build_categories() -> void:
|
||||||
for child: Node in _category_list.get_children():
|
for child: Node in _category_list.get_children():
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
|
|
@ -461,7 +765,9 @@ func _refresh_options() -> void:
|
||||||
_fur_custom_color_display = null
|
_fur_custom_color_display = null
|
||||||
_fur_custom_color_label = null
|
_fur_custom_color_label = null
|
||||||
for child: Node in _option_list.get_children():
|
for child: Node in _option_list.get_children():
|
||||||
|
_option_list.remove_child(child)
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
|
_refresh_controller_zone_after_options.call_deferred()
|
||||||
if _category_id == VOICE_CATEGORY_ID:
|
if _category_id == VOICE_CATEGORY_ID:
|
||||||
_build_voice_options()
|
_build_voice_options()
|
||||||
return
|
return
|
||||||
|
|
@ -994,6 +1300,7 @@ func _resize_feature_preview_image(image: Image, max_dimension: int) -> void:
|
||||||
|
|
||||||
func _build_fur_options(options: Array) -> void:
|
func _build_fur_options(options: Array) -> void:
|
||||||
var section_tabs := HBoxContainer.new()
|
var section_tabs := HBoxContainer.new()
|
||||||
|
section_tabs.name = "FurSectionTabs"
|
||||||
section_tabs.add_theme_constant_override("separation", 8)
|
section_tabs.add_theme_constant_override("separation", 8)
|
||||||
_option_list.add_child(section_tabs)
|
_option_list.add_child(section_tabs)
|
||||||
for section_id: String in [FUR_SECTION_PATTERNS, FUR_SECTION_COLORS]:
|
for section_id: String in [FUR_SECTION_PATTERNS, FUR_SECTION_COLORS]:
|
||||||
|
|
@ -1075,6 +1382,7 @@ func _build_fur_pattern_options() -> void:
|
||||||
pattern_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
|
pattern_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
|
||||||
pattern_margin.add_child(pattern_scroll)
|
pattern_margin.add_child(pattern_scroll)
|
||||||
var pattern_grid := GridContainer.new()
|
var pattern_grid := GridContainer.new()
|
||||||
|
pattern_grid.name = "FurPatternGrid"
|
||||||
pattern_grid.columns = FUR_PATTERN_GRID_COLUMNS
|
pattern_grid.columns = FUR_PATTERN_GRID_COLUMNS
|
||||||
pattern_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
pattern_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
pattern_grid.add_theme_constant_override("h_separation", 8)
|
pattern_grid.add_theme_constant_override("h_separation", 8)
|
||||||
|
|
@ -1270,6 +1578,7 @@ func _build_fur_palette(
|
||||||
custom_picker.tooltip_text = "Choose a custom color for channel %d." % (
|
custom_picker.tooltip_text = "Choose a custom color for channel %d." % (
|
||||||
CharacterCustomizationCatalog.FUR_COLOR_IDS.find(category_id) + 1
|
CharacterCustomizationCatalog.FUR_COLOR_IDS.find(category_id) + 1
|
||||||
)
|
)
|
||||||
|
custom_picker.set_meta(&"fur_color_category_id", category_id)
|
||||||
custom_picker.color_changed.connect(
|
custom_picker.color_changed.connect(
|
||||||
_select_custom_fur_color.bind(category_id)
|
_select_custom_fur_color.bind(category_id)
|
||||||
)
|
)
|
||||||
|
|
@ -1341,6 +1650,7 @@ func _select_custom_fur_color(color: Color, category_id: String) -> void:
|
||||||
)
|
)
|
||||||
_update_fur_custom_color_display(color)
|
_update_fur_custom_color_display(color)
|
||||||
_preview.apply_appearance_profile(_draft_appearance)
|
_preview.apply_appearance_profile(_draft_appearance)
|
||||||
|
_queue_draft_appearance_preview()
|
||||||
_dirty = _draft_differs()
|
_dirty = _draft_differs()
|
||||||
_refresh_actions()
|
_refresh_actions()
|
||||||
|
|
||||||
|
|
@ -1417,8 +1727,7 @@ func _theme_fur_color_picker_popup(button: ColorPickerButton) -> void:
|
||||||
var picker: ColorPicker = button.get_picker()
|
var picker: ColorPicker = button.get_picker()
|
||||||
popup.accessibility_name = "custom fur color picker"
|
popup.accessibility_name = "custom fur color picker"
|
||||||
popup.theme = GameTheme
|
popup.theme = GameTheme
|
||||||
popup.min_size = FUR_COLOR_PICKER_POPUP_SIZE
|
_fit_color_picker_popup(button)
|
||||||
popup.size = FUR_COLOR_PICKER_POPUP_SIZE
|
|
||||||
var popup_style := UtilityPageStyle.rounded_style(
|
var popup_style := UtilityPageStyle.rounded_style(
|
||||||
UtilityPageStyle.OCEAN_PANEL_DEEP,
|
UtilityPageStyle.OCEAN_PANEL_DEEP,
|
||||||
20,
|
20,
|
||||||
|
|
@ -1428,19 +1737,119 @@ func _theme_fur_color_picker_popup(button: ColorPickerButton) -> void:
|
||||||
popup_style.content_margin_right = 24.0
|
popup_style.content_margin_right = 24.0
|
||||||
popup_style.content_margin_bottom = 24.0
|
popup_style.content_margin_bottom = 24.0
|
||||||
popup.add_theme_stylebox_override("panel", popup_style)
|
popup.add_theme_stylebox_override("panel", popup_style)
|
||||||
picker.custom_minimum_size = Vector2(
|
|
||||||
FUR_COLOR_PICKER_POPUP_SIZE.x - 48,
|
|
||||||
FUR_COLOR_PICKER_POPUP_SIZE.y - 48,
|
|
||||||
)
|
|
||||||
picker.edit_alpha = false
|
picker.edit_alpha = false
|
||||||
picker.edit_intensity = false
|
picker.edit_intensity = false
|
||||||
picker.can_add_swatches = false
|
picker.can_add_swatches = false
|
||||||
picker.presets_visible = false
|
picker.presets_visible = false
|
||||||
picker.add_theme_constant_override("margin", 16)
|
picker.add_theme_constant_override("margin", 16)
|
||||||
picker.add_theme_constant_override("sv_width", FUR_COLOR_PICKER_SV_SIZE.x)
|
|
||||||
picker.add_theme_constant_override("sv_height", FUR_COLOR_PICKER_SV_SIZE.y)
|
|
||||||
picker.add_theme_constant_override("h_width", 38)
|
picker.add_theme_constant_override("h_width", 38)
|
||||||
picker.add_theme_constant_override("label_width", 28)
|
picker.add_theme_constant_override("label_width", 28)
|
||||||
|
if not popup.about_to_popup.is_connected(
|
||||||
|
_on_controller_color_picker_opened.bind(button)
|
||||||
|
):
|
||||||
|
popup.about_to_popup.connect(
|
||||||
|
_on_controller_color_picker_opened.bind(button)
|
||||||
|
)
|
||||||
|
if not popup.popup_hide.is_connected(
|
||||||
|
_on_controller_color_picker_closed.bind(button)
|
||||||
|
):
|
||||||
|
popup.popup_hide.connect(
|
||||||
|
_on_controller_color_picker_closed.bind(button)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _fit_color_picker_popup(button: ColorPickerButton) -> void:
|
||||||
|
var popup: PopupPanel = button.get_popup()
|
||||||
|
var picker: ColorPicker = button.get_picker()
|
||||||
|
var window_size: Vector2i = get_window().size
|
||||||
|
var available := Vector2i(
|
||||||
|
maxi(window_size.x - 32, 420),
|
||||||
|
maxi(window_size.y - 32, 340),
|
||||||
|
)
|
||||||
|
var target := Vector2i(
|
||||||
|
mini(FUR_COLOR_PICKER_POPUP_SIZE.x, available.x),
|
||||||
|
mini(FUR_COLOR_PICKER_POPUP_SIZE.y, available.y),
|
||||||
|
)
|
||||||
|
popup.min_size = target
|
||||||
|
popup.size = target
|
||||||
|
picker.custom_minimum_size = Vector2(
|
||||||
|
target.x - 48,
|
||||||
|
target.y - 48,
|
||||||
|
)
|
||||||
|
picker.add_theme_constant_override(
|
||||||
|
"sv_width",
|
||||||
|
mini(FUR_COLOR_PICKER_SV_SIZE.x, target.x - 110),
|
||||||
|
)
|
||||||
|
picker.add_theme_constant_override(
|
||||||
|
"sv_height",
|
||||||
|
mini(FUR_COLOR_PICKER_SV_SIZE.y, target.y - 180),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_controller_color_picker_opened(button: ColorPickerButton) -> void:
|
||||||
|
_active_color_picker_button = button
|
||||||
|
_active_color_picker_return_depth = _controller_option_depth
|
||||||
|
_controller_zone = ControllerZone.COLOR_PICKER
|
||||||
|
_fit_color_picker_popup(button)
|
||||||
|
_apply_controller_zone_focus.call_deferred()
|
||||||
|
_focus_controller_zone.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_controller_color_picker_closed(button: ColorPickerButton) -> void:
|
||||||
|
if _active_color_picker_button != button:
|
||||||
|
return
|
||||||
|
_active_color_picker_button = null
|
||||||
|
_controller_zone = ControllerZone.OPTIONS
|
||||||
|
_controller_option_depth = _active_color_picker_return_depth
|
||||||
|
_apply_controller_zone_focus.call_deferred()
|
||||||
|
_focus_controller_zone.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _close_controller_color_picker() -> void:
|
||||||
|
if _active_color_picker_button != null:
|
||||||
|
_active_color_picker_button.get_popup().hide()
|
||||||
|
else:
|
||||||
|
_controller_zone = ControllerZone.OPTIONS
|
||||||
|
_controller_option_depth = _active_color_picker_return_depth
|
||||||
|
_apply_controller_zone_focus()
|
||||||
|
call_deferred("_focus_controller_zone")
|
||||||
|
|
||||||
|
|
||||||
|
func _color_picker_controller_controls() -> Array[Control]:
|
||||||
|
if _active_color_picker_button == null:
|
||||||
|
return []
|
||||||
|
return _controls_under(_active_color_picker_button.get_picker())
|
||||||
|
|
||||||
|
|
||||||
|
func _adjust_controller_color_gamut(stick: Vector2, delta: float) -> void:
|
||||||
|
if _active_color_picker_button == null:
|
||||||
|
return
|
||||||
|
var color: Color = _active_color_picker_button.color
|
||||||
|
var saturation: float = clampf(color.s + stick.x * delta * 0.72, 0.0, 1.0)
|
||||||
|
var value: float = clampf(color.v - stick.y * delta * 0.72, 0.0, 1.0)
|
||||||
|
var adjusted := Color.from_hsv(color.h, saturation, value, 1.0)
|
||||||
|
_active_color_picker_button.color = adjusted
|
||||||
|
var category_id := str(_active_color_picker_button.get_meta(
|
||||||
|
&"fur_color_category_id",
|
||||||
|
_active_fur_color_id,
|
||||||
|
))
|
||||||
|
_select_custom_fur_color(adjusted, category_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_controller_zone_after_options() -> void:
|
||||||
|
if _controller_zone != ControllerZone.OPTIONS:
|
||||||
|
return
|
||||||
|
var groups: Array = _controller_option_groups()
|
||||||
|
if groups.is_empty():
|
||||||
|
_controller_zone = ControllerZone.CATEGORIES
|
||||||
|
else:
|
||||||
|
_controller_option_depth = clampi(
|
||||||
|
_controller_option_depth,
|
||||||
|
0,
|
||||||
|
groups.size() - 1,
|
||||||
|
)
|
||||||
|
_apply_controller_zone_focus()
|
||||||
|
_focus_controller_zone()
|
||||||
|
|
||||||
|
|
||||||
func _update_fur_custom_color_display(color: Color) -> void:
|
func _update_fur_custom_color_display(color: Color) -> void:
|
||||||
|
|
@ -1465,6 +1874,7 @@ func _update_fur_custom_color_display(color: Color) -> void:
|
||||||
func _select_option(category_id: String, option_id: String) -> void:
|
func _select_option(category_id: String, option_id: String) -> void:
|
||||||
_draft_appearance[category_id] = option_id
|
_draft_appearance[category_id] = option_id
|
||||||
_preview.apply_appearance_profile(_draft_appearance)
|
_preview.apply_appearance_profile(_draft_appearance)
|
||||||
|
_queue_draft_appearance_preview()
|
||||||
_dirty = _draft_differs()
|
_dirty = _draft_differs()
|
||||||
_refresh_options()
|
_refresh_options()
|
||||||
_refresh_actions()
|
_refresh_actions()
|
||||||
|
|
@ -1479,6 +1889,7 @@ func _select_scale(value: float) -> void:
|
||||||
)
|
)
|
||||||
_update_scale_value_label(resolved_scale)
|
_update_scale_value_label(resolved_scale)
|
||||||
_preview.apply_appearance_profile(_draft_appearance)
|
_preview.apply_appearance_profile(_draft_appearance)
|
||||||
|
_queue_draft_appearance_preview()
|
||||||
_dirty = _draft_differs()
|
_dirty = _draft_differs()
|
||||||
_refresh_actions()
|
_refresh_actions()
|
||||||
|
|
||||||
|
|
@ -1532,6 +1943,7 @@ func _on_conflict_result(
|
||||||
)
|
)
|
||||||
UtilityPageStyle.apply_compact_ocean_button(anyway)
|
UtilityPageStyle.apply_compact_ocean_button(anyway)
|
||||||
_suggestions.add_child(anyway)
|
_suggestions.add_child(anyway)
|
||||||
|
_apply_controller_zone_focus()
|
||||||
|
|
||||||
|
|
||||||
func _on_experience_changed(_total_experience: int, _level: int) -> void:
|
func _on_experience_changed(_total_experience: int, _level: int) -> void:
|
||||||
|
|
@ -1607,6 +2019,8 @@ func _load_persisted() -> void:
|
||||||
)
|
)
|
||||||
_name_edit.text = _draft_name
|
_name_edit.text = _draft_name
|
||||||
_preview.apply_appearance_profile(_draft_appearance)
|
_preview.apply_appearance_profile(_draft_appearance)
|
||||||
|
_appearance_preview_timer.stop()
|
||||||
|
_service.preview_appearance(_persisted_appearance)
|
||||||
_dirty = false
|
_dirty = false
|
||||||
_allow_duplicate = false
|
_allow_duplicate = false
|
||||||
_name_status.text = ""
|
_name_status.text = ""
|
||||||
|
|
@ -1633,9 +2047,17 @@ func _show_confirmation(action: String) -> void:
|
||||||
else:
|
else:
|
||||||
_confirmation_label.text = "Discard unsaved profile changes?"
|
_confirmation_label.text = "Discard unsaved profile changes?"
|
||||||
_confirmation_confirm.text = "discard changes"
|
_confirmation_confirm.text = "discard changes"
|
||||||
|
_apply_controller_zone_focus()
|
||||||
_keep_editing_button.grab_focus()
|
_keep_editing_button.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _close_discard_confirmation() -> void:
|
||||||
|
_discard_confirmation.visible = false
|
||||||
|
_confirmation_action = ""
|
||||||
|
_apply_controller_zone_focus()
|
||||||
|
call_deferred("_focus_controller_zone")
|
||||||
|
|
||||||
|
|
||||||
func _confirm_pending_action() -> void:
|
func _confirm_pending_action() -> void:
|
||||||
_discard_confirmation.visible = false
|
_discard_confirmation.visible = false
|
||||||
if _confirmation_action == "defaults":
|
if _confirmation_action == "defaults":
|
||||||
|
|
@ -1644,12 +2066,30 @@ func _confirm_pending_action() -> void:
|
||||||
_draft_speech_speed_id = VoiceProfilesType.DEFAULT_SPEED_ID
|
_draft_speech_speed_id = VoiceProfilesType.DEFAULT_SPEED_ID
|
||||||
_draft_call_id = VoiceProfilesType.DEFAULT_CALL_ID
|
_draft_call_id = VoiceProfilesType.DEFAULT_CALL_ID
|
||||||
_preview.apply_appearance_profile(_draft_appearance)
|
_preview.apply_appearance_profile(_draft_appearance)
|
||||||
|
_queue_draft_appearance_preview()
|
||||||
_dirty = _draft_differs()
|
_dirty = _draft_differs()
|
||||||
_refresh_options()
|
_refresh_options()
|
||||||
_refresh_actions()
|
_refresh_actions()
|
||||||
else:
|
else:
|
||||||
_confirm_discard()
|
_confirm_discard()
|
||||||
_confirmation_action = ""
|
_confirmation_action = ""
|
||||||
|
_apply_controller_zone_focus()
|
||||||
|
call_deferred("_focus_controller_zone")
|
||||||
|
|
||||||
|
|
||||||
|
func _queue_draft_appearance_preview() -> void:
|
||||||
|
if _service == null or _appearance_preview_timer == null:
|
||||||
|
return
|
||||||
|
# Throttle continuous controls such as the custom color picker instead of
|
||||||
|
# debouncing them. Remote players should see an in-progress drag, while the
|
||||||
|
# reliable channel remains capped at one current snapshot per interval.
|
||||||
|
if _appearance_preview_timer.is_stopped():
|
||||||
|
_appearance_preview_timer.start()
|
||||||
|
|
||||||
|
|
||||||
|
func _publish_draft_appearance() -> void:
|
||||||
|
if _service != null:
|
||||||
|
_service.preview_appearance(_draft_appearance)
|
||||||
|
|
||||||
|
|
||||||
func _draft_differs() -> bool:
|
func _draft_differs() -> bool:
|
||||||
|
|
@ -1672,4 +2112,6 @@ func _refresh_actions() -> void:
|
||||||
|
|
||||||
func _clear_suggestions() -> void:
|
func _clear_suggestions() -> void:
|
||||||
for child: Node in _suggestions.get_children():
|
for child: Node in _suggestions.get_children():
|
||||||
|
_suggestions.remove_child(child)
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
|
_apply_controller_zone_focus()
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ func set_world_pixel_size(pixel_size: int) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
focus_mode = Control.FOCUS_ALL
|
focus_mode = Control.FOCUS_NONE
|
||||||
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||||
gui_input.connect(_on_gui_input)
|
gui_input.connect(_on_gui_input)
|
||||||
resized.connect(_refresh_pixelation_filter)
|
resized.connect(_refresh_pixelation_filter)
|
||||||
|
|
@ -91,20 +91,18 @@ func reset_view() -> void:
|
||||||
_apply_camera_orbit()
|
_apply_camera_orbit()
|
||||||
|
|
||||||
|
|
||||||
|
func apply_controller_orbit(stick: Vector2, delta: float) -> void:
|
||||||
|
if absf(stick.x) > 0.1:
|
||||||
|
_rotate(stick.x * keyboard_speed * delta)
|
||||||
|
if absf(stick.y) > 0.1:
|
||||||
|
_zoom_preview(stick.y * CAMERA_ZOOM_STEP * 4.0 * delta)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
_sync_world_lighting()
|
_sync_world_lighting()
|
||||||
if not has_focus():
|
if not has_focus():
|
||||||
return
|
return
|
||||||
var axis := Input.get_axis("ui_left", "ui_right")
|
var axis := Input.get_axis("ui_left", "ui_right")
|
||||||
var right_stick: float = (
|
|
||||||
_controller_mapping_manager.get_role_axis(
|
|
||||||
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
|
|
||||||
)
|
|
||||||
if _controller_mapping_manager != null
|
|
||||||
else Input.get_joy_axis(0, JOY_AXIS_RIGHT_X)
|
|
||||||
)
|
|
||||||
if absf(right_stick) > 0.2:
|
|
||||||
axis = right_stick
|
|
||||||
if absf(axis) > 0.1:
|
if absf(axis) > 0.1:
|
||||||
_rotate(axis * keyboard_speed * delta)
|
_rotate(axis * keyboard_speed * delta)
|
||||||
|
|
||||||
|
|
@ -113,18 +111,15 @@ func _on_gui_input(event: InputEvent) -> void:
|
||||||
if event is InputEventMouseButton and event.shift_pressed:
|
if event is InputEventMouseButton and event.shift_pressed:
|
||||||
if event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
|
if event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
|
||||||
_zoom_preview(-CAMERA_ZOOM_STEP)
|
_zoom_preview(-CAMERA_ZOOM_STEP)
|
||||||
grab_focus()
|
|
||||||
accept_event()
|
accept_event()
|
||||||
return
|
return
|
||||||
if event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed:
|
if event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed:
|
||||||
_zoom_preview(CAMERA_ZOOM_STEP)
|
_zoom_preview(CAMERA_ZOOM_STEP)
|
||||||
grab_focus()
|
|
||||||
accept_event()
|
accept_event()
|
||||||
return
|
return
|
||||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
|
||||||
_dragging = event.pressed
|
_dragging = event.pressed
|
||||||
if event.pressed:
|
if event.pressed:
|
||||||
grab_focus()
|
|
||||||
accept_event()
|
accept_event()
|
||||||
elif event is InputEventMouseMotion and _dragging:
|
elif event is InputEventMouseMotion and _dragging:
|
||||||
_rotate(event.relative.x * drag_sensitivity)
|
_rotate(event.relative.x * drag_sensitivity)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue