Release alpha 10 with bilingual UI and social co-op
This commit is contained in:
1 parent
7d5cad7277
commit
d53467baf2
22 files changed
+1714
-271
No files matched your search
+234
-5
@@ -10,6 +10,8 @@ signal match_found(match_data)
|
||||
signal match_snapshot_received(snapshot: Dictionary)
|
||||
signal match_event_received(event: Dictionary)
|
||||
signal private_room_created(room_code: String)
|
||||
signal friends_changed()
|
||||
signal match_invite_received(invite: Dictionary)
|
||||
|
||||
const SERVER_KEY := "voidbastion-public-v1"
|
||||
const PRODUCTION_HOST := "api.void-bastion.gergames.de"
|
||||
@@ -22,6 +24,7 @@ const OP_UPGRADE := 11
|
||||
const OP_SELL := 12
|
||||
const OP_START_WAVE := 13
|
||||
const OP_READY := 14
|
||||
const MAX_PENDING_SNAPSHOTS := 5
|
||||
|
||||
var client: NakamaClient
|
||||
var session: NakamaSession
|
||||
@@ -37,6 +40,18 @@ var intentional_disconnect := false
|
||||
var reconnect_in_progress := false
|
||||
var account_manager: AccountManager
|
||||
var auto_connect_enabled := true
|
||||
var snapshot_previous_ms := 0
|
||||
var snapshot_report_ms := 0
|
||||
var snapshot_count := 0
|
||||
var snapshot_gap_max_ms := 0
|
||||
var snapshot_bytes_max := 0
|
||||
var snapshot_parse_max_us := 0
|
||||
var snapshot_large_gap_count := 0
|
||||
var snapshot_coalesced_count := 0
|
||||
var pending_snapshot_payloads: Array[String] = []
|
||||
var friends: Array[Dictionary] = []
|
||||
var friend_statuses: Dictionary = {}
|
||||
var presence_status := ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -46,6 +61,35 @@ func _ready() -> void:
|
||||
call_deferred("connect_and_authenticate")
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_flush_pending_snapshots()
|
||||
|
||||
|
||||
func _flush_pending_snapshots() -> void:
|
||||
if pending_snapshot_payloads.is_empty():
|
||||
return
|
||||
var payloads := pending_snapshot_payloads.duplicate()
|
||||
pending_snapshot_payloads.clear()
|
||||
var latest: Dictionary = {}
|
||||
var recent_shots: Array = []
|
||||
for payload in payloads:
|
||||
var parse_start_us := Time.get_ticks_usec()
|
||||
var parsed = JSON.parse_string(payload)
|
||||
snapshot_parse_max_us = maxi(snapshot_parse_max_us, Time.get_ticks_usec() - parse_start_us)
|
||||
if not parsed is Dictionary:
|
||||
continue
|
||||
latest = parsed
|
||||
var shots = parsed.get("shots", [])
|
||||
if shots is Array:
|
||||
recent_shots.append_array(shots)
|
||||
if latest.is_empty():
|
||||
return
|
||||
if payloads.size() > 1:
|
||||
latest["shots"] = recent_shots
|
||||
snapshot_coalesced_count += payloads.size() - 1
|
||||
match_snapshot_received.emit(latest)
|
||||
|
||||
|
||||
func _is_headless_or_editor() -> bool:
|
||||
return DisplayServer.get_name() == "headless" or Engine.is_editor_hint()
|
||||
|
||||
@@ -96,6 +140,8 @@ func _after_authentication() -> void:
|
||||
else:
|
||||
intentional_disconnect = false
|
||||
_set_connection_state("online")
|
||||
var parent_game = get_parent()
|
||||
await set_presence_status("playing" if parent_game != null and parent_game.game_mode in ["playing", "paused", "victory", "game_over", "endless_offer", "endless_result"] else "menu")
|
||||
authenticated.emit(session.user_id, session.username)
|
||||
await flush_pending_scores()
|
||||
|
||||
@@ -113,16 +159,22 @@ func _record_activity() -> void:
|
||||
func _bind_socket_signals() -> void:
|
||||
socket.received_matchmaker_matched.connect(_on_matchmaker_matched)
|
||||
socket.received_match_state.connect(_on_match_state)
|
||||
socket.received_status_presence.connect(_on_status_presence)
|
||||
socket.received_notification.connect(_on_notification)
|
||||
socket.closed.connect(_on_socket_closed)
|
||||
|
||||
|
||||
func disconnect_from_server() -> void:
|
||||
intentional_disconnect = true
|
||||
pending_snapshot_payloads.clear()
|
||||
if socket:
|
||||
socket.close()
|
||||
socket = null
|
||||
session = null
|
||||
current_match = null
|
||||
friends.clear()
|
||||
friend_statuses.clear()
|
||||
presence_status = ""
|
||||
matchmaker_ticket = ""
|
||||
private_room_code = ""
|
||||
lobby_ready = false
|
||||
@@ -190,6 +242,124 @@ func delete_online_account() -> Dictionary:
|
||||
return {"accepted": true}
|
||||
|
||||
|
||||
func redeem_unlock_code(code: String) -> Dictionary:
|
||||
if not is_authenticated():
|
||||
return {"accepted": false, "error": "offline"}
|
||||
var result = await client.rpc_async(session, "redeem_unlock_code", JSON.stringify({"code": code}))
|
||||
var parsed := _parse_rpc_result(result)
|
||||
if parsed.is_empty():
|
||||
return {"accepted": false, "error": "invalid"}
|
||||
return parsed
|
||||
|
||||
|
||||
func get_promo_unlocks() -> Dictionary:
|
||||
if not is_authenticated():
|
||||
return {}
|
||||
var result = await client.rpc_async(session, "get_promo_unlocks", "{}")
|
||||
var parsed := _parse_rpc_result(result)
|
||||
return parsed if bool(parsed.get("accepted", false)) else {}
|
||||
|
||||
|
||||
func set_presence_status(value: String) -> void:
|
||||
if not is_online() or value == presence_status:
|
||||
return
|
||||
var safe_value := "playing" if value == "playing" else "menu"
|
||||
var result = await socket.update_status_async(safe_value)
|
||||
if result != null and not result.is_exception():
|
||||
presence_status = safe_value
|
||||
|
||||
|
||||
func refresh_friends() -> bool:
|
||||
if not is_authenticated():
|
||||
return false
|
||||
var result = await client.list_friends_async(session, null, 100)
|
||||
if result == null or result.is_exception():
|
||||
return false
|
||||
var rows: Array[Dictionary] = []
|
||||
var follow_ids := PackedStringArray()
|
||||
for item in result.friends:
|
||||
if item.user == null:
|
||||
continue
|
||||
var user_id: String = item.user.id
|
||||
var state: int = item.state
|
||||
rows.append({"id": user_id, "username": item.user.username,
|
||||
"name": item.user.display_name if not item.user.display_name.is_empty() else item.user.username,
|
||||
"state": state, "online": item.user.online})
|
||||
if state == 0:
|
||||
follow_ids.append(user_id)
|
||||
friends = rows
|
||||
friend_statuses.clear()
|
||||
if is_online() and not follow_ids.is_empty():
|
||||
var statuses = await socket.follow_users_async(follow_ids, PackedStringArray())
|
||||
if statuses != null and not statuses.is_exception():
|
||||
for presence in statuses.presences:
|
||||
friend_statuses[presence.user_id] = presence.status
|
||||
friends_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func add_friend_by_username(username: String) -> bool:
|
||||
var normalized := username.strip_edges()
|
||||
if not is_authenticated() or normalized.is_empty() or normalized == session.username:
|
||||
return false
|
||||
var result = await client.add_friends_async(session, null, PackedStringArray([normalized]))
|
||||
if result == null or result.is_exception():
|
||||
return false
|
||||
await refresh_friends()
|
||||
return true
|
||||
|
||||
|
||||
func accept_friend(user_id: String) -> bool:
|
||||
if not is_authenticated() or user_id.is_empty():
|
||||
return false
|
||||
var result = await client.add_friends_async(session, PackedStringArray([user_id]))
|
||||
if result == null or result.is_exception():
|
||||
return false
|
||||
await refresh_friends()
|
||||
return true
|
||||
|
||||
|
||||
func remove_friend(user_id: String) -> bool:
|
||||
if not is_authenticated() or user_id.is_empty():
|
||||
return false
|
||||
var result = await client.delete_friends_async(session, PackedStringArray([user_id]))
|
||||
if result == null or result.is_exception():
|
||||
return false
|
||||
friend_statuses.erase(user_id)
|
||||
await refresh_friends()
|
||||
return true
|
||||
|
||||
|
||||
func invite_friend(user_id: String, room_code: String) -> bool:
|
||||
if not is_authenticated() or user_id.is_empty() or room_code.length() != 6:
|
||||
return false
|
||||
var result = await client.rpc_async(session, "invite_friend", JSON.stringify({"friend_id": user_id, "room_code": room_code}))
|
||||
return bool(_parse_rpc_result(result).get("accepted", false))
|
||||
|
||||
|
||||
func _on_status_presence(event) -> void:
|
||||
for presence in event.joins:
|
||||
friend_statuses[presence.user_id] = presence.status
|
||||
for presence in event.leaves:
|
||||
friend_statuses.erase(presence.user_id)
|
||||
friends_changed.emit()
|
||||
|
||||
|
||||
func _on_notification(notification) -> void:
|
||||
if notification.code in [-2, -3]:
|
||||
refresh_friends()
|
||||
return
|
||||
if notification.code != 101:
|
||||
return
|
||||
var content = JSON.parse_string(notification.content)
|
||||
if not content is Dictionary:
|
||||
return
|
||||
var code := String(content.get("room_code", ""))
|
||||
if code.length() != 6:
|
||||
return
|
||||
match_invite_received.emit({"room_code": code, "from": String(content.get("from", "Mitspieler"))})
|
||||
|
||||
|
||||
func load_leaderboard(map_index: int, difficulty_index: int, endless: bool, limit := 20) -> Array:
|
||||
if not is_authenticated():
|
||||
return []
|
||||
@@ -245,6 +415,7 @@ func begin_matchmaking(mode := "quick", difficulty := 1) -> bool:
|
||||
|
||||
func cancel_matchmaking() -> void:
|
||||
intentional_disconnect = true
|
||||
pending_snapshot_payloads.clear()
|
||||
if socket and not matchmaker_ticket.is_empty():
|
||||
await socket.remove_matchmaker_async(matchmaker_ticket)
|
||||
matchmaker_ticket = ""
|
||||
@@ -257,6 +428,7 @@ func cancel_matchmaking() -> void:
|
||||
|
||||
func create_private_room(difficulty := 1) -> bool:
|
||||
intentional_disconnect = false
|
||||
pending_snapshot_payloads.clear()
|
||||
if not is_online() or socket == null:
|
||||
matchmaking_state_changed.emit("offline")
|
||||
return false
|
||||
@@ -284,6 +456,7 @@ func create_private_room(difficulty := 1) -> bool:
|
||||
|
||||
func join_private_room(room_code: String) -> bool:
|
||||
intentional_disconnect = false
|
||||
pending_snapshot_payloads.clear()
|
||||
if not is_online() or socket == null:
|
||||
matchmaking_state_changed.emit("offline")
|
||||
return false
|
||||
@@ -310,6 +483,7 @@ func join_private_room(room_code: String) -> bool:
|
||||
|
||||
func _on_matchmaker_matched(matched) -> void:
|
||||
intentional_disconnect = false
|
||||
pending_snapshot_payloads.clear()
|
||||
matchmaker_ticket = ""
|
||||
current_match = await socket.join_matched_async(matched)
|
||||
if current_match == null or current_match.is_exception():
|
||||
@@ -351,6 +525,7 @@ func request_wave_start() -> bool:
|
||||
|
||||
|
||||
func leave_current_match() -> void:
|
||||
pending_snapshot_payloads.clear()
|
||||
if socket and current_match != null:
|
||||
await socket.leave_match_async(current_match.match_id)
|
||||
current_match = null
|
||||
@@ -361,16 +536,44 @@ func leave_current_match() -> void:
|
||||
func _on_match_state(data) -> void:
|
||||
if current_match == null or data.match_id != current_match.match_id:
|
||||
return
|
||||
var parsed = JSON.parse_string(data.data)
|
||||
if not parsed is Dictionary:
|
||||
return
|
||||
if data.op_code == OP_SNAPSHOT:
|
||||
match_snapshot_received.emit(parsed)
|
||||
var now_ms := Time.get_ticks_msec()
|
||||
if snapshot_previous_ms > 0:
|
||||
var gap_ms := now_ms - snapshot_previous_ms
|
||||
snapshot_gap_max_ms = maxi(snapshot_gap_max_ms, gap_ms)
|
||||
if gap_ms > 500:
|
||||
snapshot_large_gap_count += 1
|
||||
snapshot_previous_ms = now_ms
|
||||
snapshot_count += 1
|
||||
snapshot_bytes_max = maxi(snapshot_bytes_max, String(data.data).to_utf8_buffer().size())
|
||||
pending_snapshot_payloads.append(String(data.data))
|
||||
if pending_snapshot_payloads.size() > MAX_PENDING_SNAPSHOTS:
|
||||
pending_snapshot_payloads.pop_front()
|
||||
snapshot_coalesced_count += 1
|
||||
if snapshot_report_ms == 0:
|
||||
snapshot_report_ms = now_ms
|
||||
elif now_ms - snapshot_report_ms >= 10000:
|
||||
print("ONLINE DIAG snapshots=%d coalesced=%d max_gap_ms=%d gaps_over_500ms=%d max_bytes=%d max_parse_us=%d" % [snapshot_count, snapshot_coalesced_count, snapshot_gap_max_ms, snapshot_large_gap_count, snapshot_bytes_max, snapshot_parse_max_us])
|
||||
snapshot_report_ms = now_ms
|
||||
snapshot_count = 0
|
||||
snapshot_coalesced_count = 0
|
||||
snapshot_gap_max_ms = 0
|
||||
snapshot_large_gap_count = 0
|
||||
snapshot_bytes_max = 0
|
||||
snapshot_parse_max_us = 0
|
||||
elif data.op_code == OP_EVENT:
|
||||
match_event_received.emit(parsed)
|
||||
var parsed = JSON.parse_string(data.data)
|
||||
if parsed is Dictionary:
|
||||
match_event_received.emit(parsed)
|
||||
|
||||
|
||||
func _on_socket_closed() -> void:
|
||||
print("ONLINE DIAG websocket_closed reconnect=%s" % str(not intentional_disconnect and current_match != null))
|
||||
pending_snapshot_payloads.clear()
|
||||
presence_status = ""
|
||||
friend_statuses.clear()
|
||||
friends_changed.emit()
|
||||
snapshot_previous_ms = 0
|
||||
var reconnect_match_id := ""
|
||||
if not intentional_disconnect and current_match != null:
|
||||
reconnect_match_id = String(current_match.match_id)
|
||||
@@ -381,6 +584,28 @@ func _on_socket_closed() -> void:
|
||||
reconnect_in_progress = true
|
||||
matchmaking_state_changed.emit("reconnecting")
|
||||
_recover_match.call_deferred(reconnect_match_id)
|
||||
elif not intentional_disconnect and reconnect_match_id.is_empty() and not reconnect_in_progress and is_authenticated():
|
||||
reconnect_in_progress = true
|
||||
_recover_presence.call_deferred()
|
||||
|
||||
|
||||
func _recover_presence() -> void:
|
||||
for attempt in 5:
|
||||
await get_tree().create_timer(1.5 + attempt).timeout
|
||||
if intentional_disconnect or not is_authenticated():
|
||||
break
|
||||
socket = Nakama.create_socket_from(client)
|
||||
_bind_socket_signals()
|
||||
var connected = await socket.connect_async(session, true, 5)
|
||||
if connected == null or connected.is_exception():
|
||||
continue
|
||||
reconnect_in_progress = false
|
||||
_set_connection_state("online")
|
||||
var parent_game = get_parent()
|
||||
await set_presence_status("playing" if parent_game != null and parent_game.game_mode in ["playing", "paused", "victory", "game_over", "endless_offer", "endless_result"] else "menu")
|
||||
await refresh_friends()
|
||||
return
|
||||
reconnect_in_progress = false
|
||||
|
||||
|
||||
func _recover_match(match_id: String) -> void:
|
||||
@@ -395,13 +620,17 @@ func _recover_match(match_id: String) -> void:
|
||||
continue
|
||||
var rejoined = await socket.join_match_async(match_id)
|
||||
if rejoined != null and not rejoined.is_exception():
|
||||
print("ONLINE DIAG websocket_reconnected attempt=%d" % (attempt + 1))
|
||||
current_match = rejoined
|
||||
intentional_disconnect = false
|
||||
reconnect_in_progress = false
|
||||
_set_connection_state("online")
|
||||
var parent_game = get_parent()
|
||||
await set_presence_status("playing" if parent_game != null and parent_game.game_mode in ["playing", "paused", "victory", "game_over"] else "menu")
|
||||
matchmaking_state_changed.emit("reconnected")
|
||||
return
|
||||
reconnect_in_progress = false
|
||||
print("ONLINE DIAG websocket_reconnect_failed")
|
||||
matchmaking_state_changed.emit("connection_lost")
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user