1390 lines
43 KiB
GDScript
1390 lines
43 KiB
GDScript
class_name NetworkHomeService
|
|
extends Node
|
|
|
|
const NetworkHomeProtocolType = preload(
|
|
"res://network/network_home_protocol.gd"
|
|
)
|
|
const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
|
|
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
|
const OwnedItemType = preload("res://items/owned_item.gd")
|
|
|
|
signal local_transition_finished(accepted: bool, message: String)
|
|
signal local_space_changed(space_id: StringName, owner_fingerprint: String)
|
|
signal local_manifest_rejected(message: String)
|
|
|
|
const MAX_MANIFESTS: int = 128
|
|
const MAX_PENDING_LOCAL_DECOR_TRANSACTIONS: int = 16
|
|
|
|
var _session: NetworkSession
|
|
var _spawn_service: PlayerSpawnService
|
|
var _local_home: PlayerHomeStateType
|
|
var _save_manager: PlayerSaveManager
|
|
var _world_service: HomeWorldService
|
|
var _bag: PlayerBag
|
|
var _inventory_layout: PlayerInventoryLayout
|
|
var _hotbar: PlayerHotbar
|
|
var _manifests_by_fingerprint: Dictionary[String, Dictionary] = {}
|
|
var _fingerprint_by_peer: Dictionary[int, String] = {}
|
|
var _authorized_owned_counts: Dictionary[int, Dictionary] = {}
|
|
var _authorization_result_owner: Dictionary[String, int] = {}
|
|
var _interior_slots_by_space: Dictionary[StringName, Dictionary] = {}
|
|
var _next_interior_index: int = 0
|
|
var _suppress_local_submission: bool = false
|
|
var _publish_queued: bool = false
|
|
var _last_submitted_local_home_revision: int = -1
|
|
var _pending_local_decor_transactions: Dictionary[int, Dictionary] = {}
|
|
|
|
|
|
func setup(
|
|
session: NetworkSession,
|
|
spawn_service: PlayerSpawnService,
|
|
local_home: PlayerHomeStateType,
|
|
save_manager: PlayerSaveManager,
|
|
world_service: HomeWorldService,
|
|
bag: PlayerBag,
|
|
inventory_layout: PlayerInventoryLayout,
|
|
hotbar: PlayerHotbar,
|
|
) -> void:
|
|
_session = session
|
|
_spawn_service = spawn_service
|
|
_local_home = local_home
|
|
_save_manager = save_manager
|
|
_world_service = world_service
|
|
_bag = bag
|
|
_inventory_layout = inventory_layout
|
|
_hotbar = hotbar
|
|
if not _session.state_changed.is_connected(_on_session_state_changed):
|
|
_session.state_changed.connect(_on_session_state_changed)
|
|
if not _session.join_authenticated.is_connected(_on_join_authenticated):
|
|
_session.join_authenticated.connect(_on_join_authenticated)
|
|
if not _session.peer_authenticated.is_connected(_on_peer_authenticated):
|
|
_session.peer_authenticated.connect(_on_peer_authenticated)
|
|
if not _session.peer_removed.is_connected(_on_peer_removed):
|
|
_session.peer_removed.connect(_on_peer_removed)
|
|
if not _local_home.changed.is_connected(_on_local_home_changed):
|
|
_local_home.changed.connect(_on_local_home_changed)
|
|
|
|
|
|
func get_local_space() -> StringName:
|
|
if _session == null or not _session.is_gameplay_session_active():
|
|
return NetworkHomeProtocolType.WORLD_SPACE
|
|
return _session.get_peer_space(_session.get_local_peer_id())
|
|
|
|
|
|
func get_local_home_owner_fingerprint() -> String:
|
|
var space_id: String = String(get_local_space())
|
|
return space_id.trim_prefix("rv:") if space_id.begins_with("rv:") else ""
|
|
|
|
|
|
func can_local_decorate() -> bool:
|
|
return (
|
|
get_local_home_owner_fingerprint()
|
|
== _session.get_local_identity_fingerprint()
|
|
)
|
|
|
|
|
|
func has_pending_local_decor_mutation() -> bool:
|
|
return not _pending_local_decor_transactions.is_empty()
|
|
|
|
|
|
func get_authoritative_owned_count(
|
|
peer_id: int,
|
|
decor_id: StringName,
|
|
) -> int:
|
|
var authorized: Dictionary = _authorized_owned_counts.get(peer_id, {})
|
|
if authorized.has(String(decor_id)):
|
|
return int(authorized[String(decor_id)])
|
|
var fingerprint: String = _fingerprint_for_peer(peer_id)
|
|
var manifest: Dictionary = _manifests_by_fingerprint.get(fingerprint, {})
|
|
if manifest.is_empty():
|
|
return -1
|
|
var home: Dictionary = manifest.get("home", {})
|
|
var owned: Dictionary = home.get("owned_decor", {})
|
|
return int(owned.get(String(decor_id), 0))
|
|
|
|
|
|
func place_local_decor(
|
|
decor_id: StringName,
|
|
position: Vector3,
|
|
yaw_radians: float,
|
|
mode: PlayerHomeStateType.PlacementMode,
|
|
) -> String:
|
|
if (
|
|
not can_local_decorate()
|
|
or has_pending_local_decor_mutation()
|
|
or _bag == null
|
|
or _bag.get_quantity(decor_id) <= 0
|
|
or _world_service == null
|
|
):
|
|
return ""
|
|
var owner_fingerprint: String = (
|
|
_session.get_local_identity_fingerprint()
|
|
)
|
|
var normalized: Dictionary = (
|
|
_local_home.get_nearest_valid_placement_transform(
|
|
decor_id,
|
|
position,
|
|
yaw_radians,
|
|
mode,
|
|
"",
|
|
Callable(
|
|
self, "_candidate_clear_of_players"
|
|
).bind(owner_fingerprint, decor_id),
|
|
)
|
|
)
|
|
if normalized.is_empty():
|
|
return ""
|
|
var transaction: Dictionary = _capture_local_decor_transaction()
|
|
if transaction.is_empty():
|
|
return ""
|
|
_suppress_local_submission = true
|
|
var instance_id: String = _local_home.place_decor(
|
|
decor_id,
|
|
normalized["position"],
|
|
float(normalized["yaw"]),
|
|
mode,
|
|
)
|
|
if instance_id.is_empty():
|
|
_suppress_local_submission = false
|
|
return ""
|
|
if not _bag.remove_item(decor_id, 1):
|
|
_local_home.remove_placement(instance_id)
|
|
_suppress_local_submission = false
|
|
return ""
|
|
_suppress_local_submission = false
|
|
if not _commit_local_decor_transaction(transaction):
|
|
return ""
|
|
return instance_id
|
|
|
|
|
|
func remove_local_decor(instance_id: String) -> bool:
|
|
if (
|
|
not can_local_decorate()
|
|
or has_pending_local_decor_mutation()
|
|
or _bag == null
|
|
or _inventory_layout == null
|
|
or _hotbar == null
|
|
):
|
|
return false
|
|
var placement: Dictionary = _local_home.get_placement(instance_id)
|
|
if placement.is_empty():
|
|
return false
|
|
var transaction: Dictionary = _capture_local_decor_transaction()
|
|
if transaction.is_empty():
|
|
return false
|
|
var decor_id := StringName(str(placement["decor_id"]))
|
|
var target_hotbar_slot: int = _hotbar.find_item_slot(decor_id)
|
|
if target_hotbar_slot < 0:
|
|
target_hotbar_slot = _hotbar.find_first_empty_slot()
|
|
var prepared_hotbar_placement: bool = false
|
|
if (
|
|
target_hotbar_slot >= 0
|
|
and _inventory_layout.get_container(
|
|
PlayerInventoryLayout.EntryKind.ITEM,
|
|
decor_id,
|
|
) < 0
|
|
):
|
|
prepared_hotbar_placement = (
|
|
_inventory_layout.prepare_new_item_placement(
|
|
decor_id,
|
|
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
|
target_hotbar_slot,
|
|
)
|
|
)
|
|
var returned_to_inventory: bool = _bag.add_item(decor_id, 1)
|
|
if not returned_to_inventory:
|
|
if prepared_hotbar_placement:
|
|
_inventory_layout.cancel_prepared_item_placement(decor_id)
|
|
returned_to_inventory = _bag.add_item_to_storage(decor_id, 1)
|
|
if not returned_to_inventory:
|
|
if prepared_hotbar_placement:
|
|
_inventory_layout.cancel_prepared_item_placement(decor_id)
|
|
return false
|
|
if target_hotbar_slot >= 0:
|
|
_hotbar.assign_item(target_hotbar_slot, decor_id)
|
|
_hotbar.select_slot(target_hotbar_slot)
|
|
_suppress_local_submission = true
|
|
if not _local_home.remove_placement(instance_id):
|
|
_suppress_local_submission = false
|
|
_restore_local_decor_transaction(transaction)
|
|
return false
|
|
_suppress_local_submission = false
|
|
if not _commit_local_decor_transaction(transaction):
|
|
return false
|
|
return true
|
|
|
|
|
|
func update_local_decor(
|
|
instance_id: String,
|
|
position: Vector3,
|
|
yaw_radians: float,
|
|
mode: PlayerHomeStateType.PlacementMode,
|
|
) -> bool:
|
|
if has_pending_local_decor_mutation():
|
|
return false
|
|
var normalized: Dictionary = get_valid_local_decor_transform(
|
|
instance_id, position, yaw_radians, mode
|
|
)
|
|
if normalized.is_empty():
|
|
return false
|
|
var transaction: Dictionary = _capture_local_decor_transaction()
|
|
if transaction.is_empty():
|
|
return false
|
|
_suppress_local_submission = true
|
|
if not _local_home.update_placement(
|
|
instance_id,
|
|
normalized["position"],
|
|
float(normalized["yaw"]),
|
|
mode,
|
|
):
|
|
_suppress_local_submission = false
|
|
return false
|
|
_suppress_local_submission = false
|
|
if not _commit_local_decor_transaction(transaction):
|
|
return false
|
|
return true
|
|
|
|
|
|
func get_valid_local_decor_transform(
|
|
instance_id: String,
|
|
position: Vector3,
|
|
yaw_radians: float,
|
|
mode: PlayerHomeStateType.PlacementMode,
|
|
) -> Dictionary:
|
|
if not can_local_decorate() or _world_service == null:
|
|
return {}
|
|
var placement: Dictionary = _local_home.get_placement(instance_id)
|
|
if placement.is_empty():
|
|
return {}
|
|
var decor_id := StringName(str(placement.get("decor_id", "")))
|
|
return _local_home.get_valid_update_transform(
|
|
instance_id,
|
|
position,
|
|
yaw_radians,
|
|
mode,
|
|
Callable(
|
|
self, "_candidate_clear_of_players"
|
|
).bind(
|
|
_session.get_local_identity_fingerprint(), decor_id
|
|
),
|
|
)
|
|
|
|
|
|
func _candidate_clear_of_players(
|
|
candidate: Dictionary,
|
|
owner_fingerprint: String,
|
|
decor_id: StringName,
|
|
) -> bool:
|
|
return (
|
|
_world_service != null
|
|
and _world_service.is_decor_placement_clear_of_players(
|
|
owner_fingerprint,
|
|
decor_id,
|
|
candidate["position"],
|
|
float(candidate["yaw"]),
|
|
)
|
|
)
|
|
|
|
|
|
func request_enter(owner_fingerprint: String) -> bool:
|
|
if (
|
|
_session == null
|
|
or not _session.is_gameplay_session_active()
|
|
or not _manifests_by_fingerprint.has(owner_fingerprint)
|
|
or get_local_space() != NetworkHomeProtocolType.WORLD_SPACE
|
|
):
|
|
return false
|
|
return _submit_transition(
|
|
NetworkHomeProtocolType.TransitionAction.ENTER,
|
|
owner_fingerprint,
|
|
)
|
|
|
|
|
|
func request_exit() -> bool:
|
|
var owner_fingerprint: String = get_local_home_owner_fingerprint()
|
|
if owner_fingerprint.is_empty():
|
|
return false
|
|
return _submit_transition(
|
|
NetworkHomeProtocolType.TransitionAction.EXIT,
|
|
owner_fingerprint,
|
|
)
|
|
|
|
|
|
func authorize_decor_purchase(
|
|
peer_id: int,
|
|
product_id: StringName,
|
|
resulting_count: int,
|
|
result_id: String,
|
|
) -> void:
|
|
if not _session.is_host() or peer_id <= 0 or result_id.is_empty():
|
|
return
|
|
var counts: Dictionary = _authorized_owned_counts.get(peer_id, {})
|
|
counts[String(product_id)] = resulting_count
|
|
_authorized_owned_counts[peer_id] = counts
|
|
_authorization_result_owner[result_id] = peer_id
|
|
|
|
|
|
func finalize_decor_purchase(
|
|
peer_id: int,
|
|
result_id: String,
|
|
applied: bool,
|
|
) -> void:
|
|
if _authorization_result_owner.get(result_id, 0) != peer_id:
|
|
return
|
|
_authorization_result_owner.erase(result_id)
|
|
if applied:
|
|
return
|
|
_authorized_owned_counts.erase(peer_id)
|
|
|
|
|
|
func refresh_world_presentations() -> void:
|
|
if _world_service == null:
|
|
return
|
|
_world_service.clear_presentations()
|
|
if not _world_service.is_world_ready():
|
|
return
|
|
if not _session.is_host():
|
|
for manifest: Dictionary in _manifests_by_fingerprint.values():
|
|
_world_service.apply_manifest(manifest)
|
|
return
|
|
var local_fingerprint: String = _session.get_local_identity_fingerprint()
|
|
if (
|
|
not _session.is_dedicated_host()
|
|
and not _manifests_by_fingerprint.has(local_fingerprint)
|
|
):
|
|
publish_local_home()
|
|
var fingerprints: Array[String] = []
|
|
fingerprints.assign(_manifests_by_fingerprint.keys())
|
|
fingerprints.sort()
|
|
var reserved: Array[Transform3D] = []
|
|
for fingerprint: String in fingerprints:
|
|
var manifest: Dictionary = _manifests_by_fingerprint[fingerprint]
|
|
var transform: Transform3D = _world_service.choose_exterior_transform(
|
|
int(manifest["owner_peer_id"]), reserved
|
|
)
|
|
reserved.append(transform)
|
|
manifest["exterior_transform"] = (
|
|
NetworkHomeProtocolType.transform_to_array(transform)
|
|
)
|
|
_manifests_by_fingerprint[fingerprint] = manifest
|
|
_broadcast_manifest(manifest)
|
|
for fingerprint: String in fingerprints:
|
|
var manifest: Dictionary = _manifests_by_fingerprint[fingerprint]
|
|
_spawn_peer_at_exterior(
|
|
int(manifest["owner_peer_id"]), fingerprint
|
|
)
|
|
|
|
|
|
func publish_local_home(
|
|
allow_pending_decor_transaction: bool = false,
|
|
) -> bool:
|
|
if (
|
|
_session == null
|
|
or not _session.is_gameplay_session_active()
|
|
or _session.is_dedicated_host()
|
|
or _local_home == null
|
|
):
|
|
return false
|
|
# A purchase can update the owned decor count while an optimistic placement
|
|
# is waiting on the host. Hold that follow-up manifest until the placement is
|
|
# accepted or rolled back; otherwise it can carry the still-unverified
|
|
# placement and turn one rejection into a second stale rejection.
|
|
if (
|
|
not allow_pending_decor_transaction
|
|
and has_pending_local_decor_mutation()
|
|
):
|
|
_publish_queued = true
|
|
return true
|
|
if _session.is_host() and (
|
|
_world_service == null or not _world_service.is_world_ready()
|
|
):
|
|
return false
|
|
var home_data: Dictionary = _local_home.to_save_data()
|
|
var revision: int = int(home_data.get("revision", -1))
|
|
if revision >= 0 and revision == _last_submitted_local_home_revision:
|
|
return true
|
|
var request: Dictionary = {
|
|
"request_id": _new_id("home"),
|
|
"session_id": _session.get_session_id(),
|
|
"owner_fingerprint": _session.get_local_identity_fingerprint(),
|
|
"home": home_data,
|
|
"digest": NetworkHomeProtocolType.manifest_digest(home_data),
|
|
}
|
|
request["sender_signature"] = _session.sign_local_action(
|
|
"portable_home_manifest",
|
|
NetworkHomeProtocolType.manifest_signature_fields(request),
|
|
)
|
|
_last_submitted_local_home_revision = revision
|
|
if _session.is_host():
|
|
_process_manifest_request(_session.get_local_peer_id(), request)
|
|
else:
|
|
submit_home_manifest.rpc_id(1, request)
|
|
return true
|
|
|
|
|
|
@rpc(
|
|
"any_peer", "call_remote", "reliable",
|
|
NetworkHomeProtocolType.RELIABLE_CHANNEL,
|
|
)
|
|
func submit_home_manifest(data: Dictionary) -> void:
|
|
var sender_id: int = multiplayer.get_remote_sender_id()
|
|
if (
|
|
_session == null
|
|
or not _session.is_host()
|
|
or not _session.is_authenticated_peer(sender_id)
|
|
):
|
|
return
|
|
_process_manifest_request(sender_id, data)
|
|
|
|
|
|
func _process_manifest_request(peer_id: int, data: Dictionary) -> void:
|
|
if (
|
|
not NetworkHomeProtocolType.validate_manifest_request(data)
|
|
or str(data["session_id"]) != _session.get_session_id()
|
|
):
|
|
_reject_manifest(peer_id, "The RV update was invalid.")
|
|
return
|
|
var record := _session.get_peer_record(peer_id)
|
|
var fingerprint: String = str(data["owner_fingerprint"])
|
|
if (
|
|
record == null
|
|
or record.identity_fingerprint != fingerprint
|
|
or not _session.verify_peer_action(
|
|
peer_id,
|
|
"portable_home_manifest",
|
|
NetworkHomeProtocolType.manifest_signature_fields(data),
|
|
data["sender_signature"],
|
|
)
|
|
):
|
|
_reject_manifest(peer_id, "The RV owner could not be verified.")
|
|
return
|
|
var home_data: Dictionary = PlayerHomeStateType.sanitize_save_data(
|
|
data["home"]
|
|
)
|
|
var prior: Dictionary = _manifests_by_fingerprint.get(fingerprint, {})
|
|
var is_first_manifest: bool = prior.is_empty()
|
|
if not _valid_manifest_revision(peer_id, prior, home_data):
|
|
_reject_manifest(peer_id, "The RV update was rejected by the host.")
|
|
return
|
|
if not _moved_decor_clear_of_players(fingerprint, prior, home_data):
|
|
_reject_manifest(peer_id, "A player is in the way of that decor.")
|
|
return
|
|
var manifest: Dictionary
|
|
if prior.is_empty():
|
|
if _manifests_by_fingerprint.size() >= MAX_MANIFESTS:
|
|
_reject_manifest(peer_id, "This room cannot place another RV.")
|
|
return
|
|
var reserved: Array[Transform3D] = []
|
|
for existing: Dictionary in _manifests_by_fingerprint.values():
|
|
reserved.append(NetworkHomeProtocolType.array_to_transform(
|
|
existing["exterior_transform"]
|
|
))
|
|
var exterior_transform: Transform3D = (
|
|
_world_service.choose_exterior_transform(peer_id, reserved)
|
|
)
|
|
manifest = {
|
|
"owner_peer_id": peer_id,
|
|
"owner_fingerprint": fingerprint,
|
|
"home": home_data,
|
|
"exterior_transform": (
|
|
NetworkHomeProtocolType.transform_to_array(exterior_transform)
|
|
),
|
|
"interior_index": _next_interior_index,
|
|
}
|
|
_next_interior_index += 1
|
|
else:
|
|
manifest = prior.duplicate(true)
|
|
manifest["home"] = home_data
|
|
manifest["owner_peer_id"] = peer_id
|
|
_manifests_by_fingerprint[fingerprint] = manifest
|
|
_fingerprint_by_peer[peer_id] = fingerprint
|
|
_consume_owned_authorizations(peer_id, home_data)
|
|
if peer_id == _session.get_local_peer_id():
|
|
_discard_local_decor_transactions_through(
|
|
int(home_data.get("revision", -1))
|
|
)
|
|
if _last_submitted_local_home_revision == int(
|
|
home_data.get("revision", -1)
|
|
):
|
|
_last_submitted_local_home_revision = -1
|
|
_flush_queued_local_home_publish()
|
|
_broadcast_manifest(manifest)
|
|
if is_first_manifest:
|
|
_spawn_peer_at_exterior(peer_id, fingerprint)
|
|
|
|
|
|
func _valid_manifest_revision(
|
|
peer_id: int,
|
|
prior_manifest: Dictionary,
|
|
home_data: Dictionary,
|
|
) -> bool:
|
|
if prior_manifest.is_empty():
|
|
# A portable RV is owned by the player's signed local save. A new host has
|
|
# no prior room ledger with which to compare its first manifest.
|
|
return true
|
|
var prior_home: Dictionary = prior_manifest["home"]
|
|
if (
|
|
int(home_data["revision"]) <= int(prior_home["revision"])
|
|
or int(home_data["tier"]) != int(prior_home["tier"])
|
|
or str(home_data["exterior_id"]) != str(prior_home["exterior_id"])
|
|
):
|
|
return false
|
|
var prior_owned: Dictionary = prior_home["owned_decor"]
|
|
var next_owned: Dictionary = home_data["owned_decor"]
|
|
var all_ids: Dictionary[String, bool] = {}
|
|
for value: Variant in prior_owned.keys():
|
|
all_ids[str(value)] = true
|
|
for value: Variant in next_owned.keys():
|
|
all_ids[str(value)] = true
|
|
var authorized: Dictionary = _authorized_owned_counts.get(peer_id, {})
|
|
for product_id: String in all_ids:
|
|
var prior_count: int = int(prior_owned.get(product_id, 0))
|
|
var next_count: int = int(next_owned.get(product_id, 0))
|
|
if prior_count == next_count:
|
|
continue
|
|
if next_count < prior_count or int(authorized.get(product_id, -1)) != next_count:
|
|
return false
|
|
return true
|
|
|
|
|
|
func _moved_decor_clear_of_players(
|
|
owner_fingerprint: String,
|
|
prior_manifest: Dictionary,
|
|
home_data: Dictionary,
|
|
) -> bool:
|
|
if _world_service == null:
|
|
return false
|
|
var prior_by_instance: Dictionary[String, Dictionary] = {}
|
|
if not prior_manifest.is_empty():
|
|
var prior_home: Dictionary = prior_manifest.get("home", {})
|
|
for value: Variant in prior_home.get("placements", []):
|
|
if typeof(value) != TYPE_DICTIONARY:
|
|
continue
|
|
var prior_placement: Dictionary = value
|
|
prior_by_instance[str(
|
|
prior_placement.get("instance_id", "")
|
|
)] = prior_placement
|
|
for value: Variant in home_data.get("placements", []):
|
|
if typeof(value) != TYPE_DICTIONARY:
|
|
return false
|
|
var placement: Dictionary = value
|
|
var instance_id: String = str(placement.get("instance_id", ""))
|
|
var prior_placement: Dictionary = prior_by_instance.get(
|
|
instance_id, {}
|
|
)
|
|
if (
|
|
not prior_placement.is_empty()
|
|
and _same_decor_transform(prior_placement, placement)
|
|
):
|
|
continue
|
|
var decor_id := StringName(str(placement.get("decor_id", "")))
|
|
var position: Vector3 = _placement_position(placement)
|
|
if (
|
|
not position.is_finite()
|
|
or not _world_service.is_decor_placement_clear_of_players(
|
|
owner_fingerprint,
|
|
decor_id,
|
|
position,
|
|
float(placement.get("yaw", 0.0)),
|
|
)
|
|
):
|
|
return false
|
|
return true
|
|
|
|
|
|
static func _same_decor_transform(
|
|
first: Dictionary,
|
|
second: Dictionary,
|
|
) -> bool:
|
|
if str(first.get("decor_id", "")) != str(second.get("decor_id", "")):
|
|
return false
|
|
var first_position: Vector3 = _placement_position(first)
|
|
var second_position: Vector3 = _placement_position(second)
|
|
return (
|
|
first_position.is_equal_approx(second_position)
|
|
and is_equal_approx(
|
|
float(first.get("yaw", 0.0)),
|
|
float(second.get("yaw", 0.0)),
|
|
)
|
|
)
|
|
|
|
|
|
static func _placement_position(placement: Dictionary) -> Vector3:
|
|
var values: Array = placement.get("position", [])
|
|
if values.size() != 3:
|
|
return Vector3.INF
|
|
return Vector3(
|
|
float(values[0]), float(values[1]), float(values[2])
|
|
)
|
|
|
|
|
|
func _consume_owned_authorizations(peer_id: int, home_data: Dictionary) -> void:
|
|
var authorized: Dictionary = _authorized_owned_counts.get(peer_id, {})
|
|
if authorized.is_empty():
|
|
return
|
|
var owned: Dictionary = home_data["owned_decor"]
|
|
for product_id: String in authorized.keys():
|
|
if int(owned.get(product_id, -1)) == int(authorized[product_id]):
|
|
authorized.erase(product_id)
|
|
if authorized.is_empty():
|
|
_authorized_owned_counts.erase(peer_id)
|
|
else:
|
|
_authorized_owned_counts[peer_id] = authorized
|
|
|
|
|
|
func _broadcast_manifest(manifest: Dictionary) -> void:
|
|
if _world_service != null and _world_service.is_world_ready():
|
|
_world_service.apply_manifest(manifest)
|
|
for peer_id: int in _session.get_authenticated_peer_ids():
|
|
if peer_id == 1:
|
|
continue
|
|
receive_home_manifest.rpc_id(peer_id, manifest)
|
|
|
|
|
|
@rpc(
|
|
"authority", "call_remote", "reliable",
|
|
NetworkHomeProtocolType.RELIABLE_CHANNEL,
|
|
)
|
|
func receive_home_manifest(data: Dictionary) -> void:
|
|
if not NetworkHomeProtocolType.validate_manifest_broadcast(data):
|
|
return
|
|
var fingerprint: String = str(data["owner_fingerprint"])
|
|
_manifests_by_fingerprint[fingerprint] = data.duplicate(true)
|
|
_fingerprint_by_peer[int(data["owner_peer_id"])] = fingerprint
|
|
if fingerprint == _session.get_local_identity_fingerprint():
|
|
var home: Dictionary = data.get("home", {})
|
|
_discard_local_decor_transactions_through(
|
|
int(home.get("revision", -1))
|
|
)
|
|
if _last_submitted_local_home_revision == int(
|
|
home.get("revision", -1)
|
|
):
|
|
_last_submitted_local_home_revision = -1
|
|
_flush_queued_local_home_publish()
|
|
if _world_service != null and _world_service.is_world_ready():
|
|
_world_service.apply_manifest(data)
|
|
|
|
|
|
func _reject_manifest(peer_id: int, message: String) -> void:
|
|
var fingerprint: String = _fingerprint_for_peer(peer_id)
|
|
var canonical: Dictionary = _manifests_by_fingerprint.get(fingerprint, {})
|
|
if peer_id == _session.get_local_peer_id():
|
|
_apply_manifest_rejection(message, canonical)
|
|
else:
|
|
receive_home_manifest_rejection.rpc_id(peer_id, message, canonical)
|
|
|
|
|
|
@rpc(
|
|
"authority", "call_remote", "reliable",
|
|
NetworkHomeProtocolType.RELIABLE_CHANNEL,
|
|
)
|
|
func receive_home_manifest_rejection(
|
|
message: String,
|
|
canonical: Dictionary,
|
|
) -> void:
|
|
_apply_manifest_rejection(message, canonical)
|
|
|
|
|
|
func _apply_manifest_rejection(message: String, canonical: Dictionary) -> void:
|
|
_last_submitted_local_home_revision = -1
|
|
var canonical_home: Dictionary = canonical.get("home", {})
|
|
var canonical_revision: int = int(canonical_home.get("revision", -1))
|
|
var transaction: Dictionary = _get_local_decor_rejection_transaction(
|
|
canonical_revision
|
|
)
|
|
if canonical.is_empty() or not NetworkHomeProtocolType.validate_manifest_broadcast(canonical):
|
|
if not transaction.is_empty():
|
|
_restore_local_decor_transaction(transaction)
|
|
_pending_local_decor_transactions.clear()
|
|
if _save_manager != null and not _save_manager.save_if_dirty():
|
|
push_warning("Unable to save the restored RV decor state.")
|
|
_flush_queued_local_home_publish()
|
|
local_manifest_rejected.emit(message)
|
|
return
|
|
# A shop result can be accepted locally while its own home manifest is still
|
|
# in flight. A delayed rejection for an earlier placement must not erase that
|
|
# accepted purchase just because the host's canonical manifest predates it.
|
|
var locally_owned_decor: Dictionary[StringName, int] = (
|
|
_capture_local_owned_decor_counts()
|
|
)
|
|
# Only carry ownership forward across an actual pending decor-action
|
|
# rejection. A standalone rejected manifest must remain authoritative rather
|
|
# than repeatedly re-submitting an unverified owned-count increase.
|
|
var has_owned_decor_increase: bool = (
|
|
not transaction.is_empty()
|
|
and _has_local_owned_decor_increase(
|
|
locally_owned_decor,
|
|
canonical_home,
|
|
)
|
|
)
|
|
_suppress_local_submission = true
|
|
var restored: bool = _local_home.restore_from_save_data(canonical_home)
|
|
var owned_decor_restored: bool = (
|
|
restored
|
|
and (
|
|
not has_owned_decor_increase
|
|
or _restore_local_owned_decor_increases(locally_owned_decor)
|
|
)
|
|
)
|
|
var reconciled: bool = (
|
|
restored
|
|
and owned_decor_restored
|
|
and _reconcile_local_decor_inventory(transaction)
|
|
)
|
|
_suppress_local_submission = false
|
|
_discard_local_decor_transactions_after(canonical_revision)
|
|
if not reconciled:
|
|
push_error("Unable to reconcile RV decor after a host rejection.")
|
|
elif _save_manager != null and not _save_manager.save_if_dirty():
|
|
push_warning("Unable to save the restored RV decor state.")
|
|
receive_home_manifest(canonical)
|
|
if reconciled and has_owned_decor_increase:
|
|
# Submit the preserved purchase as a fresh revision after applying the
|
|
# canonical placement rollback. This is deliberately separate from the
|
|
# rejected action, so the host can validate the already-authorized count.
|
|
if not publish_local_home():
|
|
push_warning("Unable to publish the restored RV decor ownership.")
|
|
local_manifest_rejected.emit(message)
|
|
|
|
|
|
func _capture_local_decor_transaction() -> Dictionary:
|
|
if (
|
|
_local_home == null
|
|
or _bag == null
|
|
or _inventory_layout == null
|
|
or _hotbar == null
|
|
):
|
|
return {}
|
|
var saved_items: Array[OwnedItemType] = []
|
|
for item: OwnedItemType in _bag.get_all_items():
|
|
saved_items.append(item.duplicate_record())
|
|
return {
|
|
"home": _local_home.to_save_data(),
|
|
"bag_items": saved_items,
|
|
"layout": _inventory_layout.to_save_data(),
|
|
"hotbar_slots": _hotbar.get_slots(),
|
|
"hotbar_fish_slots": _hotbar.get_fish_slots(),
|
|
"selected_hotbar_slot": _hotbar.get_selected_slot(),
|
|
}
|
|
|
|
|
|
func _capture_local_owned_decor_counts() -> Dictionary[StringName, int]:
|
|
var counts: Dictionary[StringName, int] = {}
|
|
if _local_home == null:
|
|
return counts
|
|
for decor_id: StringName in DecorCatalogType.get_product_ids():
|
|
counts[decor_id] = _local_home.get_owned_count(decor_id)
|
|
return counts
|
|
|
|
|
|
func _has_local_owned_decor_increase(
|
|
local_counts: Dictionary[StringName, int],
|
|
canonical_home: Dictionary,
|
|
) -> bool:
|
|
var canonical_owned: Dictionary = canonical_home.get("owned_decor", {})
|
|
for decor_id: StringName in DecorCatalogType.get_product_ids():
|
|
if int(local_counts.get(decor_id, 0)) > int(
|
|
canonical_owned.get(String(decor_id), 0)
|
|
):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _restore_local_owned_decor_increases(
|
|
local_counts: Dictionary[StringName, int],
|
|
) -> bool:
|
|
if _local_home == null:
|
|
return false
|
|
for decor_id: StringName in DecorCatalogType.get_product_ids():
|
|
var local_count: int = int(local_counts.get(decor_id, 0))
|
|
var canonical_count: int = _local_home.get_owned_count(decor_id)
|
|
while canonical_count < local_count:
|
|
if not _local_home.add_owned_decor(decor_id, 1):
|
|
return false
|
|
canonical_count += 1
|
|
return true
|
|
|
|
|
|
func _commit_local_decor_transaction(transaction: Dictionary) -> bool:
|
|
if _save_manager == null or not _save_manager.save_if_dirty():
|
|
_restore_local_decor_transaction(transaction)
|
|
return false
|
|
var revision: int = _local_home.get_revision()
|
|
_pending_local_decor_transactions[revision] = transaction.duplicate(true)
|
|
_trim_pending_local_decor_transactions()
|
|
if publish_local_home(true):
|
|
return true
|
|
_pending_local_decor_transactions.erase(revision)
|
|
_restore_local_decor_transaction(transaction)
|
|
_flush_queued_local_home_publish()
|
|
# The optimistic mutation was already written before we attempted to publish
|
|
# it. If the session ended or could not accept the manifest, immediately
|
|
# persist the restored state so a restart cannot resurrect the rejected
|
|
# placement or consume its decor item.
|
|
if _save_manager != null and not _save_manager.save_if_dirty():
|
|
push_warning("Unable to save the restored RV decor state.")
|
|
return false
|
|
|
|
|
|
func _restore_local_decor_transaction(transaction: Dictionary) -> bool:
|
|
if (
|
|
transaction.is_empty()
|
|
or _local_home == null
|
|
or _bag == null
|
|
or _inventory_layout == null
|
|
or _hotbar == null
|
|
):
|
|
return false
|
|
var home_data: Variant = transaction.get("home", {})
|
|
var layout_data: Variant = transaction.get("layout", {})
|
|
var stored_items: Variant = transaction.get("bag_items", [])
|
|
var stored_slots: Variant = transaction.get("hotbar_slots", [])
|
|
var stored_fish_slots: Variant = transaction.get("hotbar_fish_slots", [])
|
|
if (
|
|
typeof(home_data) != TYPE_DICTIONARY
|
|
or typeof(layout_data) != TYPE_DICTIONARY
|
|
or typeof(stored_items) != TYPE_ARRAY
|
|
or typeof(stored_slots) != TYPE_ARRAY
|
|
or typeof(stored_fish_slots) != TYPE_ARRAY
|
|
):
|
|
return false
|
|
var items: Array[OwnedItemType] = []
|
|
for value: Variant in stored_items as Array:
|
|
if not value is OwnedItemType:
|
|
return false
|
|
items.append((value as OwnedItemType).duplicate_record())
|
|
var slots: Array[StringName] = []
|
|
for value: Variant in stored_slots as Array:
|
|
slots.append(StringName(str(value)))
|
|
var fish_slots: Array[StringName] = []
|
|
for value: Variant in stored_fish_slots as Array:
|
|
fish_slots.append(StringName(str(value)))
|
|
var was_suppressed: bool = _suppress_local_submission
|
|
_suppress_local_submission = true
|
|
var home_restored: bool = _local_home.restore_from_save_data(
|
|
home_data as Dictionary
|
|
)
|
|
var bag_restored: bool = _bag.replace_all_items(items)
|
|
var layout_restored: bool = _inventory_layout.restore_from_save_data(
|
|
layout_data as Dictionary
|
|
)
|
|
var hotbar_restored: bool = _hotbar.replace_state(
|
|
slots,
|
|
int(transaction.get("selected_hotbar_slot", 0)),
|
|
fish_slots,
|
|
false,
|
|
)
|
|
_suppress_local_submission = was_suppressed
|
|
return home_restored and bag_restored and layout_restored and hotbar_restored
|
|
|
|
|
|
func _reconcile_local_decor_inventory(transaction: Dictionary) -> bool:
|
|
if _bag == null or _local_home == null:
|
|
return false
|
|
var preferred_slots: Dictionary[StringName, int] = (
|
|
_decor_hotbar_slots_from_transaction(transaction)
|
|
)
|
|
for decor_id: StringName in DecorCatalogType.get_product_ids():
|
|
var target_quantity: int = _local_home.get_available_count(decor_id)
|
|
var current_quantity: int = _bag.get_quantity(decor_id)
|
|
if current_quantity > target_quantity:
|
|
if not _bag.remove_item(
|
|
decor_id, current_quantity - target_quantity
|
|
):
|
|
return false
|
|
continue
|
|
while current_quantity < target_quantity:
|
|
var preferred_slot: int = int(
|
|
preferred_slots.get(decor_id, -1)
|
|
)
|
|
if not _restore_decor_item_to_bag(decor_id, preferred_slot):
|
|
return false
|
|
current_quantity += 1
|
|
return true
|
|
|
|
|
|
func _decor_hotbar_slots_from_transaction(
|
|
transaction: Dictionary,
|
|
) -> Dictionary[StringName, int]:
|
|
var result: Dictionary[StringName, int] = {}
|
|
var stored_slots: Variant = transaction.get("hotbar_slots", [])
|
|
if typeof(stored_slots) != TYPE_ARRAY:
|
|
return result
|
|
for slot_index: int in range((stored_slots as Array).size()):
|
|
var decor_id := StringName(str((stored_slots as Array)[slot_index]))
|
|
if DecorCatalogType.has_product(decor_id):
|
|
result[decor_id] = slot_index
|
|
return result
|
|
|
|
|
|
func _restore_decor_item_to_bag(
|
|
decor_id: StringName,
|
|
preferred_hotbar_slot: int,
|
|
) -> bool:
|
|
if _bag == null:
|
|
return false
|
|
var prepared_slot: int = -1
|
|
if (
|
|
_inventory_layout != null
|
|
and _hotbar != null
|
|
and _inventory_layout.get_container(
|
|
PlayerInventoryLayout.EntryKind.ITEM, decor_id
|
|
) < 0
|
|
):
|
|
var candidate_slot: int = preferred_hotbar_slot
|
|
if candidate_slot < 0:
|
|
candidate_slot = _hotbar.find_first_empty_slot()
|
|
if candidate_slot >= 0 and _inventory_layout.prepare_new_item_placement(
|
|
decor_id,
|
|
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
|
candidate_slot,
|
|
):
|
|
prepared_slot = candidate_slot
|
|
var restored: bool = _bag.add_item(decor_id, 1)
|
|
if not restored:
|
|
if prepared_slot >= 0:
|
|
_inventory_layout.cancel_prepared_item_placement(decor_id)
|
|
if _bag.get_owned_item(decor_id) == null:
|
|
restored = _bag.add_item_to_storage(decor_id, 1)
|
|
if restored and prepared_slot >= 0:
|
|
_hotbar.assign_item(prepared_slot, decor_id)
|
|
_hotbar.select_slot(prepared_slot)
|
|
return restored
|
|
|
|
|
|
func _get_local_decor_rejection_transaction(
|
|
canonical_revision: int,
|
|
) -> Dictionary:
|
|
var revisions: Array[int] = []
|
|
revisions.assign(_pending_local_decor_transactions.keys())
|
|
revisions.sort()
|
|
for revision: int in revisions:
|
|
if canonical_revision < 0 or revision > canonical_revision:
|
|
return _pending_local_decor_transactions[revision].duplicate(true)
|
|
return {}
|
|
|
|
|
|
func _discard_local_decor_transactions_through(revision: int) -> void:
|
|
for pending_revision: int in _pending_local_decor_transactions.keys():
|
|
if pending_revision <= revision:
|
|
_pending_local_decor_transactions.erase(pending_revision)
|
|
|
|
|
|
func _discard_local_decor_transactions_after(revision: int) -> void:
|
|
for pending_revision: int in _pending_local_decor_transactions.keys():
|
|
if pending_revision > revision:
|
|
_pending_local_decor_transactions.erase(pending_revision)
|
|
|
|
|
|
func _trim_pending_local_decor_transactions() -> void:
|
|
var revisions: Array[int] = []
|
|
revisions.assign(_pending_local_decor_transactions.keys())
|
|
revisions.sort()
|
|
while revisions.size() > MAX_PENDING_LOCAL_DECOR_TRANSACTIONS:
|
|
_pending_local_decor_transactions.erase(revisions.pop_front())
|
|
|
|
|
|
func _submit_transition(action: int, owner_fingerprint: String) -> bool:
|
|
var request: Dictionary = {
|
|
"request_id": _new_id("home_transition"),
|
|
"session_id": _session.get_session_id(),
|
|
"action": action,
|
|
"owner_fingerprint": owner_fingerprint,
|
|
}
|
|
request["sender_signature"] = _session.sign_local_action(
|
|
"portable_home_transition",
|
|
NetworkHomeProtocolType.transition_signature_fields(request),
|
|
)
|
|
if _session.is_host():
|
|
_process_transition_request(_session.get_local_peer_id(), request)
|
|
else:
|
|
submit_home_transition.rpc_id(1, request)
|
|
return true
|
|
|
|
|
|
@rpc(
|
|
"any_peer", "call_remote", "reliable",
|
|
NetworkHomeProtocolType.RELIABLE_CHANNEL,
|
|
)
|
|
func submit_home_transition(data: Dictionary) -> void:
|
|
var sender_id: int = multiplayer.get_remote_sender_id()
|
|
if (
|
|
not _session.is_host()
|
|
or not _session.is_authenticated_peer(sender_id)
|
|
):
|
|
return
|
|
_process_transition_request(sender_id, data)
|
|
|
|
|
|
func _process_transition_request(peer_id: int, data: Dictionary) -> void:
|
|
if (
|
|
not NetworkHomeProtocolType.validate_transition_request(data)
|
|
or str(data["session_id"]) != _session.get_session_id()
|
|
or not _session.verify_peer_action(
|
|
peer_id,
|
|
"portable_home_transition",
|
|
NetworkHomeProtocolType.transition_signature_fields(data),
|
|
data["sender_signature"],
|
|
)
|
|
):
|
|
_send_transition_rejection(peer_id, "The RV transition was invalid.")
|
|
return
|
|
var owner_fingerprint: String = str(data["owner_fingerprint"])
|
|
var action: int = int(data["action"])
|
|
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
|
var manifest: Dictionary = _manifests_by_fingerprint.get(
|
|
owner_fingerprint, {}
|
|
)
|
|
if avatar == null or manifest.is_empty():
|
|
_send_transition_rejection(peer_id, "That RV is unavailable.")
|
|
return
|
|
if action == NetworkHomeProtocolType.TransitionAction.ENTER:
|
|
var record := _session.get_peer_record(peer_id)
|
|
var owner_is_local: bool = (
|
|
record != null and record.identity_fingerprint == owner_fingerprint
|
|
)
|
|
var privacy: int = int((manifest["home"] as Dictionary)["privacy"])
|
|
if (
|
|
(not owner_is_local and privacy != PlayerHomeStateType.Privacy.OPEN)
|
|
or _session.get_peer_space(peer_id)
|
|
!= NetworkHomeProtocolType.WORLD_SPACE
|
|
or not _world_service.is_avatar_near_exterior(
|
|
avatar, owner_fingerprint
|
|
)
|
|
):
|
|
_send_transition_rejection(peer_id, "You cannot enter that RV.")
|
|
return
|
|
var interior_space: StringName = (
|
|
NetworkHomeProtocolType.make_space_id(owner_fingerprint)
|
|
)
|
|
var visitor_slot: int = _reserve_interior_slot(
|
|
interior_space, peer_id
|
|
)
|
|
_apply_host_transition(
|
|
peer_id,
|
|
interior_space,
|
|
owner_fingerprint,
|
|
_world_service.get_interior_spawn_transform(
|
|
owner_fingerprint, visitor_slot
|
|
),
|
|
)
|
|
return
|
|
if (
|
|
_session.get_peer_space(peer_id)
|
|
!= NetworkHomeProtocolType.make_space_id(owner_fingerprint)
|
|
or not _world_service.is_avatar_near_interior_exit(
|
|
avatar, owner_fingerprint
|
|
)
|
|
):
|
|
_send_transition_rejection(peer_id, "Move closer to the RV door.")
|
|
return
|
|
_apply_host_transition(
|
|
peer_id,
|
|
NetworkHomeProtocolType.WORLD_SPACE,
|
|
owner_fingerprint,
|
|
_world_service.get_exterior_spawn_transform(owner_fingerprint),
|
|
)
|
|
|
|
|
|
func _spawn_peer_at_exterior(
|
|
peer_id: int,
|
|
owner_fingerprint: String,
|
|
) -> void:
|
|
if (
|
|
not _session.is_host()
|
|
or _session.get_peer_space(peer_id)
|
|
!= NetworkHomeProtocolType.WORLD_SPACE
|
|
or _spawn_service.get_avatar(peer_id) == null
|
|
):
|
|
return
|
|
_apply_host_transition(
|
|
peer_id,
|
|
NetworkHomeProtocolType.WORLD_SPACE,
|
|
owner_fingerprint,
|
|
_world_service.get_exterior_spawn_transform(owner_fingerprint),
|
|
)
|
|
|
|
|
|
func _apply_host_transition(
|
|
peer_id: int,
|
|
space_id: StringName,
|
|
owner_fingerprint: String,
|
|
transform: Transform3D,
|
|
) -> void:
|
|
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
|
var movement_sequence: int = 0
|
|
if avatar != null:
|
|
movement_sequence = int(
|
|
avatar.make_network_snapshot(peer_id).get(
|
|
"acknowledged_input", 0
|
|
)
|
|
)
|
|
var transform_data: Array[float] = (
|
|
NetworkHomeProtocolType.transform_to_array(transform)
|
|
)
|
|
if not NetworkHomeProtocolType.validate_transition_transform(
|
|
transform_data
|
|
):
|
|
push_error("Refused an out-of-bounds RV transition transform.")
|
|
return
|
|
var previous_space: StringName = _session.get_peer_space(peer_id)
|
|
if (
|
|
String(previous_space).begins_with("rv:")
|
|
and previous_space != space_id
|
|
):
|
|
_release_interior_slot(previous_space, peer_id)
|
|
var result: Dictionary = {
|
|
"peer_id": peer_id,
|
|
"space_id": String(space_id),
|
|
"owner_fingerprint": owner_fingerprint,
|
|
"transform": transform_data,
|
|
"movement_sequence": movement_sequence,
|
|
}
|
|
_apply_transition_result(result)
|
|
for recipient_id: int in _session.get_authenticated_peer_ids():
|
|
if recipient_id == 1:
|
|
continue
|
|
receive_home_transition.rpc_id(recipient_id, result)
|
|
|
|
|
|
@rpc(
|
|
"authority", "call_remote", "reliable",
|
|
NetworkHomeProtocolType.RELIABLE_CHANNEL,
|
|
)
|
|
func receive_home_transition(data: Dictionary) -> void:
|
|
if (
|
|
typeof(data.get("peer_id")) != TYPE_INT
|
|
or typeof(data.get("space_id")) not in [TYPE_STRING, TYPE_STRING_NAME]
|
|
or typeof(data.get("movement_sequence", 0)) != TYPE_INT
|
|
or int(data.get("movement_sequence", 0)) < 0
|
|
or not NetworkIdentityCrypto.valid_fingerprint(
|
|
data.get("owner_fingerprint")
|
|
)
|
|
or not NetworkHomeProtocolType.validate_transition_transform(
|
|
data.get("transform")
|
|
)
|
|
):
|
|
return
|
|
_apply_transition_result(data)
|
|
|
|
|
|
func _apply_transition_result(data: Dictionary) -> void:
|
|
var peer_id: int = int(data["peer_id"])
|
|
var space_id := StringName(str(data["space_id"]))
|
|
if not NetworkHomeProtocolType.validate_transition_transform(
|
|
data.get("transform")
|
|
):
|
|
return
|
|
var target_transform: Transform3D = (
|
|
NetworkHomeProtocolType.array_to_transform(data["transform"])
|
|
)
|
|
if not _session.set_peer_space(peer_id, space_id):
|
|
return
|
|
if not _session.apply_authoritative_space_transition(
|
|
peer_id,
|
|
target_transform,
|
|
int(data.get("movement_sequence", 0)),
|
|
):
|
|
return
|
|
if peer_id == _session.get_local_peer_id():
|
|
_world_service.set_local_space(space_id)
|
|
local_space_changed.emit(space_id, str(data["owner_fingerprint"]))
|
|
local_transition_finished.emit(true, "")
|
|
_refresh_peer_visibility()
|
|
|
|
|
|
func _send_transition_rejection(peer_id: int, message: String) -> void:
|
|
if peer_id == _session.get_local_peer_id():
|
|
local_transition_finished.emit(false, message)
|
|
else:
|
|
receive_home_transition_rejection.rpc_id(peer_id, message)
|
|
|
|
|
|
@rpc(
|
|
"authority", "call_remote", "reliable",
|
|
NetworkHomeProtocolType.RELIABLE_CHANNEL,
|
|
)
|
|
func receive_home_transition_rejection(message: String) -> void:
|
|
local_transition_finished.emit(
|
|
false, message.left(NetworkHomeProtocolType.MAX_MESSAGE_LENGTH)
|
|
)
|
|
|
|
|
|
func _refresh_peer_visibility() -> void:
|
|
var local_peer_id: int = _session.get_local_peer_id()
|
|
if local_peer_id <= 0:
|
|
return
|
|
for peer_id: int in _spawn_service.get_peer_ids():
|
|
if peer_id == local_peer_id:
|
|
continue
|
|
_spawn_service.set_peer_presentation_visible(
|
|
peer_id, _session.peers_share_space(local_peer_id, peer_id)
|
|
)
|
|
|
|
|
|
func _on_local_home_changed() -> void:
|
|
if _suppress_local_submission or _publish_queued:
|
|
return
|
|
_publish_queued = true
|
|
call_deferred("_publish_queued_home")
|
|
|
|
|
|
func _publish_queued_home() -> void:
|
|
if has_pending_local_decor_mutation():
|
|
return
|
|
_publish_queued = false
|
|
if _save_manager != null:
|
|
_save_manager.save_if_dirty()
|
|
publish_local_home()
|
|
|
|
|
|
func _flush_queued_local_home_publish() -> void:
|
|
if not _publish_queued or has_pending_local_decor_mutation():
|
|
return
|
|
call_deferred("_publish_queued_home")
|
|
|
|
|
|
func _on_join_authenticated() -> void:
|
|
publish_local_home()
|
|
|
|
|
|
func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
|
|
if not _session.is_host():
|
|
return
|
|
for manifest: Dictionary in _manifests_by_fingerprint.values():
|
|
receive_home_manifest.rpc_id(peer_id, manifest)
|
|
for existing_peer_id: int in _session.get_authenticated_peer_ids():
|
|
if existing_peer_id == peer_id:
|
|
continue
|
|
var avatar: Player = _spawn_service.get_avatar(existing_peer_id)
|
|
if avatar == null:
|
|
continue
|
|
var space_id: StringName = _session.get_peer_space(existing_peer_id)
|
|
var owner_fingerprint: String = (
|
|
String(space_id).trim_prefix("rv:")
|
|
if String(space_id).begins_with("rv:")
|
|
else _fingerprint_for_peer(existing_peer_id)
|
|
)
|
|
if owner_fingerprint.is_empty():
|
|
continue
|
|
receive_home_transition.rpc_id(peer_id, {
|
|
"peer_id": existing_peer_id,
|
|
"space_id": String(space_id),
|
|
"owner_fingerprint": owner_fingerprint,
|
|
"transform": NetworkHomeProtocolType.transform_to_array(
|
|
avatar.global_transform
|
|
),
|
|
})
|
|
|
|
|
|
func _on_peer_removed(peer_id: int) -> void:
|
|
_release_peer_interior_slots(peer_id)
|
|
var fingerprint: String = _fingerprint_by_peer.get(peer_id, "")
|
|
_fingerprint_by_peer.erase(peer_id)
|
|
_authorized_owned_counts.erase(peer_id)
|
|
if fingerprint.is_empty():
|
|
return
|
|
if _session.is_host():
|
|
for occupant_id: int in _session.get_authenticated_peer_ids():
|
|
if (
|
|
occupant_id == peer_id
|
|
or _session.get_peer_space(occupant_id)
|
|
!= NetworkHomeProtocolType.make_space_id(fingerprint)
|
|
):
|
|
continue
|
|
_apply_host_transition(
|
|
occupant_id,
|
|
NetworkHomeProtocolType.WORLD_SPACE,
|
|
fingerprint,
|
|
_world_service.get_exterior_spawn_transform(fingerprint),
|
|
)
|
|
_manifests_by_fingerprint.erase(fingerprint)
|
|
_world_service.remove_presentation(fingerprint)
|
|
if _session.is_host():
|
|
for recipient_id: int in _session.get_authenticated_peer_ids():
|
|
if recipient_id != 1:
|
|
receive_home_removed.rpc_id(recipient_id, fingerprint)
|
|
|
|
|
|
@rpc(
|
|
"authority", "call_remote", "reliable",
|
|
NetworkHomeProtocolType.RELIABLE_CHANNEL,
|
|
)
|
|
func receive_home_removed(owner_fingerprint: String) -> void:
|
|
if not NetworkIdentityCrypto.valid_fingerprint(owner_fingerprint):
|
|
return
|
|
_manifests_by_fingerprint.erase(owner_fingerprint)
|
|
_world_service.remove_presentation(owner_fingerprint)
|
|
|
|
|
|
func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|
if state in [NetworkSession.State.PRIVATE_HOST, NetworkSession.State.OPEN_HOST]:
|
|
if (
|
|
not _session.is_dedicated_host()
|
|
and _world_service != null
|
|
and _world_service.is_world_ready()
|
|
):
|
|
publish_local_home()
|
|
return
|
|
if state in [
|
|
NetworkSession.State.INACTIVE,
|
|
NetworkSession.State.DISCONNECTING,
|
|
NetworkSession.State.CONNECTION_FAILED,
|
|
NetworkSession.State.SERVER_LOST,
|
|
]:
|
|
_manifests_by_fingerprint.clear()
|
|
_fingerprint_by_peer.clear()
|
|
_authorized_owned_counts.clear()
|
|
_authorization_result_owner.clear()
|
|
_publish_queued = false
|
|
_last_submitted_local_home_revision = -1
|
|
_pending_local_decor_transactions.clear()
|
|
_interior_slots_by_space.clear()
|
|
_next_interior_index = 0
|
|
_world_service.clear_presentations()
|
|
_world_service.set_local_space(NetworkHomeProtocolType.WORLD_SPACE)
|
|
local_space_changed.emit(NetworkHomeProtocolType.WORLD_SPACE, "")
|
|
|
|
|
|
func _fingerprint_for_peer(peer_id: int) -> String:
|
|
if _fingerprint_by_peer.has(peer_id):
|
|
return _fingerprint_by_peer[peer_id]
|
|
var record := _session.get_peer_record(peer_id)
|
|
return record.identity_fingerprint if record != null else ""
|
|
|
|
|
|
func _reserve_interior_slot(space_id: StringName, peer_id: int) -> int:
|
|
var slots: Dictionary = _interior_slots_by_space.get(space_id, {})
|
|
if slots.has(peer_id):
|
|
return int(slots[peer_id])
|
|
var used_slots: Dictionary[int, bool] = {}
|
|
for value: Variant in slots.values():
|
|
used_slots[int(value)] = true
|
|
var slot: int = 0
|
|
while used_slots.has(slot):
|
|
slot += 1
|
|
slots[peer_id] = slot
|
|
_interior_slots_by_space[space_id] = slots
|
|
return slot
|
|
|
|
|
|
func _release_interior_slot(space_id: StringName, peer_id: int) -> void:
|
|
var slots: Dictionary = _interior_slots_by_space.get(space_id, {})
|
|
if slots.is_empty():
|
|
return
|
|
slots.erase(peer_id)
|
|
if slots.is_empty():
|
|
_interior_slots_by_space.erase(space_id)
|
|
else:
|
|
_interior_slots_by_space[space_id] = slots
|
|
|
|
|
|
func _release_peer_interior_slots(peer_id: int) -> void:
|
|
for space_id: StringName in _interior_slots_by_space.keys():
|
|
_release_interior_slot(space_id, peer_id)
|
|
|
|
|
|
func _new_id(prefix: String) -> String:
|
|
return "%s:%s" % [
|
|
prefix, Crypto.new().generate_random_bytes(16).hex_encode(),
|
|
]
|