Add portable RV homes and decor prototype
This commit is contained in:
parent
81decf2b71
commit
fe099f47dd
56 changed files with 5459 additions and 112 deletions
176
network/network_home_protocol.gd
Normal file
176
network/network_home_protocol.gd
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
class_name NetworkHomeProtocol
|
||||
extends RefCounted
|
||||
|
||||
const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
|
||||
|
||||
const CAPABILITY: StringName = &"portable_home_v1"
|
||||
const RELIABLE_CHANNEL: int = NetworkProtocol.HOME_RELIABLE_CHANNEL
|
||||
const MAX_ID_LENGTH: int = 96
|
||||
const MAX_MESSAGE_LENGTH: int = 160
|
||||
const WORLD_SPACE: StringName = &"world"
|
||||
|
||||
enum TransitionAction {
|
||||
ENTER,
|
||||
EXIT,
|
||||
}
|
||||
|
||||
|
||||
static func make_space_id(owner_fingerprint: String) -> StringName:
|
||||
return StringName("rv:%s" % owner_fingerprint)
|
||||
|
||||
|
||||
static func manifest_digest(home_data: Dictionary) -> String:
|
||||
var sanitized: Dictionary = PlayerHomeStateType.sanitize_save_data(home_data)
|
||||
if sanitized.is_empty():
|
||||
return ""
|
||||
var canonical_owned: Array[Dictionary] = []
|
||||
var owned: Dictionary = sanitized["owned_decor"]
|
||||
var owned_ids: Array[String] = []
|
||||
for value: Variant in owned.keys():
|
||||
owned_ids.append(str(value))
|
||||
owned_ids.sort()
|
||||
for decor_id: String in owned_ids:
|
||||
canonical_owned.append({
|
||||
"id": decor_id,
|
||||
"quantity": int(owned[decor_id]),
|
||||
})
|
||||
var placements: Array[Dictionary] = []
|
||||
for value: Variant in sanitized["placements"]:
|
||||
placements.append((value as Dictionary).duplicate(true))
|
||||
placements.sort_custom(func(first: Dictionary, second: Dictionary) -> bool:
|
||||
return str(first["instance_id"]) < str(second["instance_id"])
|
||||
)
|
||||
var canonical: Dictionary = {
|
||||
"schema_version": int(sanitized["schema_version"]),
|
||||
"tier": int(sanitized["tier"]),
|
||||
"exterior_id": str(sanitized["exterior_id"]),
|
||||
"privacy": int(sanitized["privacy"]),
|
||||
"owned": canonical_owned,
|
||||
"placements": placements,
|
||||
"revision": int(sanitized["revision"]),
|
||||
"next_instance_sequence": int(sanitized["next_instance_sequence"]),
|
||||
}
|
||||
return JSON.stringify(canonical).sha256_text()
|
||||
|
||||
|
||||
static func manifest_signature_fields(data: Dictionary) -> Array:
|
||||
return [
|
||||
str(data.get("session_id", "")),
|
||||
str(data.get("request_id", "")),
|
||||
str(data.get("owner_fingerprint", "")),
|
||||
int((data.get("home", {}) as Dictionary).get("revision", -1)),
|
||||
str(data.get("digest", "")),
|
||||
]
|
||||
|
||||
|
||||
static func validate_manifest_request(value: Variant) -> bool:
|
||||
if typeof(value) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var data: Dictionary = value
|
||||
if (
|
||||
not _valid_id(data.get("request_id"))
|
||||
or not _valid_id(data.get("session_id"))
|
||||
or not NetworkIdentityCrypto.valid_fingerprint(
|
||||
data.get("owner_fingerprint")
|
||||
)
|
||||
or typeof(data.get("home")) != TYPE_DICTIONARY
|
||||
or typeof(data.get("digest")) != TYPE_STRING
|
||||
or str(data["digest"]).length() != 64
|
||||
or typeof(data.get("sender_signature")) != TYPE_PACKED_BYTE_ARRAY
|
||||
):
|
||||
return false
|
||||
var sanitized: Dictionary = PlayerHomeStateType.sanitize_save_data(
|
||||
data["home"]
|
||||
)
|
||||
return (
|
||||
not sanitized.is_empty()
|
||||
and manifest_digest(sanitized) == str(data["digest"])
|
||||
)
|
||||
|
||||
|
||||
static func validate_manifest_broadcast(value: Variant) -> bool:
|
||||
if typeof(value) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var data: Dictionary = value
|
||||
return (
|
||||
typeof(data.get("owner_peer_id")) == TYPE_INT
|
||||
and int(data["owner_peer_id"]) > 0
|
||||
and NetworkIdentityCrypto.valid_fingerprint(
|
||||
data.get("owner_fingerprint")
|
||||
)
|
||||
and typeof(data.get("home")) == TYPE_DICTIONARY
|
||||
and not PlayerHomeStateType.sanitize_save_data(data["home"]).is_empty()
|
||||
and _valid_transform(data.get("exterior_transform"))
|
||||
and typeof(data.get("interior_index")) == TYPE_INT
|
||||
and int(data["interior_index"]) >= 0
|
||||
and int(data["interior_index"]) < 128
|
||||
)
|
||||
|
||||
|
||||
static func transition_signature_fields(data: Dictionary) -> Array:
|
||||
return [
|
||||
str(data.get("session_id", "")),
|
||||
str(data.get("request_id", "")),
|
||||
int(data.get("action", -1)),
|
||||
str(data.get("owner_fingerprint", "")),
|
||||
]
|
||||
|
||||
|
||||
static func validate_transition_request(value: Variant) -> bool:
|
||||
if typeof(value) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var data: Dictionary = value
|
||||
return (
|
||||
_valid_id(data.get("request_id"))
|
||||
and _valid_id(data.get("session_id"))
|
||||
and typeof(data.get("action")) == TYPE_INT
|
||||
and int(data["action"]) >= TransitionAction.ENTER
|
||||
and int(data["action"]) <= TransitionAction.EXIT
|
||||
and NetworkIdentityCrypto.valid_fingerprint(
|
||||
data.get("owner_fingerprint")
|
||||
)
|
||||
and typeof(data.get("sender_signature")) == TYPE_PACKED_BYTE_ARRAY
|
||||
)
|
||||
|
||||
|
||||
static func transform_to_array(value: Transform3D) -> Array[float]:
|
||||
return [
|
||||
value.basis.x.x, value.basis.x.y, value.basis.x.z,
|
||||
value.basis.y.x, value.basis.y.y, value.basis.y.z,
|
||||
value.basis.z.x, value.basis.z.y, value.basis.z.z,
|
||||
value.origin.x, value.origin.y, value.origin.z,
|
||||
]
|
||||
|
||||
|
||||
static func array_to_transform(value: Variant) -> Transform3D:
|
||||
if not _valid_transform(value):
|
||||
return Transform3D()
|
||||
var fields: Array = value
|
||||
return Transform3D(
|
||||
Basis(
|
||||
Vector3(float(fields[0]), float(fields[1]), float(fields[2])),
|
||||
Vector3(float(fields[3]), float(fields[4]), float(fields[5])),
|
||||
Vector3(float(fields[6]), float(fields[7]), float(fields[8])),
|
||||
),
|
||||
Vector3(float(fields[9]), float(fields[10]), float(fields[11])),
|
||||
)
|
||||
|
||||
|
||||
static func _valid_transform(value: Variant) -> bool:
|
||||
if typeof(value) != TYPE_ARRAY or (value as Array).size() != 12:
|
||||
return false
|
||||
for component: Variant in value:
|
||||
if (
|
||||
typeof(component) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or not is_finite(float(component))
|
||||
):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _valid_id(value: Variant) -> bool:
|
||||
return (
|
||||
typeof(value) == TYPE_STRING
|
||||
and not str(value).is_empty()
|
||||
and str(value).length() <= MAX_ID_LENGTH
|
||||
)
|
||||
1
network/network_home_protocol.gd.uid
Normal file
1
network/network_home_protocol.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://fl3qwiluejhm
|
||||
956
network/network_home_service.gd
Normal file
956
network/network_home_service.gd
Normal file
|
|
@ -0,0 +1,956 @@
|
|||
class_name NetworkHomeService
|
||||
extends Node
|
||||
|
||||
const NetworkHomeProtocolType = preload(
|
||||
"res://network/network_home_protocol.gd"
|
||||
)
|
||||
const PlayerHomeStateType = preload("res://homes/player_home_state.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
|
||||
|
||||
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 _next_interior_index: int = 0
|
||||
var _suppress_local_submission: bool = false
|
||||
var _publish_queued: bool = false
|
||||
|
||||
|
||||
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 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 _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 instance_id: String = _local_home.place_decor(
|
||||
decor_id,
|
||||
normalized["position"],
|
||||
float(normalized["yaw"]),
|
||||
mode,
|
||||
)
|
||||
if instance_id.is_empty():
|
||||
return ""
|
||||
if not _bag.remove_item(decor_id, 1):
|
||||
_local_home.remove_placement(instance_id)
|
||||
return ""
|
||||
_save_manager.save_if_dirty()
|
||||
return instance_id
|
||||
|
||||
|
||||
func remove_local_decor(instance_id: String) -> bool:
|
||||
if (
|
||||
not can_local_decorate()
|
||||
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 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:
|
||||
return false
|
||||
if target_hotbar_slot >= 0:
|
||||
_hotbar.assign_item(target_hotbar_slot, decor_id)
|
||||
_hotbar.select_slot(target_hotbar_slot)
|
||||
if not _local_home.remove_placement(instance_id):
|
||||
_bag.remove_item(decor_id, 1)
|
||||
return false
|
||||
_save_manager.save_if_dirty()
|
||||
return true
|
||||
|
||||
|
||||
func update_local_decor(
|
||||
instance_id: String,
|
||||
position: Vector3,
|
||||
yaw_radians: float,
|
||||
mode: PlayerHomeStateType.PlacementMode,
|
||||
) -> bool:
|
||||
var normalized: Dictionary = get_valid_local_decor_transform(
|
||||
instance_id, position, yaw_radians, mode
|
||||
)
|
||||
if normalized.is_empty():
|
||||
return false
|
||||
if not _local_home.update_placement(
|
||||
instance_id,
|
||||
normalized["position"],
|
||||
float(normalized["yaw"]),
|
||||
mode,
|
||||
):
|
||||
return false
|
||||
_save_manager.save_if_dirty()
|
||||
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() -> bool:
|
||||
if (
|
||||
_session == null
|
||||
or not _session.is_gameplay_session_active()
|
||||
or _session.is_dedicated_host()
|
||||
or _local_home == null
|
||||
):
|
||||
return false
|
||||
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 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),
|
||||
)
|
||||
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)
|
||||
_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 _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:
|
||||
if (
|
||||
canonical.is_empty()
|
||||
or not NetworkHomeProtocolType.validate_manifest_broadcast(canonical)
|
||||
):
|
||||
local_manifest_rejected.emit(message)
|
||||
return
|
||||
_suppress_local_submission = true
|
||||
_local_home.restore_from_save_data(canonical["home"])
|
||||
_suppress_local_submission = false
|
||||
_save_manager.save_if_dirty()
|
||||
receive_home_manifest(canonical)
|
||||
local_manifest_rejected.emit(message)
|
||||
|
||||
|
||||
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
|
||||
_apply_host_transition(
|
||||
peer_id,
|
||||
NetworkHomeProtocolType.make_space_id(owner_fingerprint),
|
||||
owner_fingerprint,
|
||||
_world_service.get_interior_spawn_transform(
|
||||
owner_fingerprint, peer_id
|
||||
),
|
||||
)
|
||||
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 result: Dictionary = {
|
||||
"peer_id": peer_id,
|
||||
"space_id": String(space_id),
|
||||
"owner_fingerprint": owner_fingerprint,
|
||||
"transform": NetworkHomeProtocolType.transform_to_array(transform),
|
||||
}
|
||||
_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)
|
||||
_session.publish_authoritative_teleport(peer_id)
|
||||
|
||||
|
||||
@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 not NetworkIdentityCrypto.valid_fingerprint(
|
||||
data.get("owner_fingerprint")
|
||||
)
|
||||
):
|
||||
return
|
||||
var transform: Transform3D = NetworkHomeProtocolType.array_to_transform(
|
||||
data.get("transform")
|
||||
)
|
||||
if transform == Transform3D():
|
||||
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 _session.set_peer_space(peer_id, space_id):
|
||||
return
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
if avatar != null:
|
||||
avatar.global_transform = NetworkHomeProtocolType.array_to_transform(
|
||||
data["transform"]
|
||||
)
|
||||
avatar.velocity = Vector3.ZERO
|
||||
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:
|
||||
_publish_queued = false
|
||||
if _save_manager != null:
|
||||
_save_manager.save_if_dirty()
|
||||
publish_local_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:
|
||||
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()
|
||||
_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 _new_id(prefix: String) -> String:
|
||||
return "%s:%s" % [
|
||||
prefix, Crypto.new().generate_random_bytes(16).hex_encode(),
|
||||
]
|
||||
1
network/network_home_service.gd.uid
Normal file
1
network/network_home_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ong8kug36ku7
|
||||
|
|
@ -5,6 +5,7 @@ const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
|
|||
|
||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
||||
const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
|
||||
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
||||
|
||||
const MAX_LEDGER_ENTRIES: int = 64
|
||||
|
||||
|
|
@ -394,6 +395,10 @@ func _apply_equipped(data: Dictionary) -> void:
|
|||
item_id == FishingShopStockType.STANDARD_SHOVEL_ID
|
||||
and bool(data["owns_item"]),
|
||||
)
|
||||
avatar.set_active_decor_package(
|
||||
DecorCatalogType.has_product(item_id)
|
||||
and bool(data["owns_item"]),
|
||||
)
|
||||
equipped_state_changed.emit(
|
||||
peer_id, StringName(str(data["item_id"])), int(data["category"])
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ extends RefCounted
|
|||
|
||||
const WorldLayoutType = preload("res://world/world_layout.gd")
|
||||
|
||||
const PROTOCOL_VERSION: int = 11
|
||||
const PROTOCOL_VERSION: int = 12
|
||||
const GAME_BUILD: String = "prealpha"
|
||||
const MAX_GAME_VERSION_LENGTH: int = 64
|
||||
const MAX_DISPLAY_NAME_LENGTH: int = 24
|
||||
|
|
@ -17,9 +17,9 @@ const MAX_SIGNATURE_LENGTH: int = 2048
|
|||
# 8 reliable ordered session chat, 9 reliable private session mail,
|
||||
# 10 observer-only fishing summaries, 11 reliable movement animation state,
|
||||
# 12 reliable fish showcase state, 13 reliable world-spawn events,
|
||||
# 14 reliable artwork state, 15 world-spawn snapshots, and 16 reliable
|
||||
# fishing lifecycle. Bulk world/art payloads must not head-of-line block held
|
||||
# items, fishing, or animation presentation.
|
||||
# 14 reliable artwork state, 15 world-spawn snapshots, 16 reliable fishing
|
||||
# lifecycle, and 17 reliable portable-home state. Bulk world/art payloads must
|
||||
# not head-of-line block held items, fishing, animation, or home transitions.
|
||||
const SALE_RELIABLE_CHANNEL: int = 5
|
||||
const SHOP_RELIABLE_CHANNEL: int = 6
|
||||
const ITEM_RELIABLE_CHANNEL: int = 7
|
||||
|
|
@ -30,7 +30,8 @@ const SHOWCASE_RELIABLE_CHANNEL: int = 12
|
|||
const WORLD_SPAWN_RELIABLE_CHANNEL: int = 13
|
||||
const DRAWING_RELIABLE_CHANNEL: int = 14
|
||||
const FISHING_RELIABLE_CHANNEL: int = 16
|
||||
const ENET_CHANNEL_COUNT: int = 17
|
||||
const HOME_RELIABLE_CHANNEL: int = 17
|
||||
const ENET_CHANNEL_COUNT: int = 18
|
||||
const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2"
|
||||
const ART_SHOP_CAPABILITY: String = "art_shop_v1"
|
||||
const BACKPACK_SHOP_CAPABILITY: String = "backpack_shop_v1"
|
||||
|
|
@ -45,6 +46,7 @@ const WORLD_LAYOUT_CAPABILITY: String = "world_layout_v1"
|
|||
const FRIENDS_CAPABILITY: String = "friends_v1"
|
||||
const MOVEMENT_RECONCILIATION_CAPABILITY: String = "movement_reconciliation_v2"
|
||||
const FISHING_REPLICATION_CAPABILITY: String = "fishing_replication_v2"
|
||||
const HOME_CAPABILITY: String = "portable_home_v1"
|
||||
const DEFAULT_WORLD_SEED: int = 13001
|
||||
const MAX_WORLD_SEED: int = 2147483646
|
||||
|
||||
|
|
@ -202,6 +204,7 @@ static func make_client_hello(
|
|||
FRIENDS_CAPABILITY,
|
||||
MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
FISHING_REPLICATION_CAPABILITY,
|
||||
HOME_CAPABILITY,
|
||||
]),
|
||||
"cosmetic_snapshot": cosmetic_snapshot,
|
||||
"identity_fingerprint": identity_fingerprint,
|
||||
|
|
@ -342,6 +345,7 @@ static func make_server_hello(
|
|||
FRIENDS_CAPABILITY,
|
||||
MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
FISHING_REPLICATION_CAPABILITY,
|
||||
HOME_CAPABILITY,
|
||||
"chat_v1",
|
||||
"mail_v1",
|
||||
"profile_v1",
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ var _last_server_protocol_version: int = 0
|
|||
var _last_server_world_seed: int = NetworkProtocol.DEFAULT_WORLD_SEED
|
||||
var _last_server_world_layout: StringName = WorldLayout.GENERATED
|
||||
var _server_capabilities: PackedStringArray = PackedStringArray()
|
||||
var _peer_space_ids: Dictionary[int, StringName] = {}
|
||||
var _profile_ready: bool = false
|
||||
var _player_identity: PlayerIdentityStore
|
||||
var _host_identity: HostIdentityStore
|
||||
|
|
@ -334,6 +335,7 @@ func _register_player_host() -> void:
|
|||
NetworkProtocol.FRIENDS_CAPABILITY,
|
||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||
NetworkProtocol.HOME_CAPABILITY,
|
||||
]),
|
||||
)
|
||||
_registry.update_appearance(1, _local_appearance_snapshot)
|
||||
|
|
@ -661,6 +663,7 @@ func supports_server_capability(capability: StringName) -> bool:
|
|||
NetworkProtocol.FRIENDS_CAPABILITY,
|
||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||
NetworkProtocol.HOME_CAPABILITY,
|
||||
"chat_v1",
|
||||
"mail_v1",
|
||||
"profile_v1",
|
||||
|
|
@ -685,6 +688,21 @@ func get_authenticated_peer_ids() -> Array[int]:
|
|||
return _registry.get_peer_ids()
|
||||
|
||||
|
||||
func set_peer_space(peer_id: int, space_id: StringName) -> bool:
|
||||
if not _registry.has_peer(peer_id) or space_id.is_empty():
|
||||
return false
|
||||
_peer_space_ids[peer_id] = space_id
|
||||
return true
|
||||
|
||||
|
||||
func get_peer_space(peer_id: int) -> StringName:
|
||||
return _peer_space_ids.get(peer_id, &"world")
|
||||
|
||||
|
||||
func peers_share_space(first_peer_id: int, second_peer_id: int) -> bool:
|
||||
return get_peer_space(first_peer_id) == get_peer_space(second_peer_id)
|
||||
|
||||
|
||||
func get_peer_rtt_ms(peer_id: int) -> int:
|
||||
if peer_id == 1:
|
||||
return 0
|
||||
|
|
@ -1030,6 +1048,7 @@ func _on_peer_disconnected(peer_id: int) -> void:
|
|||
_pending_identity_challenges.erase(peer_id)
|
||||
_authenticated_identity_cache.erase(peer_id)
|
||||
_operator_peer_ids.erase(peer_id)
|
||||
_peer_space_ids.erase(peer_id)
|
||||
_last_animation_state_by_peer.erase(peer_id)
|
||||
_pending_animation_state_by_peer.erase(peer_id)
|
||||
var recovery_attempt: String = _recovery_attempts.get(peer_id, "")
|
||||
|
|
@ -1313,6 +1332,7 @@ func submit_client_hello(data: Dictionary) -> void:
|
|||
for required_capability: String in [
|
||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||
NetworkProtocol.HOME_CAPABILITY,
|
||||
]:
|
||||
if required_capability not in client_capabilities:
|
||||
_reject_peer(
|
||||
|
|
@ -1546,6 +1566,7 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
for required_capability: String in [
|
||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||
NetworkProtocol.HOME_CAPABILITY,
|
||||
]:
|
||||
if required_capability not in _server_capabilities:
|
||||
_teardown_peer()
|
||||
|
|
@ -1578,6 +1599,7 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
NetworkProtocol.FRIENDS_CAPABILITY,
|
||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||
NetworkProtocol.HOME_CAPABILITY,
|
||||
]),
|
||||
)
|
||||
_registry.update_appearance(local_peer_id, _local_appearance_snapshot)
|
||||
|
|
@ -1623,6 +1645,7 @@ func receive_peer_despawn(peer_id: int) -> void:
|
|||
if state != State.JOINED_CLIENT:
|
||||
return
|
||||
_operator_peer_ids.erase(peer_id)
|
||||
_peer_space_ids.erase(peer_id)
|
||||
_last_animation_state_by_peer.erase(peer_id)
|
||||
_pending_animation_state_by_peer.erase(peer_id)
|
||||
_registry.remove_peer(peer_id)
|
||||
|
|
@ -2247,6 +2270,8 @@ func _broadcast_movement_snapshots() -> void:
|
|||
continue
|
||||
var snapshots: Array = []
|
||||
for subject_id: int in peer_ids:
|
||||
if not peers_share_space(recipient_id, subject_id):
|
||||
continue
|
||||
var subject_avatar: Player = _spawn_service.get_avatar(subject_id)
|
||||
if subject_avatar == null:
|
||||
continue
|
||||
|
|
@ -2315,7 +2340,7 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void:
|
|||
_animation_refresh_accumulator,
|
||||
ANIMATION_REFRESH_INTERVAL,
|
||||
)
|
||||
var updates: Array = []
|
||||
var updates_by_subject: Dictionary[int, Array] = {}
|
||||
for subject_id: int in peer_ids:
|
||||
var avatar: Player = _spawn_service.get_avatar(subject_id)
|
||||
if avatar == null:
|
||||
|
|
@ -2333,12 +2358,18 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void:
|
|||
_last_animation_state_by_peer[subject_id] = state.duplicate(true)
|
||||
var encoded: Array = _encode_movement_animation(subject_id, state)
|
||||
if not encoded.is_empty():
|
||||
updates.append(encoded)
|
||||
if updates.is_empty():
|
||||
updates_by_subject[subject_id] = encoded
|
||||
if updates_by_subject.is_empty():
|
||||
return
|
||||
for recipient_id: int in peer_ids:
|
||||
if recipient_id == 1:
|
||||
continue
|
||||
var updates: Array = []
|
||||
for subject_id: int in updates_by_subject:
|
||||
if peers_share_space(recipient_id, subject_id):
|
||||
updates.append(updates_by_subject[subject_id])
|
||||
if updates.is_empty():
|
||||
continue
|
||||
receive_movement_animations.rpc_id(recipient_id, updates)
|
||||
_movement_animation_packets_sent += 1
|
||||
_movement_animation_states_sent += updates.size()
|
||||
|
|
@ -2888,6 +2919,7 @@ func _teardown_peer() -> void:
|
|||
_pending_animation_state_by_peer.clear()
|
||||
_last_local_animation_action_signature.clear()
|
||||
_server_capabilities = PackedStringArray()
|
||||
_peer_space_ids.clear()
|
||||
_server_identity_fingerprint = ""
|
||||
_server_identity_public_key = ""
|
||||
_session_identity_keys.clear()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ extends RefCounted
|
|||
const CAPABILITY: StringName = &"shop_v1"
|
||||
const ART_CAPABILITY: StringName = &"art_shop_v1"
|
||||
const BACKPACK_CAPABILITY: StringName = &"backpack_shop_v1"
|
||||
const DECOR_CAPABILITY: StringName = &"portable_home_v1"
|
||||
const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL
|
||||
const MAX_ID_LENGTH: int = 96
|
||||
const MAX_MESSAGE_LENGTH: int = 160
|
||||
|
|
@ -18,6 +19,7 @@ enum ProductCategory {
|
|||
ART_KIT,
|
||||
ART_UPGRADE,
|
||||
BACKPACK_CAPACITY_UPGRADE,
|
||||
DECOR,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ const ItemDataType = preload("res://items/item_data.gd")
|
|||
const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
|
||||
const OwnedItemType = preload("res://items/owned_item.gd")
|
||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
||||
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
||||
|
||||
const SHOP_ID: StringName = &"main_fishing_shop"
|
||||
const FISHING_SHOP_ID: StringName = &"main_fishing_shop"
|
||||
const DECOR_SHOP_ID: StringName = &"rv_decor_shop"
|
||||
const REEL_PRODUCT_ID: StringName = &"reel_speed_upgrade"
|
||||
const BARRIER_PRODUCT_ID: StringName = &"barrier_power_upgrade"
|
||||
const COOLER_PRODUCT_ID: StringName = &"cooler_capacity_upgrade"
|
||||
|
|
@ -31,6 +33,7 @@ var _session: NetworkSession
|
|||
var _spawn_service: PlayerSpawnService
|
||||
var _network_fishing: NetworkFishingService
|
||||
var _interaction: FishingShopInteraction
|
||||
var _decor_interaction: DecorShopInteraction
|
||||
var _wallet: PlayerWallet
|
||||
var _bag: PlayerBag
|
||||
var _item_catalog: ItemCatalog
|
||||
|
|
@ -47,6 +50,8 @@ var _received_results: Dictionary[String, bool] = {}
|
|||
var _applied_results: Dictionary[String, bool] = {}
|
||||
var _pending_local_request: Dictionary = {}
|
||||
var _reservations: PlayerAssetReservationService
|
||||
var _home_state: PlayerHomeState
|
||||
var _network_home: NetworkHomeService
|
||||
|
||||
|
||||
func setup(
|
||||
|
|
@ -63,6 +68,9 @@ func setup(
|
|||
save_manager: PlayerSaveManager,
|
||||
reservations: PlayerAssetReservationService,
|
||||
inventory_layout: PlayerInventoryLayout = null,
|
||||
home_state: PlayerHomeState = null,
|
||||
network_home: NetworkHomeService = null,
|
||||
decor_interaction: DecorShopInteraction = null,
|
||||
) -> void:
|
||||
_session = session
|
||||
_spawn_service = spawn_service
|
||||
|
|
@ -77,6 +85,9 @@ func setup(
|
|||
_save_manager = save_manager
|
||||
_reservations = reservations
|
||||
_inventory_layout = inventory_layout
|
||||
_home_state = home_state
|
||||
_network_home = network_home
|
||||
_decor_interaction = decor_interaction
|
||||
if not _session.peer_removed.is_connected(_on_peer_removed):
|
||||
_session.peer_removed.connect(_on_peer_removed)
|
||||
if not _session.state_changed.is_connected(_on_session_state_changed):
|
||||
|
|
@ -87,6 +98,12 @@ func set_shop_interaction(interaction: FishingShopInteraction) -> void:
|
|||
_interaction = interaction
|
||||
|
||||
|
||||
func set_decor_shop_interaction(
|
||||
interaction: DecorShopInteraction,
|
||||
) -> void:
|
||||
_decor_interaction = interaction
|
||||
|
||||
|
||||
func can_request_purchase() -> bool:
|
||||
return (
|
||||
_session != null
|
||||
|
|
@ -124,6 +141,19 @@ func can_request_backpack_purchase() -> bool:
|
|||
)
|
||||
|
||||
|
||||
func can_request_decor_purchase() -> bool:
|
||||
return (
|
||||
can_request_purchase()
|
||||
and _home_state != null
|
||||
and (
|
||||
_session.is_host()
|
||||
or _session.supports_server_capability(
|
||||
NetworkShopProtocol.DECOR_CAPABILITY
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func is_local_purchase_pending() -> bool:
|
||||
return not _pending_local_request.is_empty()
|
||||
|
||||
|
|
@ -193,6 +223,15 @@ func request_backpack_capacity_upgrade() -> String:
|
|||
)
|
||||
|
||||
|
||||
func request_decor(product_id: StringName) -> String:
|
||||
return _request_purchase(
|
||||
product_id,
|
||||
NetworkShopProtocol.ProductCategory.DECOR,
|
||||
1,
|
||||
_home_state.get_owned_count(product_id) if _home_state != null else 0,
|
||||
)
|
||||
|
||||
|
||||
func request_art_kit() -> String:
|
||||
return _request_purchase(
|
||||
ArtShopStockType.ART_KIT_ITEM_ID,
|
||||
|
|
@ -269,11 +308,25 @@ func _request_purchase(
|
|||
product_id, category, 0, 0,
|
||||
)
|
||||
return ""
|
||||
if (
|
||||
category == NetworkShopProtocol.ProductCategory.DECOR
|
||||
and not can_request_decor_purchase()
|
||||
):
|
||||
local_purchase_finished.emit(
|
||||
"", false, "Decor require a newer server.",
|
||||
product_id, category, 0, 0,
|
||||
)
|
||||
return ""
|
||||
var request_id: String = _new_id("shop")
|
||||
var shop_id: StringName = (
|
||||
DECOR_SHOP_ID
|
||||
if category == NetworkShopProtocol.ProductCategory.DECOR
|
||||
else FISHING_SHOP_ID
|
||||
)
|
||||
var request: Dictionary = {
|
||||
"request_id": request_id,
|
||||
"session_id": _session.get_session_id(),
|
||||
"shop_id": str(SHOP_ID),
|
||||
"shop_id": str(shop_id),
|
||||
"product_id": str(product_id),
|
||||
"category": category,
|
||||
"quantity": quantity,
|
||||
|
|
@ -345,16 +398,34 @@ func _handle_purchase_request(peer_id: int, data: Dictionary) -> void:
|
|||
_rejected_result(data, "A purchase is already pending.")
|
||||
)
|
||||
return
|
||||
if not _is_shop_available_for_peer(peer_id):
|
||||
var requested_shop_id := StringName(str(data["shop_id"]))
|
||||
if not _is_shop_available_for_peer(peer_id, requested_shop_id):
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
var requested_interaction: FishingShopInteraction = (
|
||||
_decor_interaction
|
||||
if requested_shop_id == DECOR_SHOP_ID
|
||||
else _interaction
|
||||
)
|
||||
var message: String = (
|
||||
"The shop is unavailable."
|
||||
if avatar == null or _interaction == null
|
||||
if avatar == null or requested_interaction == null
|
||||
else "Move closer to the shop."
|
||||
)
|
||||
_record_and_send(peer_id, _rejected_result(data, message))
|
||||
return
|
||||
var result: Dictionary = _build_authoritative_result(peer_id, data)
|
||||
if (
|
||||
bool(result.get("accepted", false))
|
||||
and int(result.get("category", -1))
|
||||
== NetworkShopProtocol.ProductCategory.DECOR
|
||||
and _network_home != null
|
||||
):
|
||||
_network_home.authorize_decor_purchase(
|
||||
peer_id,
|
||||
StringName(str(result["product_id"])),
|
||||
int(result["resulting_state"]),
|
||||
str(result["result_id"]),
|
||||
)
|
||||
_pending_by_peer[peer_id] = request_id
|
||||
_record_and_send(peer_id, result)
|
||||
|
||||
|
|
@ -368,10 +439,24 @@ func _build_authoritative_result(
|
|||
var quantity: int = request["quantity"]
|
||||
var wallet_balance: int = request["wallet_balance"]
|
||||
var current_state: int = request["current_state"]
|
||||
if (
|
||||
category == NetworkShopProtocol.ProductCategory.DECOR
|
||||
and _network_home != null
|
||||
):
|
||||
var authoritative_count: int = (
|
||||
_network_home.get_authoritative_owned_count(peer_id, product_id)
|
||||
)
|
||||
if authoritative_count >= 0:
|
||||
current_state = authoritative_count
|
||||
var cost: int = -1
|
||||
var resulting_state: int = current_state
|
||||
var rejection: String = ""
|
||||
if StringName(str(request["shop_id"])) != SHOP_ID:
|
||||
var expected_shop_id: StringName = (
|
||||
DECOR_SHOP_ID
|
||||
if category == NetworkShopProtocol.ProductCategory.DECOR
|
||||
else FISHING_SHOP_ID
|
||||
)
|
||||
if StringName(str(request["shop_id"])) != expected_shop_id:
|
||||
rejection = "The shop is unavailable."
|
||||
elif quantity < 1:
|
||||
rejection = "Purchase could not be completed."
|
||||
|
|
@ -545,6 +630,19 @@ func _build_authoritative_result(
|
|||
resulting_state = PlayerArtUnlocks.resulting_mask(
|
||||
current_state, product_id
|
||||
)
|
||||
NetworkShopProtocol.ProductCategory.DECOR:
|
||||
cost = DecorCatalogType.get_price(product_id)
|
||||
if (
|
||||
not DecorCatalogType.has_product(product_id)
|
||||
or quantity != 1
|
||||
or current_state < 0
|
||||
or current_state
|
||||
>= DecorCatalogType.MAX_OWNED_PER_PRODUCT
|
||||
or cost < 0
|
||||
):
|
||||
rejection = "Decor is unavailable."
|
||||
else:
|
||||
resulting_state = current_state + 1
|
||||
if rejection.is_empty() and (cost < 0 or wallet_balance < cost):
|
||||
rejection = "Insufficient funds."
|
||||
if not rejection.is_empty():
|
||||
|
|
@ -667,6 +765,9 @@ func _apply_purchase_result(data: Dictionary) -> void:
|
|||
if _inventory_layout != null else 0
|
||||
)
|
||||
var art_snapshot: int = _art_unlocks.get_unlock_mask()
|
||||
var home_snapshot: Dictionary = (
|
||||
_home_state.to_save_data() if _home_state != null else {}
|
||||
)
|
||||
var applied: bool = _apply_local_product(data)
|
||||
if not applied or not _save_manager.save_if_dirty():
|
||||
_bag.replace_all_items(bag_snapshot)
|
||||
|
|
@ -676,6 +777,8 @@ func _apply_purchase_result(data: Dictionary) -> void:
|
|||
if _inventory_layout != null:
|
||||
_inventory_layout.restore_backpack_level(backpack_snapshot)
|
||||
_art_unlocks.restore_mask(art_snapshot)
|
||||
if _home_state != null and not home_snapshot.is_empty():
|
||||
_home_state.restore_from_save_data(home_snapshot)
|
||||
_wallet.restore_balance(wallet_snapshot)
|
||||
_save_manager.save_if_dirty()
|
||||
_fail_local_apply(data, "Purchase could not be completed.")
|
||||
|
|
@ -696,6 +799,7 @@ func _validate_local_result(data: Dictionary) -> String:
|
|||
or _cooler_capacity == null
|
||||
or _art_unlocks == null
|
||||
or _save_manager == null
|
||||
or _home_state == null
|
||||
or _wallet.get_balance() != int(data["expected_wallet"])
|
||||
or str(data["product_id"])
|
||||
!= str(_pending_local_request.get("product_id", ""))
|
||||
|
|
@ -806,6 +910,20 @@ func _validate_local_result(data: Dictionary) -> String:
|
|||
or cost != ArtShopStockType.UPGRADE_PRICE
|
||||
):
|
||||
return "Purchase could not be completed."
|
||||
NetworkShopProtocol.ProductCategory.DECOR:
|
||||
if (
|
||||
not DecorCatalogType.has_product(product_id)
|
||||
or _home_state.get_owned_count(product_id) != expected_state
|
||||
or resulting_state != expected_state + 1
|
||||
or cost != DecorCatalogType.get_price(product_id)
|
||||
or expected_state >= DecorCatalogType.MAX_OWNED_PER_PRODUCT
|
||||
or not _bag.can_add_item(product_id, 1)
|
||||
):
|
||||
return (
|
||||
"Your inventory is full."
|
||||
if not _bag.can_add_item(product_id, 1)
|
||||
else "Purchase could not be completed."
|
||||
)
|
||||
_:
|
||||
return "Purchase could not be completed."
|
||||
return ""
|
||||
|
|
@ -845,6 +963,12 @@ func _apply_local_product(data: Dictionary) -> bool:
|
|||
return _art_unlocks.purchase_product(
|
||||
product_id, _wallet, int(data["total_cost"])
|
||||
)
|
||||
NetworkShopProtocol.ProductCategory.DECOR:
|
||||
return (
|
||||
_wallet.debit(int(data["total_cost"]))
|
||||
and _bag.add_item(product_id, 1)
|
||||
and _home_state.add_owned_decor(product_id, 1)
|
||||
)
|
||||
return false
|
||||
|
||||
|
||||
|
|
@ -921,7 +1045,7 @@ func _handle_acknowledgement(
|
|||
peer_id: int,
|
||||
result_id: String,
|
||||
request_id: String,
|
||||
_applied: bool,
|
||||
applied: bool,
|
||||
message: String,
|
||||
) -> void:
|
||||
if (
|
||||
|
|
@ -933,20 +1057,32 @@ func _handle_acknowledgement(
|
|||
or _result_owners.get(result_id, 0) != peer_id
|
||||
):
|
||||
return
|
||||
if _network_home != null:
|
||||
_network_home.finalize_decor_purchase(
|
||||
peer_id, result_id, applied
|
||||
)
|
||||
_acknowledged_results[result_id] = true
|
||||
_bound_dictionary(_acknowledged_results)
|
||||
if _pending_by_peer.get(peer_id, "") == request_id:
|
||||
_pending_by_peer.erase(peer_id)
|
||||
|
||||
|
||||
func _is_shop_available_for_peer(peer_id: int) -> bool:
|
||||
func _is_shop_available_for_peer(
|
||||
peer_id: int,
|
||||
shop_id: StringName,
|
||||
) -> bool:
|
||||
var interaction: FishingShopInteraction = (
|
||||
_decor_interaction
|
||||
if shop_id == DECOR_SHOP_ID
|
||||
else _interaction
|
||||
)
|
||||
if (
|
||||
_session == null
|
||||
or not _session.is_host()
|
||||
or not _session.is_gameplay_session_active()
|
||||
or _spawn_service == null
|
||||
or _interaction == null
|
||||
or not is_instance_valid(_interaction)
|
||||
or interaction == null
|
||||
or not is_instance_valid(interaction)
|
||||
):
|
||||
return false
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
|
|
@ -957,7 +1093,7 @@ func _is_shop_available_for_peer(peer_id: int) -> bool:
|
|||
_network_fishing == null
|
||||
or not _network_fishing.has_peer_attempt(peer_id)
|
||||
)
|
||||
and _interaction.is_avatar_in_range(avatar)
|
||||
and interaction.is_avatar_in_range(avatar)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue