Initial Void Bastion 0.20 prototype
This commit is contained in:
commit
7362ca0f02
251 files changed
+29688
No files matched your search
@@ -0,0 +1,272 @@
|
||||
class_name OnlineManager
|
||||
extends Node
|
||||
|
||||
signal connection_state_changed(state: String)
|
||||
signal authenticated(user_id: String, username: String)
|
||||
signal leaderboard_loaded(leaderboard_id: String, records: Array)
|
||||
signal score_submitted(result: Dictionary)
|
||||
signal matchmaking_state_changed(state: String)
|
||||
signal match_found(match_data)
|
||||
signal match_snapshot_received(snapshot: Dictionary)
|
||||
signal match_event_received(event: Dictionary)
|
||||
|
||||
const SERVER_KEY := "voidbastion-public-v1"
|
||||
const PRODUCTION_HOST := "api.void-bastion.gergames.de"
|
||||
const DEVELOPMENT_HOST := "192.168.178.20"
|
||||
const DEFAULT_PORT := 7350
|
||||
const OP_SNAPSHOT := 1
|
||||
const OP_EVENT := 2
|
||||
const OP_BUILD := 10
|
||||
const OP_UPGRADE := 11
|
||||
const OP_SELL := 12
|
||||
const OP_START_WAVE := 13
|
||||
const OP_READY := 14
|
||||
|
||||
var client: NakamaClient
|
||||
var session: NakamaSession
|
||||
var socket: NakamaSocket
|
||||
var connection_state := "offline"
|
||||
var last_error := ""
|
||||
var current_host := ""
|
||||
var current_match = null
|
||||
var matchmaker_ticket := ""
|
||||
var account_manager: AccountManager
|
||||
var auto_connect_enabled := true
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
account_manager = get_parent().get_node_or_null("AccountManager") as AccountManager
|
||||
if _is_headless_or_editor():
|
||||
return
|
||||
call_deferred("connect_and_authenticate")
|
||||
|
||||
|
||||
func _is_headless_or_editor() -> bool:
|
||||
return DisplayServer.get_name() == "headless" or Engine.is_editor_hint()
|
||||
|
||||
|
||||
func connect_and_authenticate(force_production := false) -> bool:
|
||||
if connection_state in ["connecting", "online"]:
|
||||
return connection_state == "online"
|
||||
_set_connection_state("connecting")
|
||||
last_error = ""
|
||||
var production_ok := await _authenticate_on(PRODUCTION_HOST, 443, "https")
|
||||
if production_ok:
|
||||
await _after_authentication()
|
||||
return true
|
||||
if OS.is_debug_build() and not force_production:
|
||||
var development_ok := await _authenticate_on(DEVELOPMENT_HOST, DEFAULT_PORT, "http")
|
||||
if development_ok:
|
||||
await _after_authentication()
|
||||
return true
|
||||
_set_connection_state("offline")
|
||||
return false
|
||||
|
||||
|
||||
func _authenticate_on(host: String, port: int, scheme: String) -> bool:
|
||||
current_host = host
|
||||
client = Nakama.create_client(SERVER_KEY, host, port, scheme, 5, NakamaLogger.LOG_LEVEL.ERROR)
|
||||
var device_id := _device_id()
|
||||
var username = "pilot_%s" % device_id.md5_text().left(20)
|
||||
var result: NakamaSession = await client.authenticate_device_async(device_id, username, true)
|
||||
if result == null or result.is_exception():
|
||||
last_error = "Server %s nicht erreichbar" % host
|
||||
return false
|
||||
session = result
|
||||
if account_manager:
|
||||
await client.update_account_async(session, null, account_manager.display_name)
|
||||
account_manager.attach_online_identity(session.user_id, session.username)
|
||||
return true
|
||||
|
||||
|
||||
func _after_authentication() -> void:
|
||||
socket = Nakama.create_socket_from(client)
|
||||
socket.received_matchmaker_matched.connect(_on_matchmaker_matched)
|
||||
socket.received_match_state.connect(_on_match_state)
|
||||
socket.closed.connect(_on_socket_closed)
|
||||
var result = await socket.connect_async(session, true, 5)
|
||||
if result == null or result.is_exception():
|
||||
last_error = "Echtzeitverbindung konnte nicht aufgebaut werden"
|
||||
socket = null
|
||||
_set_connection_state("authenticated")
|
||||
else:
|
||||
_set_connection_state("online")
|
||||
authenticated.emit(session.user_id, session.username)
|
||||
await flush_pending_scores()
|
||||
|
||||
|
||||
func disconnect_from_server() -> void:
|
||||
if socket:
|
||||
socket.close()
|
||||
socket = null
|
||||
session = null
|
||||
current_match = null
|
||||
matchmaker_ticket = ""
|
||||
_set_connection_state("offline")
|
||||
|
||||
|
||||
func server_info() -> Dictionary:
|
||||
if not is_authenticated():
|
||||
return {}
|
||||
var result = await client.rpc_async(session, "server_info", "{}")
|
||||
return _parse_rpc_result(result)
|
||||
|
||||
|
||||
func submit_run(record: Dictionary) -> bool:
|
||||
if not is_authenticated():
|
||||
return false
|
||||
var payload := record.duplicate(true)
|
||||
payload["client_version"] = "0.20"
|
||||
var result = await client.rpc_async(session, "submit_run", JSON.stringify(payload))
|
||||
var parsed := _parse_rpc_result(result)
|
||||
if parsed.is_empty() or not bool(parsed.get("accepted", false)):
|
||||
return false
|
||||
if account_manager:
|
||||
account_manager.mark_submission_complete(record)
|
||||
score_submitted.emit(parsed)
|
||||
return true
|
||||
|
||||
|
||||
func flush_pending_scores() -> void:
|
||||
if not account_manager or not is_authenticated():
|
||||
return
|
||||
var queued: Array = account_manager.pending_submissions.duplicate(true)
|
||||
for record in queued:
|
||||
if record is Dictionary:
|
||||
await submit_run(record)
|
||||
|
||||
|
||||
func load_leaderboard(map_index: int, difficulty_index: int, endless: bool, limit := 20) -> Array:
|
||||
if not is_authenticated():
|
||||
return []
|
||||
var leaderboard_id := make_leaderboard_id(map_index, difficulty_index, "endless" if endless else "mission")
|
||||
var result = await client.list_leaderboard_records_async(session, leaderboard_id, null, null, clampi(limit, 1, 100))
|
||||
if result == null or result.is_exception():
|
||||
return []
|
||||
var rows: Array = []
|
||||
for record in result.records:
|
||||
rows.append({
|
||||
"rank": int(record.rank),
|
||||
"username": record.username,
|
||||
"score": int(record.score),
|
||||
"wave": int(record.subscore),
|
||||
"owner_id": record.owner_id
|
||||
})
|
||||
leaderboard_loaded.emit(leaderboard_id, rows)
|
||||
return rows
|
||||
|
||||
|
||||
func begin_matchmaking(mode := "quick", difficulty := 1) -> bool:
|
||||
if not is_online() or socket == null:
|
||||
matchmaking_state_changed.emit("offline")
|
||||
return false
|
||||
if not matchmaker_ticket.is_empty():
|
||||
return true
|
||||
var string_properties := {"game": "void_bastion", "mode": mode}
|
||||
var numeric_properties := {"difficulty": float(difficulty)}
|
||||
var query := "+properties.game:void_bastion +properties.mode:%s +properties.difficulty:%d" % [mode, difficulty]
|
||||
var ticket = await socket.add_matchmaker_async(query, 2, 2, string_properties, numeric_properties)
|
||||
if ticket == null or ticket.is_exception():
|
||||
matchmaking_state_changed.emit("error")
|
||||
return false
|
||||
matchmaker_ticket = ticket.ticket
|
||||
matchmaking_state_changed.emit("searching")
|
||||
return true
|
||||
|
||||
|
||||
func cancel_matchmaking() -> void:
|
||||
if socket and not matchmaker_ticket.is_empty():
|
||||
await socket.remove_matchmaker_async(matchmaker_ticket)
|
||||
matchmaker_ticket = ""
|
||||
matchmaking_state_changed.emit("idle")
|
||||
|
||||
|
||||
func _on_matchmaker_matched(matched) -> void:
|
||||
matchmaker_ticket = ""
|
||||
current_match = await socket.join_matched_async(matched)
|
||||
if current_match == null or current_match.is_exception():
|
||||
matchmaking_state_changed.emit("error")
|
||||
return
|
||||
matchmaking_state_changed.emit("matched")
|
||||
match_found.emit(current_match)
|
||||
await send_match_command(OP_READY, {})
|
||||
|
||||
|
||||
func send_match_command(op_code: int, payload: Dictionary) -> bool:
|
||||
if not is_online() or current_match == null:
|
||||
return false
|
||||
var result = await socket.send_match_state_async(current_match.match_id, op_code, JSON.stringify(payload))
|
||||
return result != null and not result.is_exception()
|
||||
|
||||
|
||||
func request_tower_build(tower_type: int, position: Vector2) -> bool:
|
||||
return await send_match_command(OP_BUILD, {"tower_type": tower_type, "x": position.x, "y": position.y})
|
||||
|
||||
|
||||
func request_tower_upgrade(tower_id: int) -> bool:
|
||||
return await send_match_command(OP_UPGRADE, {"tower_id": tower_id})
|
||||
|
||||
|
||||
func request_tower_sell(tower_id: int) -> bool:
|
||||
return await send_match_command(OP_SELL, {"tower_id": tower_id})
|
||||
|
||||
|
||||
func request_wave_start() -> bool:
|
||||
return await send_match_command(OP_START_WAVE, {})
|
||||
|
||||
|
||||
func leave_current_match() -> void:
|
||||
if socket and current_match != null:
|
||||
await socket.leave_match_async(current_match.match_id)
|
||||
current_match = null
|
||||
|
||||
|
||||
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)
|
||||
elif data.op_code == OP_EVENT:
|
||||
match_event_received.emit(parsed)
|
||||
|
||||
|
||||
func _on_socket_closed() -> void:
|
||||
if connection_state != "offline":
|
||||
_set_connection_state("authenticated")
|
||||
|
||||
|
||||
func _device_id() -> String:
|
||||
if account_manager and not account_manager.player_id.is_empty():
|
||||
return account_manager.player_id
|
||||
var fallback := "voidbastion_%s" % Crypto.new().generate_random_bytes(16).hex_encode()
|
||||
return fallback
|
||||
|
||||
|
||||
func is_authenticated() -> bool:
|
||||
return session != null and session.is_valid() and not session.expired
|
||||
|
||||
|
||||
func is_online() -> bool:
|
||||
return is_authenticated() and socket != null and socket.is_connected_to_host()
|
||||
|
||||
|
||||
func make_leaderboard_id(map_index: int, difficulty_index: int, mode: String) -> String:
|
||||
var safe_mode := mode if mode in ["mission", "endless", "coop_ai", "coop_online"] else "mission"
|
||||
return "%s_map%d_difficulty%d" % [safe_mode, clampi(map_index, 0, 2), clampi(difficulty_index, 0, 2)]
|
||||
|
||||
|
||||
func _parse_rpc_result(result) -> Dictionary:
|
||||
if result == null or result.is_exception():
|
||||
return {}
|
||||
var parsed = JSON.parse_string(result.payload)
|
||||
return parsed if parsed is Dictionary else {}
|
||||
|
||||
|
||||
func _set_connection_state(value: String) -> void:
|
||||
if connection_state == value:
|
||||
return
|
||||
connection_state = value
|
||||
connection_state_changed.emit(connection_state)
|
||||
Reference in new issue
Block a user