Refine shop, online sessions, and player feedback
This commit is contained in:
parent
2ffcbf51ac
commit
c038eeadea
47 changed files with 1208 additions and 189 deletions
|
|
@ -13,7 +13,12 @@ signal public_join_state_changed(state: int)
|
|||
const BASE_URL_SETTING: String = "network/discovery/base_url"
|
||||
const BASE_URL_ENVIRONMENT: String = "NETFISHING_DISCOVERY_URL"
|
||||
const SETTINGS_PATH: String = "user://network_discovery.cfg"
|
||||
const DEFAULT_ROOM_NAME: String = "NETfishing Room"
|
||||
const DEFAULT_ROOM_NAME: String = "Player's Server"
|
||||
const LEGACY_DEFAULT_ROOM_NAME: String = "NETfishing Room"
|
||||
const LEGACY_DEDICATED_DEFAULT_ROOM_NAME: String = (
|
||||
"NETfishing Dedicated Server"
|
||||
)
|
||||
const ROOM_NAME_SUFFIX: String = "'s Server"
|
||||
const MAX_ROOM_NAME_LENGTH: int = 48
|
||||
const HEARTBEAT_INTERVAL_SECONDS: float = 15.0
|
||||
const REQUEST_TIMEOUT_SECONDS: float = 8.0
|
||||
|
|
@ -52,6 +57,7 @@ enum HostRequestKind {
|
|||
var _session: NetworkSession
|
||||
var _base_url: String = ""
|
||||
var _room_name: String = DEFAULT_ROOM_NAME
|
||||
var _room_name_uses_default: bool = true
|
||||
var _discoverable: bool = false
|
||||
var _host_status_message: String = ""
|
||||
var _host_status_is_error: bool = false
|
||||
|
|
@ -145,6 +151,9 @@ func _ready() -> void:
|
|||
|
||||
func setup(session: NetworkSession) -> void:
|
||||
_session = session
|
||||
if _room_name_uses_default:
|
||||
_room_name = _default_room_name(_session.get_local_display_name())
|
||||
_save_settings()
|
||||
_session.set_session_display_name(_room_name)
|
||||
if not _session.state_changed.is_connected(_on_session_state_changed):
|
||||
_session.state_changed.connect(_on_session_state_changed)
|
||||
|
|
@ -216,8 +225,12 @@ func set_room_name(value: String) -> bool:
|
|||
_set_host_status("Room name cannot be empty.", true)
|
||||
return false
|
||||
if cleaned == _room_name:
|
||||
if _room_name_uses_default:
|
||||
_room_name_uses_default = false
|
||||
_save_settings()
|
||||
return true
|
||||
_room_name = cleaned
|
||||
_room_name_uses_default = false
|
||||
_save_settings()
|
||||
if _session != null:
|
||||
_session.set_session_display_name(_room_name)
|
||||
|
|
@ -278,6 +291,12 @@ func prepare_public_join(room: Dictionary) -> bool:
|
|||
if _session == null or not is_configured():
|
||||
_set_public_join_state(PublicJoinState.ERROR)
|
||||
return false
|
||||
if is_own_room(room):
|
||||
_set_public_join_state(PublicJoinState.ERROR)
|
||||
public_join_status_changed.emit(
|
||||
"You are already hosting this room.", true
|
||||
)
|
||||
return false
|
||||
var endpoint: String = room_endpoint(room)
|
||||
var room_id: String = str(room.get("room_id", "")).strip_edges()
|
||||
if endpoint.is_empty() or room_id.is_empty():
|
||||
|
|
@ -343,6 +362,13 @@ func room_endpoint(room: Dictionary) -> String:
|
|||
return "%s:%d" % [address, port]
|
||||
|
||||
|
||||
func is_own_room(room: Dictionary) -> bool:
|
||||
return (
|
||||
not _lease_room_id.is_empty()
|
||||
and str(room.get("room_id", "")) == _lease_room_id
|
||||
)
|
||||
|
||||
|
||||
func _configured_base_url() -> String:
|
||||
var value: String = OS.get_environment(BASE_URL_ENVIRONMENT).strip_edges()
|
||||
if value.is_empty():
|
||||
|
|
@ -957,6 +983,16 @@ func _sanitize_room_name(value: String) -> String:
|
|||
return cleaned.left(MAX_ROOM_NAME_LENGTH)
|
||||
|
||||
|
||||
func _default_room_name(player_name: String) -> String:
|
||||
var cleaned_name: String = player_name.strip_edges()
|
||||
if cleaned_name.is_empty():
|
||||
return DEFAULT_ROOM_NAME
|
||||
return "%s%s" % [
|
||||
cleaned_name.left(MAX_ROOM_NAME_LENGTH - ROOM_NAME_SUFFIX.length()),
|
||||
ROOM_NAME_SUFFIX,
|
||||
]
|
||||
|
||||
|
||||
func _load_settings() -> void:
|
||||
var config := ConfigFile.new()
|
||||
if config.load(SETTINGS_PATH) == OK:
|
||||
|
|
@ -965,11 +1001,23 @@ func _load_settings() -> void:
|
|||
)
|
||||
if not saved_name.is_empty():
|
||||
_room_name = saved_name
|
||||
_room_name_uses_default = bool(config.get_value(
|
||||
"host",
|
||||
"room_name_uses_default",
|
||||
saved_name in [
|
||||
DEFAULT_ROOM_NAME,
|
||||
LEGACY_DEFAULT_ROOM_NAME,
|
||||
LEGACY_DEDICATED_DEFAULT_ROOM_NAME,
|
||||
],
|
||||
))
|
||||
|
||||
|
||||
func _save_settings() -> void:
|
||||
var config := ConfigFile.new()
|
||||
config.set_value("host", "room_name", _room_name)
|
||||
config.set_value(
|
||||
"host", "room_name_uses_default", _room_name_uses_default
|
||||
)
|
||||
var error: Error = config.save(SETTINGS_PATH)
|
||||
if error != OK:
|
||||
push_warning("Could not save the local NETfishing room name.")
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@ func is_open_host() -> bool:
|
|||
return _session.is_open_host()
|
||||
|
||||
|
||||
func set_host_open(is_open: bool) -> bool:
|
||||
return _session.set_host_open(is_open)
|
||||
|
||||
|
||||
func get_relationships() -> Array[Dictionary]:
|
||||
return _relationships.get_records()
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ extends RefCounted
|
|||
|
||||
const PROTOCOL_VERSION: int = 3
|
||||
const GAME_BUILD: String = "prealpha"
|
||||
const MAX_GAME_VERSION_LENGTH: int = 64
|
||||
const MAX_DISPLAY_NAME_LENGTH: int = 24
|
||||
const MAX_PROFILE_ID_LENGTH: int = 96
|
||||
const MAX_NONCE_LENGTH: int = 96
|
||||
|
|
@ -37,9 +38,80 @@ enum RejectionCode {
|
|||
SERVER_SHUTTING_DOWN,
|
||||
UNSUPPORTED_CLIENT,
|
||||
BANNED,
|
||||
CLIENT_OUTDATED,
|
||||
SERVER_OUTDATED,
|
||||
VERSION_MISMATCH,
|
||||
}
|
||||
|
||||
|
||||
static func game_version() -> String:
|
||||
return str(ProjectSettings.get_setting(
|
||||
"application/config/version", "unknown"
|
||||
)).strip_edges()
|
||||
|
||||
|
||||
static func game_version_rejection(
|
||||
client_version: String,
|
||||
server_version: String,
|
||||
) -> RejectionCode:
|
||||
if client_version == server_version:
|
||||
return RejectionCode.NONE
|
||||
var comparison: int = _compare_game_versions(
|
||||
client_version, server_version
|
||||
)
|
||||
if comparison < 0:
|
||||
return RejectionCode.CLIENT_OUTDATED
|
||||
if comparison > 0:
|
||||
return RejectionCode.SERVER_OUTDATED
|
||||
return RejectionCode.VERSION_MISMATCH
|
||||
|
||||
|
||||
static func _compare_game_versions(first: String, second: String) -> int:
|
||||
var first_parts: PackedStringArray = first.split("-", true, 1)
|
||||
var second_parts: PackedStringArray = second.split("-", true, 1)
|
||||
var first_core: PackedStringArray = first_parts[0].split(".")
|
||||
var second_core: PackedStringArray = second_parts[0].split(".")
|
||||
for index: int in maxi(first_core.size(), second_core.size()):
|
||||
var first_number: int = (
|
||||
int(first_core[index])
|
||||
if index < first_core.size() and first_core[index].is_valid_int()
|
||||
else 0
|
||||
)
|
||||
var second_number: int = (
|
||||
int(second_core[index])
|
||||
if index < second_core.size() and second_core[index].is_valid_int()
|
||||
else 0
|
||||
)
|
||||
if first_number != second_number:
|
||||
return -1 if first_number < second_number else 1
|
||||
var first_rank: int = _prerelease_rank(
|
||||
first_parts[1] if first_parts.size() > 1 else ""
|
||||
)
|
||||
var second_rank: int = _prerelease_rank(
|
||||
second_parts[1] if second_parts.size() > 1 else ""
|
||||
)
|
||||
if first_rank != second_rank:
|
||||
return -1 if first_rank < second_rank else 1
|
||||
return 0
|
||||
|
||||
|
||||
static func _prerelease_rank(label: String) -> int:
|
||||
var normalized: String = label.to_lower()
|
||||
if normalized.is_empty():
|
||||
return 5
|
||||
if normalized.begins_with("dev"):
|
||||
return 0
|
||||
if normalized.begins_with("prealpha"):
|
||||
return 1
|
||||
if normalized.begins_with("alpha"):
|
||||
return 2
|
||||
if normalized.begins_with("beta"):
|
||||
return 3
|
||||
if normalized.begins_with("rc"):
|
||||
return 4
|
||||
return 2
|
||||
|
||||
|
||||
static func make_identity_hello(
|
||||
public_key: String,
|
||||
fingerprint: String,
|
||||
|
|
@ -48,6 +120,7 @@ static func make_identity_hello(
|
|||
) -> Dictionary:
|
||||
return {
|
||||
"protocol_version": PROTOCOL_VERSION,
|
||||
"game_version": game_version(),
|
||||
"public_key": public_key,
|
||||
"fingerprint": fingerprint,
|
||||
"client_nonce": client_nonce,
|
||||
|
|
@ -62,6 +135,9 @@ static func validate_identity_hello(data: Variant) -> bool:
|
|||
var value: Dictionary = data
|
||||
return (
|
||||
typeof(value.get("protocol_version")) == TYPE_INT
|
||||
and typeof(value.get("game_version")) == TYPE_STRING
|
||||
and not str(value["game_version"]).is_empty()
|
||||
and str(value["game_version"]).length() <= MAX_GAME_VERSION_LENGTH
|
||||
and typeof(value.get("public_key")) == TYPE_STRING
|
||||
and str(value["public_key"]).to_utf8_buffer().size() <= MAX_PUBLIC_KEY_LENGTH
|
||||
and NetworkIdentityCrypto.valid_fingerprint(value.get("fingerprint"))
|
||||
|
|
@ -85,6 +161,7 @@ static func make_client_hello(
|
|||
) -> Dictionary:
|
||||
return {
|
||||
"protocol_version": PROTOCOL_VERSION,
|
||||
"game_version": game_version(),
|
||||
"game_build": GAME_BUILD,
|
||||
"local_profile_id": profile_id,
|
||||
"display_name": display_name,
|
||||
|
|
@ -108,6 +185,7 @@ static func validate_client_hello(data: Variant) -> String:
|
|||
var payload: Dictionary = data
|
||||
for key: String in [
|
||||
"protocol_version",
|
||||
"game_version",
|
||||
"game_build",
|
||||
"local_profile_id",
|
||||
"display_name",
|
||||
|
|
@ -121,6 +199,12 @@ static func validate_client_hello(data: Variant) -> String:
|
|||
return "Handshake is missing %s." % key
|
||||
if typeof(payload["protocol_version"]) != TYPE_INT:
|
||||
return "Protocol version is invalid."
|
||||
if (
|
||||
typeof(payload["game_version"]) != TYPE_STRING
|
||||
or str(payload["game_version"]).is_empty()
|
||||
or str(payload["game_version"]).length() > MAX_GAME_VERSION_LENGTH
|
||||
):
|
||||
return "Game version is invalid."
|
||||
if typeof(payload["game_build"]) != TYPE_STRING:
|
||||
return "Game build is invalid."
|
||||
if typeof(payload["local_profile_id"]) != TYPE_STRING:
|
||||
|
|
@ -171,6 +255,7 @@ static func validate_client_hello(data: Variant) -> String:
|
|||
static func client_profile_fields(data: Dictionary) -> Array:
|
||||
var appearance: Dictionary = data.get("cosmetic_snapshot", {})
|
||||
return [
|
||||
str(data.get("game_version", "")),
|
||||
str(data.get("client_nonce", "")),
|
||||
str(data.get("identity_fingerprint", "")),
|
||||
str(data.get("local_profile_id", "")),
|
||||
|
|
@ -204,6 +289,7 @@ static func make_server_hello(
|
|||
"accepted": accepted,
|
||||
"rejection_code": int(rejection_code),
|
||||
"protocol_version": PROTOCOL_VERSION,
|
||||
"game_version": game_version(),
|
||||
"session_id": session_id,
|
||||
"assigned_peer_id": assigned_peer_id,
|
||||
"server_display_name": server_display_name,
|
||||
|
|
@ -251,5 +337,20 @@ static func rejection_text(code: int) -> String:
|
|||
return "This game build is not supported by the server."
|
||||
RejectionCode.BANNED:
|
||||
return "You are not permitted to join this server."
|
||||
RejectionCode.CLIENT_OUTDATED:
|
||||
return (
|
||||
"Your NETfishing version is out of date. "
|
||||
+ "Update the game to join this server."
|
||||
)
|
||||
RejectionCode.SERVER_OUTDATED:
|
||||
return (
|
||||
"This server is out of date. "
|
||||
+ "The host needs to update NETfishing."
|
||||
)
|
||||
RejectionCode.VERSION_MISMATCH:
|
||||
return (
|
||||
"This game and server use different NETfishing versions. "
|
||||
+ "Update both to the latest release."
|
||||
)
|
||||
_:
|
||||
return "The server rejected the connection."
|
||||
|
|
|
|||
|
|
@ -315,6 +315,10 @@ func get_session_display_name() -> String:
|
|||
return _session_display_name
|
||||
|
||||
|
||||
func get_local_display_name() -> String:
|
||||
return _profile.display_name if _profile != null else ""
|
||||
|
||||
|
||||
static func _can_bind_udp_port(port: int, bind_address: String = "*") -> bool:
|
||||
var probe := PacketPeerUDP.new()
|
||||
var error: Error = probe.bind(port, bind_address)
|
||||
|
|
@ -838,6 +842,16 @@ func submit_identity_hello(data: Dictionary) -> void:
|
|||
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||
if not is_host() or sender_id <= 1 or not _pending_authentication.has(sender_id):
|
||||
return
|
||||
var client_game_version: String = str(data.get("game_version", ""))
|
||||
var version_rejection: NetworkProtocol.RejectionCode = (
|
||||
NetworkProtocol.game_version_rejection(
|
||||
client_game_version,
|
||||
NetworkProtocol.game_version(),
|
||||
)
|
||||
)
|
||||
if version_rejection != NetworkProtocol.RejectionCode.NONE:
|
||||
_reject_peer(sender_id, version_rejection)
|
||||
return
|
||||
if (
|
||||
not NetworkProtocol.validate_identity_hello(data)
|
||||
or int(data["protocol_version"]) != NetworkProtocol.PROTOCOL_VERSION
|
||||
|
|
@ -868,8 +882,10 @@ func submit_identity_hello(data: Dictionary) -> void:
|
|||
"client_nonce": str(data["client_nonce"]),
|
||||
"client_fingerprint": fingerprint,
|
||||
"client_public_key": public_pem,
|
||||
"client_game_version": client_game_version,
|
||||
"server_nonce": server_nonce,
|
||||
"server_fingerprint": _host_identity.fingerprint,
|
||||
"server_game_version": NetworkProtocol.game_version(),
|
||||
"session_id": _session_id,
|
||||
"generation": _operation_generation,
|
||||
"expires_at_msec": Time.get_ticks_msec() + 60000,
|
||||
|
|
@ -886,7 +902,25 @@ func submit_identity_hello(data: Dictionary) -> void:
|
|||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
func receive_server_identity_proof(data: Dictionary) -> void:
|
||||
if state != State.AUTHENTICATING or not _valid_server_proof_shape(data):
|
||||
if state != State.AUTHENTICATING:
|
||||
return
|
||||
var server_game_version: String = str(data.get("server_game_version", ""))
|
||||
if server_game_version.is_empty():
|
||||
_fail_identity(NetworkProtocol.rejection_text(
|
||||
NetworkProtocol.RejectionCode.SERVER_OUTDATED
|
||||
))
|
||||
return
|
||||
var version_rejection: NetworkProtocol.RejectionCode = (
|
||||
NetworkProtocol.game_version_rejection(
|
||||
NetworkProtocol.game_version(),
|
||||
server_game_version,
|
||||
)
|
||||
)
|
||||
if version_rejection != NetworkProtocol.RejectionCode.NONE:
|
||||
_fail_identity(NetworkProtocol.rejection_text(version_rejection))
|
||||
return
|
||||
if not _valid_server_proof_shape(data):
|
||||
_fail_identity("The server sent an invalid identity proof.")
|
||||
return
|
||||
if (
|
||||
str(data["attempt_id"]) != str(_client_identity_attempt.get("attempt_id", ""))
|
||||
|
|
@ -995,6 +1029,7 @@ func submit_client_identity_proof(data: Dictionary) -> void:
|
|||
_authenticated_identity_cache[sender_id] = {
|
||||
"fingerprint": challenge["client_fingerprint"],
|
||||
"public_key": challenge["client_public_key"],
|
||||
"game_version": challenge["client_game_version"],
|
||||
}
|
||||
if (
|
||||
_host_bans != null
|
||||
|
|
@ -1041,6 +1076,16 @@ func submit_client_hello(data: Dictionary) -> void:
|
|||
if not validation_error.is_empty():
|
||||
_reject_peer(sender_id, NetworkProtocol.RejectionCode.MALFORMED_HANDSHAKE)
|
||||
return
|
||||
var client_game_version: String = str(data.get("game_version", ""))
|
||||
var version_rejection: NetworkProtocol.RejectionCode = (
|
||||
NetworkProtocol.game_version_rejection(
|
||||
client_game_version,
|
||||
NetworkProtocol.game_version(),
|
||||
)
|
||||
)
|
||||
if version_rejection != NetworkProtocol.RejectionCode.NONE:
|
||||
_reject_peer(sender_id, version_rejection)
|
||||
return
|
||||
var client_capabilities: PackedStringArray = _sanitized_capabilities(
|
||||
data.get("capability_flags", [])
|
||||
)
|
||||
|
|
@ -1056,6 +1101,7 @@ func submit_client_hello(data: Dictionary) -> void:
|
|||
return
|
||||
if (
|
||||
data["identity_fingerprint"] != identity["fingerprint"]
|
||||
or client_game_version != str(identity.get("game_version", ""))
|
||||
or not NetworkIdentityCrypto.verify_fields(
|
||||
NetworkIdentityCrypto.load_public_key(identity["public_key"]),
|
||||
"handshake_client_profile",
|
||||
|
|
@ -1179,6 +1225,7 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
if (
|
||||
typeof(data.get("accepted")) != TYPE_BOOL
|
||||
or typeof(data.get("protocol_version")) != TYPE_INT
|
||||
or typeof(data.get("game_version")) != TYPE_STRING
|
||||
or typeof(data.get("rejection_code")) != TYPE_INT
|
||||
):
|
||||
_teardown_peer()
|
||||
|
|
@ -1191,6 +1238,16 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
_teardown_peer()
|
||||
_fail(message)
|
||||
return
|
||||
var version_rejection: NetworkProtocol.RejectionCode = (
|
||||
NetworkProtocol.game_version_rejection(
|
||||
NetworkProtocol.game_version(),
|
||||
str(data["game_version"]),
|
||||
)
|
||||
)
|
||||
if version_rejection != NetworkProtocol.RejectionCode.NONE:
|
||||
_teardown_peer()
|
||||
_fail(NetworkProtocol.rejection_text(version_rejection))
|
||||
return
|
||||
if int(data["protocol_version"]) != NetworkProtocol.PROTOCOL_VERSION:
|
||||
_teardown_peer()
|
||||
_fail("The server uses a different network protocol.")
|
||||
|
|
@ -1457,6 +1514,8 @@ func _verify_spawn_identity(entry: Dictionary) -> bool:
|
|||
func _identity_proof_fields(data: Dictionary) -> Array:
|
||||
return [
|
||||
NetworkProtocol.PROTOCOL_VERSION,
|
||||
str(data.get("client_game_version", "")),
|
||||
str(data.get("server_game_version", "")),
|
||||
str(data.get("attempt_id", "")),
|
||||
str(data.get("client_fingerprint", "")),
|
||||
str(data.get("client_nonce", "")),
|
||||
|
|
@ -1497,6 +1556,10 @@ func _valid_server_proof_shape(data: Variant) -> bool:
|
|||
and str(value["server_nonce"]).length() == 64
|
||||
and NetworkIdentityCrypto.valid_fingerprint(value.get("client_fingerprint"))
|
||||
and NetworkIdentityCrypto.valid_fingerprint(value.get("server_fingerprint"))
|
||||
and typeof(value.get("client_game_version")) == TYPE_STRING
|
||||
and str(value["client_game_version"]) == NetworkProtocol.game_version()
|
||||
and typeof(value.get("server_game_version")) == TYPE_STRING
|
||||
and str(value["server_game_version"]) == NetworkProtocol.game_version()
|
||||
and typeof(value.get("server_public_key")) == TYPE_STRING
|
||||
and str(value["server_public_key"]).to_utf8_buffer().size()
|
||||
<= NetworkProtocol.MAX_PUBLIC_KEY_LENGTH
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue