Add multiplayer item use and session chat
This commit is contained in:
parent
5e04b70609
commit
5dda74c9e4
27 changed files with 1361 additions and 31 deletions
|
|
@ -107,6 +107,7 @@ var _local_hotbar: PlayerHotbarType
|
||||||
var _item_catalog: ItemCatalogType
|
var _item_catalog: ItemCatalogType
|
||||||
var _fishing_upgrades: PlayerFishingUpgradesType
|
var _fishing_upgrades: PlayerFishingUpgradesType
|
||||||
var _item_effects: PlayerItemEffectsType
|
var _item_effects: PlayerItemEffectsType
|
||||||
|
var _network_item_use: NetworkItemUseService
|
||||||
var _cooler_capacity: PlayerCoolerCapacityType
|
var _cooler_capacity: PlayerCoolerCapacityType
|
||||||
var _network_session: NetworkSessionType
|
var _network_session: NetworkSessionType
|
||||||
var _network_fishing: NetworkFishingServiceType
|
var _network_fishing: NetworkFishingServiceType
|
||||||
|
|
@ -154,6 +155,7 @@ func setup(
|
||||||
item_catalog: ItemCatalogType,
|
item_catalog: ItemCatalogType,
|
||||||
fishing_upgrades: PlayerFishingUpgradesType,
|
fishing_upgrades: PlayerFishingUpgradesType,
|
||||||
item_effects: PlayerItemEffectsType,
|
item_effects: PlayerItemEffectsType,
|
||||||
|
network_item_use: NetworkItemUseService,
|
||||||
cooler_capacity: PlayerCoolerCapacityType,
|
cooler_capacity: PlayerCoolerCapacityType,
|
||||||
network_session: NetworkSessionType = null,
|
network_session: NetworkSessionType = null,
|
||||||
network_fishing: NetworkFishingServiceType = null,
|
network_fishing: NetworkFishingServiceType = null,
|
||||||
|
|
@ -166,6 +168,11 @@ func setup(
|
||||||
_item_catalog = item_catalog
|
_item_catalog = item_catalog
|
||||||
_fishing_upgrades = fishing_upgrades
|
_fishing_upgrades = fishing_upgrades
|
||||||
_item_effects = item_effects
|
_item_effects = item_effects
|
||||||
|
_network_item_use = network_item_use
|
||||||
|
if _network_item_use != null:
|
||||||
|
_network_item_use.local_item_use_finished.connect(
|
||||||
|
_on_network_item_use_finished
|
||||||
|
)
|
||||||
_cooler_capacity = cooler_capacity
|
_cooler_capacity = cooler_capacity
|
||||||
_network_session = network_session
|
_network_session = network_session
|
||||||
_network_fishing = network_fishing
|
_network_fishing = network_fishing
|
||||||
|
|
@ -416,10 +423,12 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
active_item != null
|
active_item != null
|
||||||
and active_item.category == ItemDataType.Category.CONSUMABLE
|
and active_item.category == ItemDataType.Category.CONSUMABLE
|
||||||
):
|
):
|
||||||
if _item_effects.use_consumable(active_item, _local_bag):
|
if _network_item_use != null:
|
||||||
status_changed.emit(
|
_network_item_use.request_use(active_item.item_id)
|
||||||
_item_effects.get_feedback(active_item.item_id)
|
elif _item_effects.use_consumable(active_item, _local_bag):
|
||||||
)
|
status_changed.emit(_item_effects.get_feedback(
|
||||||
|
active_item.item_id
|
||||||
|
))
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
if not has_active_fishing_rod():
|
if not has_active_fishing_rod():
|
||||||
|
|
@ -1112,11 +1121,7 @@ func build_network_context(
|
||||||
func _build_network_evidence() -> Dictionary:
|
func _build_network_evidence() -> Dictionary:
|
||||||
var rarity_multipliers: Array[float] = []
|
var rarity_multipliers: Array[float] = []
|
||||||
for rarity: int in range(FishDataType.Rarity.size()):
|
for rarity: int in range(FishDataType.Rarity.size()):
|
||||||
rarity_multipliers.append(
|
rarity_multipliers.append(1.0)
|
||||||
_item_effects.get_rarity_weight_multiplier(rarity)
|
|
||||||
if _item_effects != null
|
|
||||||
else 1.0
|
|
||||||
)
|
|
||||||
var discovered_ids: Array[String] = []
|
var discovered_ids: Array[String] = []
|
||||||
if _local_collection_log != null:
|
if _local_collection_log != null:
|
||||||
for fish_id: StringName in _local_collection_log.get_discovered_ids():
|
for fish_id: StringName in _local_collection_log.get_discovered_ids():
|
||||||
|
|
@ -1124,19 +1129,30 @@ func _build_network_evidence() -> Dictionary:
|
||||||
var item: ItemDataType = _get_active_item()
|
var item: ItemDataType = _get_active_item()
|
||||||
return {
|
return {
|
||||||
"rod_id": str(item.item_id) if item != null else "",
|
"rod_id": str(item.item_id) if item != null else "",
|
||||||
"reel_speed": _get_effective_reel_speed(),
|
"reel_speed": _active_player.reel_speed * (
|
||||||
"barrier_damage": _get_effective_barrier_damage(),
|
_fishing_upgrades.get_reel_speed_multiplier()
|
||||||
"bite_multiplier": (
|
if _fishing_upgrades != null else 1.0
|
||||||
_item_effects.get_bite_time_multiplier()
|
|
||||||
if _item_effects != null
|
|
||||||
else 1.0
|
|
||||||
),
|
),
|
||||||
|
"barrier_damage": (
|
||||||
|
_fishing_upgrades.get_barrier_damage()
|
||||||
|
if _fishing_upgrades != null else _active_player.click_power
|
||||||
|
),
|
||||||
|
"bite_multiplier": 1.0,
|
||||||
"rarity_multipliers": rarity_multipliers,
|
"rarity_multipliers": rarity_multipliers,
|
||||||
"discovered_fish_ids": discovered_ids,
|
"discovered_fish_ids": discovered_ids,
|
||||||
"capacity_available": not _is_cooler_full(),
|
"capacity_available": not _is_cooler_full(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _on_network_item_use_finished(
|
||||||
|
accepted: bool,
|
||||||
|
message: String,
|
||||||
|
) -> void:
|
||||||
|
status_changed.emit(message)
|
||||||
|
if accepted:
|
||||||
|
refresh_active_item_status()
|
||||||
|
|
||||||
|
|
||||||
func _on_network_cast_accepted(
|
func _on_network_cast_accepted(
|
||||||
_attempt_id: String,
|
_attempt_id: String,
|
||||||
target: Vector3,
|
target: Vector3,
|
||||||
|
|
|
||||||
34
main/main.gd
34
main/main.gd
|
|
@ -52,6 +52,12 @@ const NetworkSaleServiceType = preload(
|
||||||
const NetworkShopServiceType = preload(
|
const NetworkShopServiceType = preload(
|
||||||
"res://network/network_shop_service.gd"
|
"res://network/network_shop_service.gd"
|
||||||
)
|
)
|
||||||
|
const NetworkItemUseServiceType = preload(
|
||||||
|
"res://network/network_item_use_service.gd"
|
||||||
|
)
|
||||||
|
const NetworkChatServiceType = preload(
|
||||||
|
"res://network/network_chat_service.gd"
|
||||||
|
)
|
||||||
|
|
||||||
const TITLE_MUSIC_SILENCE_DB: float = -80.0
|
const TITLE_MUSIC_SILENCE_DB: float = -80.0
|
||||||
|
|
||||||
|
|
@ -91,6 +97,10 @@ const TITLE_MUSIC_SILENCE_DB: float = -80.0
|
||||||
)
|
)
|
||||||
@onready var _network_sale: NetworkSaleServiceType = %NetworkSaleService
|
@onready var _network_sale: NetworkSaleServiceType = %NetworkSaleService
|
||||||
@onready var _network_shop: NetworkShopServiceType = %NetworkShopService
|
@onready var _network_shop: NetworkShopServiceType = %NetworkShopService
|
||||||
|
@onready var _network_item_use: NetworkItemUseServiceType = (
|
||||||
|
%NetworkItemUseService
|
||||||
|
)
|
||||||
|
@onready var _network_chat: NetworkChatServiceType = %NetworkChatService
|
||||||
@onready var _players_root: Node3D = $Players
|
@onready var _players_root: Node3D = $Players
|
||||||
|
|
||||||
var _gameplay_started: bool = false
|
var _gameplay_started: bool = false
|
||||||
|
|
@ -150,6 +160,15 @@ func _ready() -> void:
|
||||||
_player.cooler_capacity
|
_player.cooler_capacity
|
||||||
)
|
)
|
||||||
_save_manager.set_autosave_enabled(false)
|
_save_manager.set_autosave_enabled(false)
|
||||||
|
_network_item_use.setup(
|
||||||
|
_network_session,
|
||||||
|
_player_spawn_service,
|
||||||
|
item_catalog,
|
||||||
|
_player.bag,
|
||||||
|
_player.item_effects,
|
||||||
|
_save_manager
|
||||||
|
)
|
||||||
|
_network_chat.setup(_network_session)
|
||||||
_network_fishing.setup(
|
_network_fishing.setup(
|
||||||
_network_session,
|
_network_session,
|
||||||
_player_spawn_service,
|
_player_spawn_service,
|
||||||
|
|
@ -159,7 +178,8 @@ func _ready() -> void:
|
||||||
_player.cooler_capacity,
|
_player.cooler_capacity,
|
||||||
_save_manager,
|
_save_manager,
|
||||||
item_catalog,
|
item_catalog,
|
||||||
fish_catalog
|
fish_catalog,
|
||||||
|
_network_item_use
|
||||||
)
|
)
|
||||||
_network_sale.setup(
|
_network_sale.setup(
|
||||||
_network_session,
|
_network_session,
|
||||||
|
|
@ -194,6 +214,7 @@ func _ready() -> void:
|
||||||
item_catalog,
|
item_catalog,
|
||||||
_player.fishing_upgrades,
|
_player.fishing_upgrades,
|
||||||
_player.item_effects,
|
_player.item_effects,
|
||||||
|
_network_item_use,
|
||||||
_player.cooler_capacity,
|
_player.cooler_capacity,
|
||||||
_network_session,
|
_network_session,
|
||||||
_network_fishing
|
_network_fishing
|
||||||
|
|
@ -217,7 +238,10 @@ func _ready() -> void:
|
||||||
_player.cooler_capacity,
|
_player.cooler_capacity,
|
||||||
_network_session,
|
_network_session,
|
||||||
_network_sale,
|
_network_sale,
|
||||||
_network_shop
|
_network_shop,
|
||||||
|
_network_chat,
|
||||||
|
_network_profile,
|
||||||
|
_player_spawn_service
|
||||||
)
|
)
|
||||||
_water_recovery.setup(
|
_water_recovery.setup(
|
||||||
_player,
|
_player,
|
||||||
|
|
@ -284,6 +308,7 @@ func _ready() -> void:
|
||||||
_player.hotbar.selected_slot_changed.connect(
|
_player.hotbar.selected_slot_changed.connect(
|
||||||
_on_active_hotbar_item_changed
|
_on_active_hotbar_item_changed
|
||||||
)
|
)
|
||||||
|
_player.bag.contents_changed.connect(_refresh_active_hotbar_item)
|
||||||
_fishing_spot.ready_for_equipment_refresh.connect(
|
_fishing_spot.ready_for_equipment_refresh.connect(
|
||||||
_refresh_active_hotbar_item
|
_refresh_active_hotbar_item
|
||||||
)
|
)
|
||||||
|
|
@ -409,6 +434,8 @@ func _set_gameplay_active(active: bool) -> void:
|
||||||
_water_recovery.set_recovery_enabled(active)
|
_water_recovery.set_recovery_enabled(active)
|
||||||
_game_ui.set_gameplay_ui_enabled(active)
|
_game_ui.set_gameplay_ui_enabled(active)
|
||||||
_save_manager.set_autosave_enabled(active)
|
_save_manager.set_autosave_enabled(active)
|
||||||
|
if active:
|
||||||
|
_refresh_active_hotbar_item()
|
||||||
|
|
||||||
|
|
||||||
func _on_new_game_requested() -> void:
|
func _on_new_game_requested() -> void:
|
||||||
|
|
@ -670,6 +697,9 @@ func _on_active_hotbar_item_changed(
|
||||||
and _player.bag.owns_item(item_id)
|
and _player.bag.owns_item(item_id)
|
||||||
)
|
)
|
||||||
_player.set_active_item_is_rod(active_is_rod)
|
_player.set_active_item_is_rod(active_is_rod)
|
||||||
|
_network_item_use.submit_local_equipped(item_id, active_is_rod or (
|
||||||
|
item != null and _player.bag.owns_item(item_id)
|
||||||
|
))
|
||||||
_fishing_spot.refresh_active_item_status()
|
_fishing_spot.refresh_active_item_status()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=24 format=3]
|
[gd_scene load_steps=26 format=3]
|
||||||
|
|
||||||
[ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"]
|
[ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"]
|
||||||
[ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"]
|
[ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"]
|
||||||
|
|
@ -23,6 +23,8 @@
|
||||||
[ext_resource type="Script" path="res://network/network_fishing_service.gd" id="21_network_fishing"]
|
[ext_resource type="Script" path="res://network/network_fishing_service.gd" id="21_network_fishing"]
|
||||||
[ext_resource type="Script" path="res://network/network_sale_service.gd" id="22_network_sale"]
|
[ext_resource type="Script" path="res://network/network_sale_service.gd" id="22_network_sale"]
|
||||||
[ext_resource type="Script" path="res://network/network_shop_service.gd" id="23_network_shop"]
|
[ext_resource type="Script" path="res://network/network_shop_service.gd" id="23_network_shop"]
|
||||||
|
[ext_resource type="Script" path="res://network/network_item_use_service.gd" id="24_network_item"]
|
||||||
|
[ext_resource type="Script" path="res://network/network_chat_service.gd" id="25_network_chat"]
|
||||||
|
|
||||||
[node name="Main" type="Node3D"]
|
[node name="Main" type="Node3D"]
|
||||||
script = ExtResource("3_main")
|
script = ExtResource("3_main")
|
||||||
|
|
@ -59,6 +61,14 @@ script = ExtResource("22_network_sale")
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("23_network_shop")
|
script = ExtResource("23_network_shop")
|
||||||
|
|
||||||
|
[node name="NetworkItemUseService" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("24_network_item")
|
||||||
|
|
||||||
|
[node name="NetworkChatService" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("25_network_chat")
|
||||||
|
|
||||||
[node name="TestWorld" parent="." instance=ExtResource("1_world")]
|
[node name="TestWorld" parent="." instance=ExtResource("1_world")]
|
||||||
|
|
||||||
[node name="Players" type="Node3D" parent="."]
|
[node name="Players" type="Node3D" parent="."]
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ func start_host(
|
||||||
var enet_peer := ENetMultiplayerPeer.new()
|
var enet_peer := ENetMultiplayerPeer.new()
|
||||||
if not bind_address.is_empty() and bind_address != "*":
|
if not bind_address.is_empty() and bind_address != "*":
|
||||||
enet_peer.set_bind_ip(bind_address)
|
enet_peer.set_bind_ip(bind_address)
|
||||||
var error: Error = enet_peer.create_server(port, max_clients, 5)
|
var error: Error = enet_peer.create_server(
|
||||||
|
port, max_clients, NetworkProtocol.ENET_CHANNEL_COUNT
|
||||||
|
)
|
||||||
if error != OK:
|
if error != OK:
|
||||||
transport_error.emit("Unable to host UDP port %d." % port)
|
transport_error.emit("Unable to host UDP port %d." % port)
|
||||||
return error
|
return error
|
||||||
|
|
@ -27,7 +29,9 @@ func connect_to_route(route: ConnectionRoute) -> Error:
|
||||||
return ERR_INVALID_PARAMETER
|
return ERR_INVALID_PARAMETER
|
||||||
var endpoint: ConnectionEndpoint = route.direct_endpoint
|
var endpoint: ConnectionEndpoint = route.direct_endpoint
|
||||||
var enet_peer := ENetMultiplayerPeer.new()
|
var enet_peer := ENetMultiplayerPeer.new()
|
||||||
var error: Error = enet_peer.create_client(endpoint.host, endpoint.port, 5)
|
var error: Error = enet_peer.create_client(
|
||||||
|
endpoint.host, endpoint.port, NetworkProtocol.ENET_CHANNEL_COUNT
|
||||||
|
)
|
||||||
if error != OK:
|
if error != OK:
|
||||||
transport_error.emit(
|
transport_error.emit(
|
||||||
"Unable to connect to %s." % endpoint.normalized_display
|
"Unable to connect to %s." % endpoint.normalized_display
|
||||||
|
|
|
||||||
61
network/network_chat_protocol.gd
Normal file
61
network/network_chat_protocol.gd
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
class_name NetworkChatProtocol
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const CAPABILITY: StringName = &"chat_v1"
|
||||||
|
const RELIABLE_CHANNEL: int = NetworkProtocol.CHAT_RELIABLE_CHANNEL
|
||||||
|
const MAX_VISIBLE_CHARACTERS: int = 300
|
||||||
|
const MAX_UTF8_BYTES: int = 1200
|
||||||
|
const MAX_HISTORY: int = 100
|
||||||
|
const LATE_JOIN_HISTORY: int = 50
|
||||||
|
const MAX_ID_LENGTH: int = 96
|
||||||
|
|
||||||
|
enum Kind { PLAYER, SYSTEM }
|
||||||
|
|
||||||
|
|
||||||
|
static func sanitize_body(value: Variant) -> String:
|
||||||
|
if typeof(value) != TYPE_STRING:
|
||||||
|
return ""
|
||||||
|
var result: String = str(value).replace("\r", " ").replace("\n", " ")
|
||||||
|
result = result.replace("\t", " ").strip_edges()
|
||||||
|
if result.is_empty() or result.length() > MAX_VISIBLE_CHARACTERS:
|
||||||
|
return ""
|
||||||
|
if result.to_utf8_buffer().size() > MAX_UTF8_BYTES:
|
||||||
|
return ""
|
||||||
|
for index: int in result.length():
|
||||||
|
var codepoint: int = result.unicode_at(index)
|
||||||
|
if codepoint < 32 or codepoint == 127:
|
||||||
|
return ""
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_request(data: Variant) -> bool:
|
||||||
|
return (
|
||||||
|
typeof(data) == TYPE_DICTIONARY
|
||||||
|
and _valid_id(data.get("request_id"))
|
||||||
|
and _valid_id(data.get("session_id"))
|
||||||
|
and not sanitize_body(data.get("body")).is_empty()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_message(data: Variant) -> bool:
|
||||||
|
return (
|
||||||
|
typeof(data) == TYPE_DICTIONARY
|
||||||
|
and _valid_id(data.get("message_id"))
|
||||||
|
and _valid_id(data.get("session_id"))
|
||||||
|
and typeof(data.get("sequence")) == TYPE_INT
|
||||||
|
and int(data["sequence"]) >= 0
|
||||||
|
and typeof(data.get("kind")) == TYPE_INT
|
||||||
|
and int(data["kind"]) in [Kind.PLAYER, Kind.SYSTEM]
|
||||||
|
and typeof(data.get("sender_peer_id")) == TYPE_INT
|
||||||
|
and typeof(data.get("sender_display_name")) == TYPE_STRING
|
||||||
|
and str(data["sender_display_name"]).length() <= 24
|
||||||
|
and not sanitize_body(data.get("body")).is_empty()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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_chat_protocol.gd.uid
Normal file
1
network/network_chat_protocol.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c3hg65v27hi2g
|
||||||
235
network/network_chat_service.gd
Normal file
235
network/network_chat_service.gd
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
class_name NetworkChatService
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
const BURST_COUNT: int = 3
|
||||||
|
const WINDOW_COUNT: int = 5
|
||||||
|
const WINDOW_SECONDS: float = 10.0
|
||||||
|
|
||||||
|
signal message_received(message: Dictionary)
|
||||||
|
signal send_rejected(message: String)
|
||||||
|
signal history_replaced(messages: Array[Dictionary])
|
||||||
|
|
||||||
|
var _session: NetworkSession
|
||||||
|
var _history: Array[Dictionary] = []
|
||||||
|
var _seen_messages: Dictionary[String, bool] = {}
|
||||||
|
var _request_ledgers: Dictionary[int, Dictionary] = {}
|
||||||
|
var _rate_times: Dictionary[int, Array] = {}
|
||||||
|
var _sequence: int = 0
|
||||||
|
var _peer_names: Dictionary[int, String] = {}
|
||||||
|
|
||||||
|
|
||||||
|
func setup(session: NetworkSession) -> void:
|
||||||
|
_session = session
|
||||||
|
_session.peer_authenticated.connect(_on_peer_authenticated)
|
||||||
|
_session.peer_removed.connect(_on_peer_removed)
|
||||||
|
_session.state_changed.connect(_on_session_state_changed)
|
||||||
|
_session.peer_display_name_changed.connect(
|
||||||
|
func(peer_id: int, display_name: String) -> void:
|
||||||
|
_peer_names[peer_id] = display_name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func send_local_message(body: String) -> bool:
|
||||||
|
if (
|
||||||
|
_session == null
|
||||||
|
or not _session.is_gameplay_session_active()
|
||||||
|
or (
|
||||||
|
not _session.is_host()
|
||||||
|
and not _session.supports_server_capability(
|
||||||
|
NetworkChatProtocol.CAPABILITY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
send_rejected.emit("Chat is unavailable.")
|
||||||
|
return false
|
||||||
|
var clean := NetworkChatProtocol.sanitize_body(body)
|
||||||
|
if clean.is_empty():
|
||||||
|
send_rejected.emit("Message is empty or too long.")
|
||||||
|
return false
|
||||||
|
var request := {
|
||||||
|
"request_id": _new_id("chat_request"),
|
||||||
|
"session_id": _session.get_session_id(),
|
||||||
|
"body": clean,
|
||||||
|
}
|
||||||
|
if _session.is_host():
|
||||||
|
_handle_request(_session.get_local_peer_id(), request)
|
||||||
|
else:
|
||||||
|
submit_chat_message.rpc_id(1, request)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func get_history() -> Array[Dictionary]:
|
||||||
|
return _history.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
||||||
|
func submit_chat_message(data: Dictionary) -> void:
|
||||||
|
var sender_id := multiplayer.get_remote_sender_id()
|
||||||
|
if _session.is_host() and _session.is_authenticated_peer(sender_id):
|
||||||
|
_handle_request(sender_id, data)
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_request(peer_id: int, data: Dictionary) -> void:
|
||||||
|
if (
|
||||||
|
not NetworkChatProtocol.validate_request(data)
|
||||||
|
or str(data["session_id"]) != _session.get_session_id()
|
||||||
|
):
|
||||||
|
_send_rejection(peer_id, "Message could not be sent.")
|
||||||
|
return
|
||||||
|
var request_id: String = data["request_id"]
|
||||||
|
var ledger: Dictionary = _request_ledgers.get(peer_id, {})
|
||||||
|
if ledger.has(request_id):
|
||||||
|
_send_message(peer_id, ledger[request_id])
|
||||||
|
return
|
||||||
|
if not _consume_rate(peer_id):
|
||||||
|
_send_rejection(peer_id, "Slow down.")
|
||||||
|
return
|
||||||
|
var record := _session.get_peer_record(peer_id)
|
||||||
|
if record == null:
|
||||||
|
return
|
||||||
|
var message := _make_message(
|
||||||
|
NetworkChatProtocol.Kind.PLAYER,
|
||||||
|
peer_id,
|
||||||
|
record.display_name,
|
||||||
|
NetworkChatProtocol.sanitize_body(data["body"])
|
||||||
|
)
|
||||||
|
ledger[request_id] = message.duplicate(true)
|
||||||
|
while ledger.size() > 64:
|
||||||
|
ledger.erase(ledger.keys().front())
|
||||||
|
_request_ledgers[peer_id] = ledger
|
||||||
|
_broadcast(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _make_message(
|
||||||
|
kind: int,
|
||||||
|
peer_id: int,
|
||||||
|
display_name: String,
|
||||||
|
body: String,
|
||||||
|
) -> Dictionary:
|
||||||
|
_sequence += 1
|
||||||
|
return {
|
||||||
|
"message_id": _new_id("chat"),
|
||||||
|
"session_id": _session.get_session_id(),
|
||||||
|
"sequence": _sequence,
|
||||||
|
"kind": kind,
|
||||||
|
"sender_peer_id": peer_id,
|
||||||
|
"sender_display_name": display_name.left(24),
|
||||||
|
"body": body,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _broadcast(message: Dictionary) -> void:
|
||||||
|
_apply_message(message)
|
||||||
|
receive_chat_message.rpc(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _send_message(peer_id: int, message: Dictionary) -> void:
|
||||||
|
if peer_id == _session.get_local_peer_id():
|
||||||
|
_apply_message(message)
|
||||||
|
else:
|
||||||
|
receive_chat_message.rpc_id(peer_id, message)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
||||||
|
func receive_chat_message(data: Dictionary) -> void:
|
||||||
|
_apply_message(data)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_message(data: Dictionary) -> void:
|
||||||
|
if (
|
||||||
|
not NetworkChatProtocol.validate_message(data)
|
||||||
|
or str(data["session_id"]) != _session.get_session_id()
|
||||||
|
or _seen_messages.has(str(data["message_id"]))
|
||||||
|
):
|
||||||
|
return
|
||||||
|
_seen_messages[data["message_id"]] = true
|
||||||
|
_history.append(data.duplicate(true))
|
||||||
|
while _history.size() > NetworkChatProtocol.MAX_HISTORY:
|
||||||
|
_history.pop_front()
|
||||||
|
message_received.emit(data.duplicate(true))
|
||||||
|
|
||||||
|
|
||||||
|
func _send_rejection(peer_id: int, message: String) -> void:
|
||||||
|
if peer_id == _session.get_local_peer_id():
|
||||||
|
send_rejected.emit(message)
|
||||||
|
else:
|
||||||
|
receive_chat_rejection.rpc_id(peer_id, message.left(80))
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
||||||
|
func receive_chat_rejection(message: String) -> void:
|
||||||
|
send_rejected.emit(message.left(80))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_peer_authenticated(peer_id: int, display_name: String) -> void:
|
||||||
|
if not _session.is_host():
|
||||||
|
return
|
||||||
|
_peer_names[peer_id] = display_name
|
||||||
|
var start := maxi(0, _history.size() - NetworkChatProtocol.LATE_JOIN_HISTORY)
|
||||||
|
receive_chat_history.rpc_id(peer_id, _history.slice(start))
|
||||||
|
_broadcast(_make_message(
|
||||||
|
NetworkChatProtocol.Kind.SYSTEM, 0, "", "%s joined." % display_name
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_peer_removed(peer_id: int) -> void:
|
||||||
|
_request_ledgers.erase(peer_id)
|
||||||
|
_rate_times.erase(peer_id)
|
||||||
|
if not _session.is_host():
|
||||||
|
return
|
||||||
|
var name: String = _peer_names.get(peer_id, "Player")
|
||||||
|
_peer_names.erase(peer_id)
|
||||||
|
_broadcast(_make_message(
|
||||||
|
NetworkChatProtocol.Kind.SYSTEM, 0, "", "%s left." % name
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
||||||
|
func receive_chat_history(values: Array) -> void:
|
||||||
|
if values.size() > NetworkChatProtocol.LATE_JOIN_HISTORY:
|
||||||
|
return
|
||||||
|
_history.clear()
|
||||||
|
_seen_messages.clear()
|
||||||
|
for value: Variant in values:
|
||||||
|
if NetworkChatProtocol.validate_message(value):
|
||||||
|
_apply_message(value)
|
||||||
|
history_replaced.emit(_history.duplicate(true))
|
||||||
|
|
||||||
|
|
||||||
|
func _consume_rate(peer_id: int) -> bool:
|
||||||
|
var now := Time.get_ticks_msec() / 1000.0
|
||||||
|
var times: Array = _rate_times.get(peer_id, [])
|
||||||
|
while not times.is_empty() and now - float(times.front()) > WINDOW_SECONDS:
|
||||||
|
times.pop_front()
|
||||||
|
var recent_burst := 0
|
||||||
|
for value: Variant in times:
|
||||||
|
if now - float(value) <= 1.0:
|
||||||
|
recent_burst += 1
|
||||||
|
if recent_burst >= BURST_COUNT or times.size() >= WINDOW_COUNT:
|
||||||
|
_rate_times[peer_id] = times
|
||||||
|
return false
|
||||||
|
times.append(now)
|
||||||
|
_rate_times[peer_id] = times
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
|
if state in [
|
||||||
|
NetworkSession.State.INACTIVE,
|
||||||
|
NetworkSession.State.DISCONNECTING,
|
||||||
|
NetworkSession.State.CONNECTION_FAILED,
|
||||||
|
NetworkSession.State.SERVER_LOST,
|
||||||
|
]:
|
||||||
|
_history.clear()
|
||||||
|
_seen_messages.clear()
|
||||||
|
_request_ledgers.clear()
|
||||||
|
_rate_times.clear()
|
||||||
|
_peer_names.clear()
|
||||||
|
_sequence = 0
|
||||||
|
history_replaced.emit([])
|
||||||
|
|
||||||
|
|
||||||
|
func _new_id(prefix: String) -> String:
|
||||||
|
return "%s:%s" % [
|
||||||
|
prefix, Crypto.new().generate_random_bytes(16).hex_encode(),
|
||||||
|
]
|
||||||
1
network/network_chat_service.gd.uid
Normal file
1
network/network_chat_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b10jayoog5dc2
|
||||||
|
|
@ -32,6 +32,7 @@ var _local_capacity: PlayerCoolerCapacity
|
||||||
var _save_manager: PlayerSaveManager
|
var _save_manager: PlayerSaveManager
|
||||||
var _item_catalog: ItemCatalog
|
var _item_catalog: ItemCatalog
|
||||||
var _fish_catalog: FishPoolType
|
var _fish_catalog: FishPoolType
|
||||||
|
var _item_use: NetworkItemUseService
|
||||||
var _attempts: Dictionary[int, NetworkFishingAttempt] = {}
|
var _attempts: Dictionary[int, NetworkFishingAttempt] = {}
|
||||||
var _request_ledgers: Dictionary[int, Dictionary] = {}
|
var _request_ledgers: Dictionary[int, Dictionary] = {}
|
||||||
var _result_ledgers: Dictionary[String, bool] = {}
|
var _result_ledgers: Dictionary[String, bool] = {}
|
||||||
|
|
@ -53,6 +54,7 @@ func setup(
|
||||||
save_manager: PlayerSaveManager,
|
save_manager: PlayerSaveManager,
|
||||||
item_catalog: ItemCatalog,
|
item_catalog: ItemCatalog,
|
||||||
fish_catalog: FishPoolType,
|
fish_catalog: FishPoolType,
|
||||||
|
item_use: NetworkItemUseService,
|
||||||
) -> void:
|
) -> void:
|
||||||
_session = session
|
_session = session
|
||||||
_spawn_service = spawn_service
|
_spawn_service = spawn_service
|
||||||
|
|
@ -63,6 +65,7 @@ func setup(
|
||||||
_save_manager = save_manager
|
_save_manager = save_manager
|
||||||
_item_catalog = item_catalog
|
_item_catalog = item_catalog
|
||||||
_fish_catalog = fish_catalog
|
_fish_catalog = fish_catalog
|
||||||
|
_item_use = item_use
|
||||||
if not _session.peer_removed.is_connected(_on_peer_removed):
|
if not _session.peer_removed.is_connected(_on_peer_removed):
|
||||||
_session.peer_removed.connect(_on_peer_removed)
|
_session.peer_removed.connect(_on_peer_removed)
|
||||||
if not _session.state_changed.is_connected(_on_session_state_changed):
|
if not _session.state_changed.is_connected(_on_session_state_changed):
|
||||||
|
|
@ -259,8 +262,12 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
||||||
if region == null or region.fish_pool == null:
|
if region == null or region.fish_pool == null:
|
||||||
_record_and_reject(peer_id, request_id, "Cannot fish here.")
|
_record_and_reject(peer_id, request_id, "Cannot fish here.")
|
||||||
return
|
return
|
||||||
|
var effects: PlayerItemEffects = (
|
||||||
|
_item_use.get_effects_for_peer(peer_id)
|
||||||
|
if _item_use != null else null
|
||||||
|
)
|
||||||
var selected_fish: FishDataType = _select_authoritative_fish(
|
var selected_fish: FishDataType = _select_authoritative_fish(
|
||||||
region, data
|
region, data, effects
|
||||||
)
|
)
|
||||||
if selected_fish == null:
|
if selected_fish == null:
|
||||||
_record_and_reject(peer_id, request_id, "Nothing is biting here.")
|
_record_and_reject(peer_id, request_id, "Nothing is biting here.")
|
||||||
|
|
@ -275,10 +282,14 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
||||||
attempt.target = target
|
attempt.target = target
|
||||||
attempt.bobber_position = target
|
attempt.bobber_position = target
|
||||||
attempt.fish_id = selected_fish.id
|
attempt.fish_id = selected_fish.id
|
||||||
attempt.reel_speed = float(data["reel_speed"])
|
attempt.reel_speed = float(data["reel_speed"]) * (
|
||||||
attempt.barrier_damage = int(data["barrier_damage"])
|
effects.get_reel_multiplier() if effects != null else 1.0
|
||||||
|
)
|
||||||
|
attempt.barrier_damage = int(data["barrier_damage"]) + (
|
||||||
|
effects.get_barrier_bonus() if effects != null else 0
|
||||||
|
)
|
||||||
attempt.bite_time_remaining = _fishing_spot.wait_time * float(
|
attempt.bite_time_remaining = _fishing_spot.wait_time * float(
|
||||||
data["bite_multiplier"]
|
effects.get_bite_time_multiplier() if effects != null else 1.0
|
||||||
)
|
)
|
||||||
attempt.controller = CatchController.new()
|
attempt.controller = CatchController.new()
|
||||||
add_child(attempt.controller)
|
add_child(attempt.controller)
|
||||||
|
|
@ -359,6 +370,7 @@ func _make_waiting_snapshot(
|
||||||
func _select_authoritative_fish(
|
func _select_authoritative_fish(
|
||||||
region: FishableWaterRegion,
|
region: FishableWaterRegion,
|
||||||
data: Dictionary,
|
data: Dictionary,
|
||||||
|
effects: PlayerItemEffects,
|
||||||
) -> FishDataType:
|
) -> FishDataType:
|
||||||
var evidence_log := CollectionLogType.new()
|
var evidence_log := CollectionLogType.new()
|
||||||
for value: Variant in data["discovered_fish_ids"]:
|
for value: Variant in data["discovered_fish_ids"]:
|
||||||
|
|
@ -370,8 +382,11 @@ func _select_authoritative_fish(
|
||||||
selector.undiscovered_weight_multiplier = (
|
selector.undiscovered_weight_multiplier = (
|
||||||
_fishing_spot.undiscovered_weight_multiplier
|
_fishing_spot.undiscovered_weight_multiplier
|
||||||
)
|
)
|
||||||
for value: Variant in data["rarity_multipliers"]:
|
for rarity: int in range(FishDataType.Rarity.size()):
|
||||||
selector.rarity_weight_multipliers.append(float(value))
|
selector.rarity_weight_multipliers.append(
|
||||||
|
effects.get_rarity_weight_multiplier(rarity)
|
||||||
|
if effects != null else 1.0
|
||||||
|
)
|
||||||
selector.begin_roll()
|
selector.begin_roll()
|
||||||
var context: FishingContextType = _fishing_spot.build_network_context(region)
|
var context: FishingContextType = _fishing_spot.build_network_context(region)
|
||||||
return selector.select_fish(region.fish_pool, context, evidence_log)
|
return selector.select_fish(region.fish_pool, context, evidence_log)
|
||||||
|
|
|
||||||
83
network/network_item_protocol.gd
Normal file
83
network/network_item_protocol.gd
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
class_name NetworkItemProtocol
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const ITEM_USE_CAPABILITY: StringName = &"item_use_v1"
|
||||||
|
const EQUIPMENT_CAPABILITY: StringName = &"equipment_v1"
|
||||||
|
const RELIABLE_CHANNEL: int = NetworkProtocol.ITEM_RELIABLE_CHANNEL
|
||||||
|
const MAX_ID_LENGTH: int = 96
|
||||||
|
const MAX_ITEM_ID_LENGTH: int = 96
|
||||||
|
const MAX_MESSAGE_LENGTH: int = 160
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_use_request(data: Variant) -> String:
|
||||||
|
if typeof(data) != TYPE_DICTIONARY:
|
||||||
|
return "Item use could not be completed."
|
||||||
|
var value: Dictionary = data
|
||||||
|
for key: String in [
|
||||||
|
"request_id", "session_id", "item_id", "quantity",
|
||||||
|
]:
|
||||||
|
if not value.has(key):
|
||||||
|
return "Item use could not be completed."
|
||||||
|
if (
|
||||||
|
not _valid_id(value["request_id"])
|
||||||
|
or not _valid_id(value["session_id"])
|
||||||
|
or typeof(value["item_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||||
|
or str(value["item_id"]).is_empty()
|
||||||
|
or str(value["item_id"]).length() > MAX_ITEM_ID_LENGTH
|
||||||
|
or typeof(value["quantity"]) != TYPE_INT
|
||||||
|
or int(value["quantity"]) < 1
|
||||||
|
or int(value["quantity"]) > 99
|
||||||
|
):
|
||||||
|
return "Item use could not be completed."
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_use_result(data: Variant) -> bool:
|
||||||
|
if typeof(data) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
var value: Dictionary = data
|
||||||
|
return (
|
||||||
|
_valid_id(value.get("result_id"))
|
||||||
|
and _valid_id(value.get("request_id"))
|
||||||
|
and _valid_id(value.get("session_id"))
|
||||||
|
and typeof(value.get("target_peer_id")) == TYPE_INT
|
||||||
|
and typeof(value.get("accepted")) == TYPE_BOOL
|
||||||
|
and typeof(value.get("item_id")) in [TYPE_STRING, TYPE_STRING_NAME]
|
||||||
|
and not str(value["item_id"]).is_empty()
|
||||||
|
and str(value["item_id"]).length() <= MAX_ITEM_ID_LENGTH
|
||||||
|
and typeof(value.get("quantity")) == TYPE_INT
|
||||||
|
and int(value["quantity"]) >= 0
|
||||||
|
and int(value["quantity"]) <= 1
|
||||||
|
and typeof(value.get("duration")) in [TYPE_FLOAT, TYPE_INT]
|
||||||
|
and is_finite(float(value["duration"]))
|
||||||
|
and float(value["duration"]) >= 0.0
|
||||||
|
and typeof(value.get("message")) == TYPE_STRING
|
||||||
|
and str(value["message"]).length() <= MAX_MESSAGE_LENGTH
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_equipped_state(data: Variant) -> bool:
|
||||||
|
if typeof(data) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
var value: Dictionary = data
|
||||||
|
return (
|
||||||
|
_valid_id(value.get("session_id"))
|
||||||
|
and typeof(value.get("owner_peer_id")) == TYPE_INT
|
||||||
|
and int(value["owner_peer_id"]) > 0
|
||||||
|
and typeof(value.get("item_id")) in [TYPE_STRING, TYPE_STRING_NAME]
|
||||||
|
and str(value["item_id"]).length() <= MAX_ITEM_ID_LENGTH
|
||||||
|
and typeof(value.get("category")) == TYPE_INT
|
||||||
|
and int(value["category"]) >= -1
|
||||||
|
and int(value["category"]) < ItemData.Category.size()
|
||||||
|
and typeof(value.get("revision")) == TYPE_INT
|
||||||
|
and int(value["revision"]) >= 0
|
||||||
|
and typeof(value.get("owns_item")) == TYPE_BOOL
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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_item_protocol.gd.uid
Normal file
1
network/network_item_protocol.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bphibe6g81mhm
|
||||||
388
network/network_item_use_service.gd
Normal file
388
network/network_item_use_service.gd
Normal file
|
|
@ -0,0 +1,388 @@
|
||||||
|
class_name NetworkItemUseService
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
const MAX_LEDGER_ENTRIES: int = 64
|
||||||
|
|
||||||
|
signal local_item_use_pending(request_id: String)
|
||||||
|
signal local_item_use_finished(accepted: bool, message: String)
|
||||||
|
signal equipped_state_changed(peer_id: int, item_id: StringName, category: int)
|
||||||
|
|
||||||
|
var _session: NetworkSession
|
||||||
|
var _spawn_service: PlayerSpawnService
|
||||||
|
var _catalog: ItemCatalog
|
||||||
|
var _local_bag: PlayerBag
|
||||||
|
var _local_effects: PlayerItemEffects
|
||||||
|
var _save_manager: PlayerSaveManager
|
||||||
|
var _requests: Dictionary[int, Dictionary] = {}
|
||||||
|
var _pending_by_peer: Dictionary[int, String] = {}
|
||||||
|
var _result_owners: Dictionary[String, int] = {}
|
||||||
|
var _received_results: Dictionary[String, bool] = {}
|
||||||
|
var _pending_local: Dictionary = {}
|
||||||
|
var _equipped_states: Dictionary[int, Dictionary] = {}
|
||||||
|
var _local_equipped_revision: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func setup(
|
||||||
|
session: NetworkSession,
|
||||||
|
spawn_service: PlayerSpawnService,
|
||||||
|
catalog: ItemCatalog,
|
||||||
|
local_bag: PlayerBag,
|
||||||
|
local_effects: PlayerItemEffects,
|
||||||
|
save_manager: PlayerSaveManager,
|
||||||
|
) -> void:
|
||||||
|
_session = session
|
||||||
|
_spawn_service = spawn_service
|
||||||
|
_catalog = catalog
|
||||||
|
_local_bag = local_bag
|
||||||
|
_local_effects = local_effects
|
||||||
|
_save_manager = save_manager
|
||||||
|
_session.peer_removed.connect(_on_peer_removed)
|
||||||
|
_session.state_changed.connect(_on_session_state_changed)
|
||||||
|
_session.peer_authenticated.connect(_on_peer_authenticated)
|
||||||
|
|
||||||
|
|
||||||
|
func request_use(item_id: StringName) -> String:
|
||||||
|
if not _pending_local.is_empty():
|
||||||
|
local_item_use_finished.emit(false, "An item use is already pending.")
|
||||||
|
return ""
|
||||||
|
if (
|
||||||
|
_session == null
|
||||||
|
or not _session.is_gameplay_session_active()
|
||||||
|
or (
|
||||||
|
not _session.is_host()
|
||||||
|
and not _session.supports_server_capability(
|
||||||
|
NetworkItemProtocol.ITEM_USE_CAPABILITY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
local_item_use_finished.emit(false, "Item use is unavailable.")
|
||||||
|
return ""
|
||||||
|
var request_id := _new_id("item")
|
||||||
|
var data := {
|
||||||
|
"request_id": request_id,
|
||||||
|
"session_id": _session.get_session_id(),
|
||||||
|
"item_id": str(item_id),
|
||||||
|
"quantity": _local_bag.get_quantity(item_id),
|
||||||
|
}
|
||||||
|
_pending_local = data.duplicate(true)
|
||||||
|
local_item_use_pending.emit(request_id)
|
||||||
|
if _session.is_host():
|
||||||
|
_handle_use_request(_session.get_local_peer_id(), data)
|
||||||
|
else:
|
||||||
|
submit_item_use.rpc_id(1, data)
|
||||||
|
return request_id
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
|
||||||
|
func submit_item_use(data: Dictionary) -> void:
|
||||||
|
var sender_id := multiplayer.get_remote_sender_id()
|
||||||
|
if _session.is_host() and _session.is_authenticated_peer(sender_id):
|
||||||
|
_handle_use_request(sender_id, data)
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_use_request(peer_id: int, data: Dictionary) -> void:
|
||||||
|
var request_id := str(data.get("request_id", ""))
|
||||||
|
var ledger: Dictionary = _requests.get(peer_id, {})
|
||||||
|
if ledger.has(request_id):
|
||||||
|
_send_result(peer_id, ledger[request_id])
|
||||||
|
return
|
||||||
|
var error := NetworkItemProtocol.validate_use_request(data)
|
||||||
|
if (
|
||||||
|
error.is_empty()
|
||||||
|
and str(data["session_id"]) != _session.get_session_id()
|
||||||
|
):
|
||||||
|
error = "Item use could not be completed."
|
||||||
|
if error.is_empty() and _pending_by_peer.has(peer_id):
|
||||||
|
error = "An item use is already pending."
|
||||||
|
var item_id := StringName(str(data.get("item_id", "")))
|
||||||
|
var item: ItemData = _catalog.get_item_by_id(item_id)
|
||||||
|
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||||
|
var duration: float = 0.0
|
||||||
|
if error.is_empty() and (
|
||||||
|
item == null
|
||||||
|
or item.category != ItemData.Category.CONSUMABLE
|
||||||
|
or not item.usable
|
||||||
|
):
|
||||||
|
error = "That item cannot be used now."
|
||||||
|
if error.is_empty() and int(data["quantity"]) < 1:
|
||||||
|
error = "You do not have that item."
|
||||||
|
if error.is_empty() and (
|
||||||
|
avatar == null
|
||||||
|
or avatar.is_water_recovery_active()
|
||||||
|
):
|
||||||
|
error = "That item cannot be used now."
|
||||||
|
if error.is_empty():
|
||||||
|
duration = avatar.item_effects.get_effect_duration(item_id)
|
||||||
|
if duration <= 0.0:
|
||||||
|
error = "That item cannot be used now."
|
||||||
|
var result := {
|
||||||
|
"result_id": _new_id("item_result"),
|
||||||
|
"request_id": request_id if not request_id.is_empty() else "invalid",
|
||||||
|
"session_id": _session.get_session_id(),
|
||||||
|
"target_peer_id": peer_id,
|
||||||
|
"accepted": error.is_empty(),
|
||||||
|
"item_id": str(item_id) if not item_id.is_empty() else "invalid",
|
||||||
|
"quantity": 1 if error.is_empty() else 0,
|
||||||
|
"duration": duration if error.is_empty() else 0.0,
|
||||||
|
"message": "Used %s." % item.display_name if error.is_empty() else error,
|
||||||
|
}
|
||||||
|
ledger[request_id] = result.duplicate(true)
|
||||||
|
_bound(ledger)
|
||||||
|
_requests[peer_id] = ledger
|
||||||
|
_result_owners[result["result_id"]] = peer_id
|
||||||
|
_pending_by_peer[peer_id] = request_id
|
||||||
|
_send_result(peer_id, result)
|
||||||
|
|
||||||
|
|
||||||
|
func _send_result(peer_id: int, result: Dictionary) -> void:
|
||||||
|
if peer_id == _session.get_local_peer_id():
|
||||||
|
_apply_result(result)
|
||||||
|
else:
|
||||||
|
receive_item_use_result.rpc_id(peer_id, result)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
|
||||||
|
func receive_item_use_result(data: Dictionary) -> void:
|
||||||
|
_apply_result(data)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_result(data: Dictionary) -> void:
|
||||||
|
if (
|
||||||
|
not NetworkItemProtocol.validate_use_result(data)
|
||||||
|
or str(data["session_id"]) != _session.get_session_id()
|
||||||
|
or int(data["target_peer_id"]) != _session.get_local_peer_id()
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var result_id: String = data["result_id"]
|
||||||
|
if _received_results.has(result_id):
|
||||||
|
_acknowledge(data, true, "")
|
||||||
|
return
|
||||||
|
if (
|
||||||
|
_pending_local.is_empty()
|
||||||
|
or str(data["request_id"]) != str(_pending_local.get("request_id", ""))
|
||||||
|
):
|
||||||
|
return
|
||||||
|
if not bool(data["accepted"]):
|
||||||
|
_received_results[result_id] = true
|
||||||
|
_pending_local.clear()
|
||||||
|
local_item_use_finished.emit(false, str(data["message"]))
|
||||||
|
_acknowledge(data, false, str(data["message"]))
|
||||||
|
return
|
||||||
|
var item_id := StringName(str(data["item_id"]))
|
||||||
|
var bag_snapshot := _local_bag.get_all_items()
|
||||||
|
if (
|
||||||
|
_local_bag.get_quantity(item_id) < 1
|
||||||
|
or not _local_bag.remove_item(item_id, 1)
|
||||||
|
or not _save_manager.save_if_dirty()
|
||||||
|
):
|
||||||
|
_local_bag.replace_all_items(bag_snapshot)
|
||||||
|
_save_manager.save_if_dirty()
|
||||||
|
_pending_local.clear()
|
||||||
|
local_item_use_finished.emit(false, "Item use could not be completed.")
|
||||||
|
_acknowledge(data, false, "Item use could not be completed.")
|
||||||
|
return
|
||||||
|
_received_results[result_id] = true
|
||||||
|
_bound(_received_results)
|
||||||
|
_pending_local.clear()
|
||||||
|
local_item_use_finished.emit(true, str(data["message"]))
|
||||||
|
_acknowledge(data, true, "")
|
||||||
|
|
||||||
|
|
||||||
|
func _acknowledge(data: Dictionary, applied: bool, message: String) -> void:
|
||||||
|
if _session.is_host():
|
||||||
|
_handle_ack(
|
||||||
|
_session.get_local_peer_id(),
|
||||||
|
str(data["result_id"]),
|
||||||
|
str(data["request_id"]),
|
||||||
|
applied,
|
||||||
|
message
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
acknowledge_item_use.rpc_id(
|
||||||
|
1, str(data["result_id"]), str(data["request_id"]), applied,
|
||||||
|
message.left(NetworkItemProtocol.MAX_MESSAGE_LENGTH)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
|
||||||
|
func acknowledge_item_use(
|
||||||
|
result_id: String,
|
||||||
|
request_id: String,
|
||||||
|
applied: bool,
|
||||||
|
message: String,
|
||||||
|
) -> void:
|
||||||
|
var sender_id := multiplayer.get_remote_sender_id()
|
||||||
|
if _session.is_host() and _session.is_authenticated_peer(sender_id):
|
||||||
|
_handle_ack(sender_id, result_id, request_id, applied, message)
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_ack(
|
||||||
|
peer_id: int,
|
||||||
|
result_id: String,
|
||||||
|
request_id: String,
|
||||||
|
applied: bool,
|
||||||
|
_message: String,
|
||||||
|
) -> void:
|
||||||
|
if (
|
||||||
|
_result_owners.get(result_id, 0) != peer_id
|
||||||
|
or _pending_by_peer.get(peer_id, "") != request_id
|
||||||
|
):
|
||||||
|
return
|
||||||
|
_pending_by_peer.erase(peer_id)
|
||||||
|
if not applied:
|
||||||
|
return
|
||||||
|
var result: Dictionary = _requests.get(peer_id, {}).get(request_id, {})
|
||||||
|
var avatar := _spawn_service.get_avatar(peer_id)
|
||||||
|
if avatar == null or result.is_empty():
|
||||||
|
return
|
||||||
|
var item_id := StringName(str(result["item_id"]))
|
||||||
|
avatar.item_effects.activate_authoritative(item_id, float(result["duration"]))
|
||||||
|
_broadcast_effect(peer_id, item_id, float(result["duration"]))
|
||||||
|
|
||||||
|
|
||||||
|
func get_effects_for_peer(peer_id: int) -> PlayerItemEffects:
|
||||||
|
var avatar := _spawn_service.get_avatar(peer_id)
|
||||||
|
return avatar.item_effects if avatar != null else null
|
||||||
|
|
||||||
|
|
||||||
|
func submit_local_equipped(item_id: StringName, owns_item: bool) -> void:
|
||||||
|
if _session == null or not _session.is_gameplay_session_active():
|
||||||
|
return
|
||||||
|
_local_equipped_revision += 1
|
||||||
|
var item: ItemData = _catalog.get_item_by_id(item_id)
|
||||||
|
var data := {
|
||||||
|
"session_id": _session.get_session_id(),
|
||||||
|
"owner_peer_id": _session.get_local_peer_id(),
|
||||||
|
"item_id": str(item_id) if item != null and owns_item else "",
|
||||||
|
"category": int(item.category) if item != null and owns_item else -1,
|
||||||
|
"revision": _local_equipped_revision,
|
||||||
|
"owns_item": owns_item and item != null,
|
||||||
|
}
|
||||||
|
if _session.is_host():
|
||||||
|
_handle_equipped_state(_session.get_local_peer_id(), data)
|
||||||
|
elif _session.supports_server_capability(
|
||||||
|
NetworkItemProtocol.EQUIPMENT_CAPABILITY
|
||||||
|
):
|
||||||
|
submit_equipped_state.rpc_id(1, data)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
|
||||||
|
func submit_equipped_state(data: Dictionary) -> void:
|
||||||
|
var sender_id := multiplayer.get_remote_sender_id()
|
||||||
|
if _session.is_host() and _session.is_authenticated_peer(sender_id):
|
||||||
|
_handle_equipped_state(sender_id, data)
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_equipped_state(peer_id: int, data: Dictionary) -> void:
|
||||||
|
if (
|
||||||
|
not NetworkItemProtocol.validate_equipped_state(data)
|
||||||
|
or str(data["session_id"]) != _session.get_session_id()
|
||||||
|
or int(data["owner_peer_id"]) != peer_id
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var previous: Dictionary = _equipped_states.get(peer_id, {})
|
||||||
|
if int(data["revision"]) <= int(previous.get("revision", -1)):
|
||||||
|
return
|
||||||
|
var item_id := StringName(str(data["item_id"]))
|
||||||
|
var item: ItemData = _catalog.get_item_by_id(item_id)
|
||||||
|
if not item_id.is_empty() and (
|
||||||
|
item == null
|
||||||
|
or not bool(data["owns_item"])
|
||||||
|
or int(item.category) != int(data["category"])
|
||||||
|
):
|
||||||
|
return
|
||||||
|
_equipped_states[peer_id] = data.duplicate(true)
|
||||||
|
_apply_equipped(data)
|
||||||
|
receive_equipped_state.rpc(data)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
|
||||||
|
func receive_equipped_state(data: Dictionary) -> void:
|
||||||
|
if NetworkItemProtocol.validate_equipped_state(data):
|
||||||
|
_equipped_states[int(data["owner_peer_id"])] = data.duplicate(true)
|
||||||
|
_apply_equipped(data)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_equipped(data: Dictionary) -> void:
|
||||||
|
var peer_id: int = data["owner_peer_id"]
|
||||||
|
var avatar := _spawn_service.get_avatar(peer_id)
|
||||||
|
if avatar != null:
|
||||||
|
avatar.set_active_item_is_rod(
|
||||||
|
int(data["category"]) == ItemData.Category.ROD
|
||||||
|
)
|
||||||
|
equipped_state_changed.emit(
|
||||||
|
peer_id, StringName(str(data["item_id"])), int(data["category"])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _broadcast_effect(peer_id: int, item_id: StringName, duration: float) -> void:
|
||||||
|
var data := {
|
||||||
|
"session_id": _session.get_session_id(),
|
||||||
|
"owner_peer_id": peer_id,
|
||||||
|
"item_id": str(item_id),
|
||||||
|
"remaining": duration,
|
||||||
|
}
|
||||||
|
_apply_effect_snapshot(data)
|
||||||
|
receive_effect_snapshot.rpc(data)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
|
||||||
|
func receive_effect_snapshot(data: Dictionary) -> void:
|
||||||
|
_apply_effect_snapshot(data)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_effect_snapshot(data: Dictionary) -> void:
|
||||||
|
if (
|
||||||
|
typeof(data.get("owner_peer_id")) != TYPE_INT
|
||||||
|
or typeof(data.get("item_id")) != TYPE_STRING
|
||||||
|
or typeof(data.get("remaining")) not in [TYPE_FLOAT, TYPE_INT]
|
||||||
|
or str(data.get("session_id", "")) != _session.get_session_id()
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var avatar := _spawn_service.get_avatar(int(data["owner_peer_id"]))
|
||||||
|
if avatar != null:
|
||||||
|
avatar.item_effects.activate_authoritative(
|
||||||
|
StringName(data["item_id"]), float(data["remaining"])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
|
||||||
|
if not _session.is_host():
|
||||||
|
return
|
||||||
|
for state: Dictionary in _equipped_states.values():
|
||||||
|
receive_equipped_state.rpc_id(peer_id, state)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_peer_removed(peer_id: int) -> void:
|
||||||
|
_requests.erase(peer_id)
|
||||||
|
_pending_by_peer.erase(peer_id)
|
||||||
|
_equipped_states.erase(peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
|
if state in [
|
||||||
|
NetworkSession.State.INACTIVE,
|
||||||
|
NetworkSession.State.DISCONNECTING,
|
||||||
|
NetworkSession.State.CONNECTION_FAILED,
|
||||||
|
NetworkSession.State.SERVER_LOST,
|
||||||
|
]:
|
||||||
|
var pending := not _pending_local.is_empty()
|
||||||
|
_requests.clear()
|
||||||
|
_pending_by_peer.clear()
|
||||||
|
_result_owners.clear()
|
||||||
|
_received_results.clear()
|
||||||
|
_equipped_states.clear()
|
||||||
|
_pending_local.clear()
|
||||||
|
if pending:
|
||||||
|
local_item_use_finished.emit(false, "Connection lost.")
|
||||||
|
|
||||||
|
|
||||||
|
func _bound(values: Dictionary) -> void:
|
||||||
|
while values.size() > MAX_LEDGER_ENTRIES:
|
||||||
|
values.erase(values.keys().front())
|
||||||
|
|
||||||
|
|
||||||
|
func _new_id(prefix: String) -> String:
|
||||||
|
return "%s:%s" % [
|
||||||
|
prefix, Crypto.new().generate_random_bytes(16).hex_encode(),
|
||||||
|
]
|
||||||
1
network/network_item_use_service.gd.uid
Normal file
1
network/network_item_use_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dx21uu253nmpw
|
||||||
|
|
@ -34,6 +34,29 @@ func load_or_create() -> bool:
|
||||||
return _save_atomic()
|
return _save_atomic()
|
||||||
|
|
||||||
|
|
||||||
|
func set_display_name(value: String) -> bool:
|
||||||
|
var clean_name: String = value.strip_edges()
|
||||||
|
if not is_valid_display_name(clean_name):
|
||||||
|
return false
|
||||||
|
var previous: String = display_name
|
||||||
|
display_name = clean_name
|
||||||
|
if _save_atomic():
|
||||||
|
return true
|
||||||
|
display_name = previous
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
static func is_valid_display_name(value: String) -> bool:
|
||||||
|
var clean_name := value.strip_edges()
|
||||||
|
if clean_name.is_empty() or clean_name.length() > 24:
|
||||||
|
return false
|
||||||
|
for index: int in clean_name.length():
|
||||||
|
var codepoint: int = clean_name.unicode_at(index)
|
||||||
|
if codepoint < 32 or codepoint == 127:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func _load_existing() -> bool:
|
func _load_existing() -> bool:
|
||||||
var file := FileAccess.open(PROFILE_PATH, FileAccess.READ)
|
var file := FileAccess.open(PROFILE_PATH, FileAccess.READ)
|
||||||
if file == null:
|
if file == null:
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,18 @@ extends RefCounted
|
||||||
|
|
||||||
const PROTOCOL_VERSION: int = 2
|
const PROTOCOL_VERSION: int = 2
|
||||||
const GAME_BUILD: String = "prealpha"
|
const GAME_BUILD: String = "prealpha"
|
||||||
const MAX_DISPLAY_NAME_LENGTH: int = 48
|
const MAX_DISPLAY_NAME_LENGTH: int = 24
|
||||||
const MAX_PROFILE_ID_LENGTH: int = 96
|
const MAX_PROFILE_ID_LENGTH: int = 96
|
||||||
const MAX_NONCE_LENGTH: int = 96
|
const MAX_NONCE_LENGTH: int = 96
|
||||||
# ENet channels: 0 reliable lifecycle, 1 movement input, 2 movement
|
# ENet channels: 0 reliable lifecycle, 1 movement input, 2 movement
|
||||||
# snapshots, 3 fishing input, 4 fishing snapshots, 5 reliable sales,
|
# snapshots, 3 fishing input, 4 fishing snapshots, 5 reliable sales,
|
||||||
# 6 reliable shop transactions.
|
# 6 reliable shop transactions, 7 reliable item/equipment lifecycle,
|
||||||
|
# 8 reliable ordered session chat.
|
||||||
const SALE_RELIABLE_CHANNEL: int = 5
|
const SALE_RELIABLE_CHANNEL: int = 5
|
||||||
const SHOP_RELIABLE_CHANNEL: int = 6
|
const SHOP_RELIABLE_CHANNEL: int = 6
|
||||||
|
const ITEM_RELIABLE_CHANNEL: int = 7
|
||||||
|
const CHAT_RELIABLE_CHANNEL: int = 8
|
||||||
|
const ENET_CHANNEL_COUNT: int = 9
|
||||||
|
|
||||||
enum RejectionCode {
|
enum RejectionCode {
|
||||||
NONE,
|
NONE,
|
||||||
|
|
@ -109,6 +113,9 @@ static func make_server_hello(
|
||||||
"fishing_v1",
|
"fishing_v1",
|
||||||
"sale_v1",
|
"sale_v1",
|
||||||
"shop_v1",
|
"shop_v1",
|
||||||
|
"item_use_v1",
|
||||||
|
"equipment_v1",
|
||||||
|
"chat_v1",
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ signal peer_authenticated(peer_id: int, display_name: String)
|
||||||
signal peer_removed(peer_id: int)
|
signal peer_removed(peer_id: int)
|
||||||
signal host_openness_changed(is_open: bool)
|
signal host_openness_changed(is_open: bool)
|
||||||
signal peer_count_changed(player_count: int, max_players: int)
|
signal peer_count_changed(player_count: int, max_players: int)
|
||||||
|
signal peer_display_name_changed(peer_id: int, display_name: String)
|
||||||
signal join_authenticated
|
signal join_authenticated
|
||||||
signal server_lost
|
signal server_lost
|
||||||
signal remote_recovery_requested(peer_id: int, entry_position: Vector3)
|
signal remote_recovery_requested(peer_id: int, entry_position: Vector3)
|
||||||
|
|
@ -275,10 +276,50 @@ func supports_server_capability(capability: StringName) -> bool:
|
||||||
if is_host():
|
if is_host():
|
||||||
return str(capability) in PackedStringArray([
|
return str(capability) in PackedStringArray([
|
||||||
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
|
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
|
||||||
|
"item_use_v1", "equipment_v1", "chat_v1",
|
||||||
])
|
])
|
||||||
return str(capability) in _server_capabilities
|
return str(capability) in _server_capabilities
|
||||||
|
|
||||||
|
|
||||||
|
func get_peer_record(peer_id: int) -> PeerRegistry.PeerRecord:
|
||||||
|
return _registry.get_peer(peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
func update_local_display_name(value: String) -> bool:
|
||||||
|
if _profile == null or not _profile.set_display_name(value):
|
||||||
|
return false
|
||||||
|
var peer_id := get_local_peer_id()
|
||||||
|
if is_host():
|
||||||
|
_apply_display_name(peer_id, _profile.display_name)
|
||||||
|
receive_display_name.rpc(peer_id, _profile.display_name)
|
||||||
|
elif is_joined_client() and supports_server_capability(&"chat_v1"):
|
||||||
|
submit_display_name.rpc_id(1, _profile.display_name)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_remote", "reliable", 0)
|
||||||
|
func submit_display_name(value: String) -> void:
|
||||||
|
var sender_id := multiplayer.get_remote_sender_id()
|
||||||
|
if (
|
||||||
|
is_host()
|
||||||
|
and is_authenticated_peer(sender_id)
|
||||||
|
and NetworkProfilePreferences.is_valid_display_name(value)
|
||||||
|
):
|
||||||
|
_apply_display_name(sender_id, value.strip_edges())
|
||||||
|
receive_display_name.rpc(sender_id, value.strip_edges())
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "reliable", 0)
|
||||||
|
func receive_display_name(peer_id: int, value: String) -> void:
|
||||||
|
if NetworkProfilePreferences.is_valid_display_name(value):
|
||||||
|
_apply_display_name(peer_id, value.strip_edges())
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_display_name(peer_id: int, value: String) -> void:
|
||||||
|
if _registry.update_display_name(peer_id, value):
|
||||||
|
peer_display_name_changed.emit(peer_id, value)
|
||||||
|
|
||||||
|
|
||||||
func get_local_peer_id() -> int:
|
func get_local_peer_id() -> int:
|
||||||
return multiplayer.get_unique_id() if is_gameplay_session_active() else 0
|
return multiplayer.get_unique_id() if is_gameplay_session_active() else 0
|
||||||
|
|
||||||
|
|
@ -414,6 +455,12 @@ func submit_client_hello(data: Dictionary) -> void:
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
var display_name: String = data["display_name"]
|
var display_name: String = data["display_name"]
|
||||||
|
if not NetworkProfilePreferences.is_valid_display_name(display_name):
|
||||||
|
_reject_peer(
|
||||||
|
sender_id,
|
||||||
|
NetworkProtocol.RejectionCode.MALFORMED_HANDSHAKE
|
||||||
|
)
|
||||||
|
return
|
||||||
if not _registry.add_peer(
|
if not _registry.add_peer(
|
||||||
sender_id,
|
sender_id,
|
||||||
profile_id,
|
profile_id,
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,14 @@ func get_peer(peer_id: int) -> PeerRecord:
|
||||||
return _records.get(peer_id)
|
return _records.get(peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
func update_display_name(peer_id: int, display_name: String) -> bool:
|
||||||
|
var record: PeerRecord = _records.get(peer_id)
|
||||||
|
if record == null or display_name.is_empty():
|
||||||
|
return false
|
||||||
|
record.display_name = display_name
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func has_peer(peer_id: int) -> bool:
|
func has_peer(peer_id: int) -> bool:
|
||||||
return _records.has(peer_id)
|
return _records.has(peer_id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -536,6 +536,10 @@ func get_body_center_height() -> float:
|
||||||
return body_center_height
|
return body_center_height
|
||||||
|
|
||||||
|
|
||||||
|
func get_gameplay_camera() -> Camera3D:
|
||||||
|
return _camera
|
||||||
|
|
||||||
|
|
||||||
func get_fishing_rod_tip() -> Marker3D:
|
func get_fishing_rod_tip() -> Marker3D:
|
||||||
return _fishing_rod_tip
|
return _fishing_rod_tip
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,28 @@ func use_consumable(
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func activate_authoritative(item_id: StringName, duration: float) -> bool:
|
||||||
|
if not _remaining.has(item_id) or not is_finite(duration) or duration <= 0.0:
|
||||||
|
return false
|
||||||
|
_remaining[item_id] = duration
|
||||||
|
effects_changed.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func restore_remaining(snapshot: Dictionary[StringName, float]) -> void:
|
||||||
|
for item_id: StringName in _remaining:
|
||||||
|
_remaining[item_id] = maxf(snapshot.get(item_id, 0.0), 0.0)
|
||||||
|
effects_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func get_remaining_snapshot() -> Dictionary[StringName, float]:
|
||||||
|
return _remaining.duplicate()
|
||||||
|
|
||||||
|
|
||||||
|
func get_effect_duration(item_id: StringName) -> float:
|
||||||
|
return _get_duration(item_id)
|
||||||
|
|
||||||
|
|
||||||
func reset_all() -> void:
|
func reset_all() -> void:
|
||||||
var changed: bool = false
|
var changed: bool = false
|
||||||
for item_id: StringName in _remaining:
|
for item_id: StringName in _remaining:
|
||||||
|
|
|
||||||
241
ui/chat_ui.gd
Normal file
241
ui/chat_ui.gd
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
class_name ChatUI
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
const INPUT_OWNER: StringName = &"chat"
|
||||||
|
const COMPACT_SECONDS: float = 8.0
|
||||||
|
const SPEECH_SECONDS: float = 6.0
|
||||||
|
|
||||||
|
var _service: NetworkChatService
|
||||||
|
var _session: NetworkSession
|
||||||
|
var _spawn: PlayerSpawnService
|
||||||
|
var _player: Player
|
||||||
|
var _fishing_spot: FishingSpot
|
||||||
|
var _history: Label
|
||||||
|
var _entry: LineEdit
|
||||||
|
var _chat_button: Button
|
||||||
|
var _panel: PanelContainer
|
||||||
|
var _speech_layer: Control
|
||||||
|
var _speech: Dictionary[int, Dictionary] = {}
|
||||||
|
var _opened: bool = false
|
||||||
|
var _available: bool = false
|
||||||
|
var _last_message_time: float = -INF
|
||||||
|
var _prior_movement: bool = true
|
||||||
|
var _prior_camera: bool = true
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_build_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func setup(
|
||||||
|
service: NetworkChatService,
|
||||||
|
session: NetworkSession,
|
||||||
|
spawn: PlayerSpawnService,
|
||||||
|
player: Player,
|
||||||
|
fishing_spot: FishingSpot,
|
||||||
|
) -> void:
|
||||||
|
_service = service
|
||||||
|
_session = session
|
||||||
|
_spawn = spawn
|
||||||
|
_player = player
|
||||||
|
_fishing_spot = fishing_spot
|
||||||
|
_service.message_received.connect(_on_message)
|
||||||
|
_service.history_replaced.connect(_on_history)
|
||||||
|
_service.send_rejected.connect(_on_rejected)
|
||||||
|
_session.peer_removed.connect(_on_peer_removed)
|
||||||
|
_refresh_history()
|
||||||
|
|
||||||
|
|
||||||
|
func open_chat() -> void:
|
||||||
|
if (
|
||||||
|
_opened or not _available or _service == null
|
||||||
|
or not _session.is_gameplay_session_active()
|
||||||
|
):
|
||||||
|
return
|
||||||
|
_opened = true
|
||||||
|
_prior_movement = _player.is_movement_enabled()
|
||||||
|
_prior_camera = _player.is_camera_input_enabled()
|
||||||
|
_player.set_movement_enabled(false)
|
||||||
|
_player.set_camera_input_enabled(false)
|
||||||
|
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
||||||
|
_panel.show()
|
||||||
|
_entry.show()
|
||||||
|
_entry.grab_focus()
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
|
||||||
|
|
||||||
|
func set_available(value: bool) -> void:
|
||||||
|
_available = value
|
||||||
|
_chat_button.visible = value
|
||||||
|
if not value:
|
||||||
|
close_chat()
|
||||||
|
|
||||||
|
|
||||||
|
func close_chat() -> void:
|
||||||
|
if not _opened:
|
||||||
|
return
|
||||||
|
_opened = false
|
||||||
|
_entry.release_focus()
|
||||||
|
_entry.hide()
|
||||||
|
_player.set_movement_enabled(_prior_movement)
|
||||||
|
_player.set_camera_input_enabled(_prior_camera)
|
||||||
|
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false)
|
||||||
|
_refresh_visibility()
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventKey and event.pressed and not event.echo:
|
||||||
|
if event.keycode in [KEY_ENTER, KEY_KP_ENTER]:
|
||||||
|
if _opened:
|
||||||
|
_send()
|
||||||
|
elif (
|
||||||
|
_available
|
||||||
|
and get_viewport().gui_get_focus_owner() is not LineEdit
|
||||||
|
):
|
||||||
|
open_chat()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif event.keycode == KEY_ESCAPE and _opened:
|
||||||
|
close_chat()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
_refresh_visibility()
|
||||||
|
var now := Time.get_ticks_msec() / 1000.0
|
||||||
|
for peer_id: int in _speech.keys():
|
||||||
|
var state: Dictionary = _speech[peer_id]
|
||||||
|
var label := state.get("label") as Label
|
||||||
|
if label == null or now >= float(state.get("expires", 0.0)):
|
||||||
|
if label != null:
|
||||||
|
label.queue_free()
|
||||||
|
_speech.erase(peer_id)
|
||||||
|
continue
|
||||||
|
var avatar := _spawn.get_avatar(peer_id)
|
||||||
|
var camera := _player.get_gameplay_camera()
|
||||||
|
if avatar == null or camera == null:
|
||||||
|
label.hide()
|
||||||
|
continue
|
||||||
|
var world_position := avatar.get_body_center_position() + Vector3.UP * 1.2
|
||||||
|
if camera.is_position_behind(world_position):
|
||||||
|
label.hide()
|
||||||
|
continue
|
||||||
|
var screen_position := camera.unproject_position(world_position)
|
||||||
|
label.position = screen_position - Vector2(label.size.x * 0.5, 42.0)
|
||||||
|
label.show()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
_panel = PanelContainer.new()
|
||||||
|
_panel.position = Vector2(18, 390)
|
||||||
|
_panel.size = Vector2(390, 240)
|
||||||
|
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
add_child(_panel)
|
||||||
|
var box := VBoxContainer.new()
|
||||||
|
box.add_theme_constant_override("separation", 6)
|
||||||
|
_panel.add_child(box)
|
||||||
|
_history = Label.new()
|
||||||
|
_history.custom_minimum_size = Vector2(360, 150)
|
||||||
|
_history.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_history.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
||||||
|
_history.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
box.add_child(_history)
|
||||||
|
_entry = LineEdit.new()
|
||||||
|
_entry.placeholder_text = "type a message…"
|
||||||
|
_entry.max_length = NetworkChatProtocol.MAX_VISIBLE_CHARACTERS
|
||||||
|
_entry.text_submitted.connect(func(_value: String) -> void: _send())
|
||||||
|
_entry.hide()
|
||||||
|
box.add_child(_entry)
|
||||||
|
_chat_button = Button.new()
|
||||||
|
_chat_button.text = "chat"
|
||||||
|
_chat_button.position = Vector2(18, 570)
|
||||||
|
_chat_button.size = Vector2(76, 54)
|
||||||
|
_chat_button.pressed.connect(open_chat)
|
||||||
|
add_child(_chat_button)
|
||||||
|
_speech_layer = Control.new()
|
||||||
|
_speech_layer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_speech_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(_speech_layer)
|
||||||
|
_panel.hide()
|
||||||
|
|
||||||
|
|
||||||
|
func _send() -> void:
|
||||||
|
var body := _entry.text
|
||||||
|
if _service.send_local_message(body):
|
||||||
|
_entry.clear()
|
||||||
|
close_chat()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_message(message: Dictionary) -> void:
|
||||||
|
_last_message_time = Time.get_ticks_msec() / 1000.0
|
||||||
|
_refresh_history()
|
||||||
|
if int(message["kind"]) != NetworkChatProtocol.Kind.PLAYER:
|
||||||
|
return
|
||||||
|
var peer_id: int = message["sender_peer_id"]
|
||||||
|
_on_peer_removed(peer_id)
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = "%s: %s" % [
|
||||||
|
str(message["sender_display_name"]), str(message["body"]).left(120),
|
||||||
|
]
|
||||||
|
label.custom_minimum_size = Vector2(80, 34)
|
||||||
|
label.size = Vector2(280, 70)
|
||||||
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_speech_layer.add_child(label)
|
||||||
|
_speech[peer_id] = {
|
||||||
|
"label": label,
|
||||||
|
"expires": Time.get_ticks_msec() / 1000.0 + SPEECH_SECONDS,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _on_history(_messages: Array[Dictionary]) -> void:
|
||||||
|
if _messages.is_empty():
|
||||||
|
for peer_id: int in _speech.keys():
|
||||||
|
_on_peer_removed(peer_id)
|
||||||
|
_refresh_history()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_rejected(message: String) -> void:
|
||||||
|
_entry.placeholder_text = message
|
||||||
|
if not _opened:
|
||||||
|
open_chat()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_history() -> void:
|
||||||
|
if _service == null:
|
||||||
|
return
|
||||||
|
var lines: Array[String] = []
|
||||||
|
var messages := _service.get_history()
|
||||||
|
var start := maxi(0, messages.size() - 8)
|
||||||
|
for message: Dictionary in messages.slice(start):
|
||||||
|
if int(message["kind"]) == NetworkChatProtocol.Kind.SYSTEM:
|
||||||
|
lines.append("• %s" % str(message["body"]))
|
||||||
|
else:
|
||||||
|
lines.append("%s: %s" % [
|
||||||
|
str(message["sender_display_name"]), str(message["body"]),
|
||||||
|
])
|
||||||
|
_history.text = "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_visibility() -> void:
|
||||||
|
if not _available:
|
||||||
|
_panel.hide()
|
||||||
|
return
|
||||||
|
if _opened:
|
||||||
|
_panel.show()
|
||||||
|
return
|
||||||
|
var recent := (
|
||||||
|
Time.get_ticks_msec() / 1000.0 - _last_message_time
|
||||||
|
< COMPACT_SECONDS
|
||||||
|
)
|
||||||
|
_panel.visible = recent and _service != null and not _service.get_history().is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_peer_removed(peer_id: int) -> void:
|
||||||
|
var state: Dictionary = _speech.get(peer_id, {})
|
||||||
|
var label := state.get("label") as Label
|
||||||
|
if label != null:
|
||||||
|
label.queue_free()
|
||||||
|
_speech.erase(peer_id)
|
||||||
1
ui/chat_ui.gd.uid
Normal file
1
ui/chat_ui.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://canbrvh1cqkie
|
||||||
|
|
@ -31,6 +31,7 @@ const PlayerItemEffectsType = preload(
|
||||||
const PlayerCoolerCapacityType = preload(
|
const PlayerCoolerCapacityType = preload(
|
||||||
"res://progression/player_cooler_capacity.gd"
|
"res://progression/player_cooler_capacity.gd"
|
||||||
)
|
)
|
||||||
|
const ChatUIType = preload("res://ui/chat_ui.gd")
|
||||||
|
|
||||||
signal pixelation_settings_visibility_changed(is_visible: bool)
|
signal pixelation_settings_visibility_changed(is_visible: bool)
|
||||||
signal crisp_reset_focus_requested
|
signal crisp_reset_focus_requested
|
||||||
|
|
@ -53,6 +54,7 @@ signal interactive_pointer_ui_changed(is_open: bool)
|
||||||
@onready var _fishing_shop: FishingShopType = %FishingShop
|
@onready var _fishing_shop: FishingShopType = %FishingShop
|
||||||
@onready var _shop_prompt: PanelContainer = %ShopPrompt
|
@onready var _shop_prompt: PanelContainer = %ShopPrompt
|
||||||
@onready var _effect_status: Label = %EffectStatus
|
@onready var _effect_status: Label = %EffectStatus
|
||||||
|
@onready var _chat_ui: ChatUIType = %ChatUI
|
||||||
@onready var _title_settings_panel: SettingsPanelType = (
|
@onready var _title_settings_panel: SettingsPanelType = (
|
||||||
$UIRoot/TitleScreen/ResponsiveTitleStage/TitlePresentationScaleRoot/SettingsPanel
|
$UIRoot/TitleScreen/ResponsiveTitleStage/TitlePresentationScaleRoot/SettingsPanel
|
||||||
)
|
)
|
||||||
|
|
@ -70,6 +72,9 @@ var _item_effects: PlayerItemEffectsType
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
_pause_menu.chat_requested.connect(
|
||||||
|
func() -> void: call_deferred("_open_chat_from_pause")
|
||||||
|
)
|
||||||
_title_settings_panel.panel_visibility_changed.connect(
|
_title_settings_panel.panel_visibility_changed.connect(
|
||||||
_on_settings_visibility_changed
|
_on_settings_visibility_changed
|
||||||
)
|
)
|
||||||
|
|
@ -84,6 +89,10 @@ func _ready() -> void:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _open_chat_from_pause() -> void:
|
||||||
|
_chat_ui.open_chat()
|
||||||
|
|
||||||
|
|
||||||
func setup(
|
func setup(
|
||||||
player: PlayerType,
|
player: PlayerType,
|
||||||
inventory: FishInventoryType,
|
inventory: FishInventoryType,
|
||||||
|
|
@ -104,9 +113,22 @@ func setup(
|
||||||
network_session: NetworkSessionType,
|
network_session: NetworkSessionType,
|
||||||
network_sale_service: NetworkSaleService,
|
network_sale_service: NetworkSaleService,
|
||||||
network_shop_service: NetworkShopService,
|
network_shop_service: NetworkShopService,
|
||||||
|
network_chat_service: NetworkChatService,
|
||||||
|
network_profile: NetworkProfilePreferences,
|
||||||
|
spawn_service: PlayerSpawnService,
|
||||||
) -> void:
|
) -> void:
|
||||||
_fishing_spot = fishing_spot
|
_fishing_spot = fishing_spot
|
||||||
_item_effects = item_effects
|
_item_effects = item_effects
|
||||||
|
_chat_ui.setup(
|
||||||
|
network_chat_service, network_session, spawn_service, player,
|
||||||
|
fishing_spot
|
||||||
|
)
|
||||||
|
_title_settings_panel.setup_network_profile(
|
||||||
|
network_profile, network_session
|
||||||
|
)
|
||||||
|
_pause_settings_panel.setup_network_profile(
|
||||||
|
network_profile, network_session
|
||||||
|
)
|
||||||
fishing_spot.status_changed.connect(_on_fishing_status_changed)
|
fishing_spot.status_changed.connect(_on_fishing_status_changed)
|
||||||
fishing_spot.catch_display_changed.connect(_on_catch_display_changed)
|
fishing_spot.catch_display_changed.connect(_on_catch_display_changed)
|
||||||
fishing_spot.showcase_changed.connect(_on_showcase_changed)
|
fishing_spot.showcase_changed.connect(_on_showcase_changed)
|
||||||
|
|
@ -202,6 +224,7 @@ func get_pause_menu() -> PauseMenuType:
|
||||||
|
|
||||||
func set_gameplay_ui_enabled(enabled: bool) -> void:
|
func set_gameplay_ui_enabled(enabled: bool) -> void:
|
||||||
_gameplay_ui_enabled = enabled
|
_gameplay_ui_enabled = enabled
|
||||||
|
_refresh_chat_availability()
|
||||||
if not enabled:
|
if not enabled:
|
||||||
close_player_menu_for_session_end()
|
close_player_menu_for_session_end()
|
||||||
_fishing_shop.close_for_session_end()
|
_fishing_shop.close_for_session_end()
|
||||||
|
|
@ -217,6 +240,7 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
|
||||||
|
|
||||||
func set_system_menu_open(is_open: bool) -> void:
|
func set_system_menu_open(is_open: bool) -> void:
|
||||||
_system_menu_open = is_open
|
_system_menu_open = is_open
|
||||||
|
_refresh_chat_availability()
|
||||||
_hotbar_ui.visible = (
|
_hotbar_ui.visible = (
|
||||||
_gameplay_ui_enabled and not is_open and not _shop_open
|
_gameplay_ui_enabled and not is_open and not _shop_open
|
||||||
)
|
)
|
||||||
|
|
@ -444,6 +468,7 @@ func _on_showcase_changed(
|
||||||
|
|
||||||
func _on_player_menu_visibility_changed(is_open: bool) -> void:
|
func _on_player_menu_visibility_changed(is_open: bool) -> void:
|
||||||
_player_menu_open = is_open
|
_player_menu_open = is_open
|
||||||
|
_refresh_chat_availability()
|
||||||
_hotbar_ui.set_gameplay_input_enabled(
|
_hotbar_ui.set_gameplay_input_enabled(
|
||||||
_gameplay_ui_enabled and not is_open
|
_gameplay_ui_enabled and not is_open
|
||||||
)
|
)
|
||||||
|
|
@ -456,6 +481,7 @@ func _on_player_menu_visibility_changed(is_open: bool) -> void:
|
||||||
|
|
||||||
func _on_shop_visibility_changed(is_open: bool) -> void:
|
func _on_shop_visibility_changed(is_open: bool) -> void:
|
||||||
_shop_open = is_open
|
_shop_open = is_open
|
||||||
|
_refresh_chat_availability()
|
||||||
_hotbar_ui.visible = (
|
_hotbar_ui.visible = (
|
||||||
_gameplay_ui_enabled and not is_open and not _system_menu_open
|
_gameplay_ui_enabled and not is_open and not _system_menu_open
|
||||||
)
|
)
|
||||||
|
|
@ -474,3 +500,14 @@ func _emit_interactive_pointer_ui_changed() -> void:
|
||||||
interactive_pointer_ui_changed.emit(
|
interactive_pointer_ui_changed.emit(
|
||||||
_system_menu_open or _player_menu_open or _shop_open
|
_system_menu_open or _player_menu_open or _shop_open
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_chat_availability() -> void:
|
||||||
|
if _chat_ui == null:
|
||||||
|
return
|
||||||
|
_chat_ui.set_available(
|
||||||
|
_gameplay_ui_enabled
|
||||||
|
and not _system_menu_open
|
||||||
|
and not _player_menu_open
|
||||||
|
and not _shop_open
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=13 format=3]
|
[gd_scene load_steps=14 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"]
|
[ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"]
|
[ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"]
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
[ext_resource type="PackedScene" path="res://ui/pause_menu.tscn" id="6_pause"]
|
[ext_resource type="PackedScene" path="res://ui/pause_menu.tscn" id="6_pause"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/hotbar.tscn" id="7_hotbar"]
|
[ext_resource type="PackedScene" path="res://ui/hotbar.tscn" id="7_hotbar"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/fishing_shop.tscn" id="8_shop"]
|
[ext_resource type="PackedScene" path="res://ui/fishing_shop.tscn" id="8_shop"]
|
||||||
|
[ext_resource type="Script" path="res://ui/chat_ui.gd" id="9_chat"]
|
||||||
|
|
||||||
[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
|
[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
|
||||||
bg_color = Color(0.055, 0.105, 0.125, 1)
|
bg_color = Color(0.055, 0.105, 0.125, 1)
|
||||||
|
|
@ -185,6 +186,17 @@ theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.9)
|
||||||
theme_override_constants/shadow_offset_x = 2
|
theme_override_constants/shadow_offset_x = 2
|
||||||
theme_override_constants/shadow_offset_y = 2
|
theme_override_constants/shadow_offset_y = 2
|
||||||
|
|
||||||
|
[node name="ChatUI" type="Control" parent="UIRoot"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
script = ExtResource("9_chat")
|
||||||
|
|
||||||
[node name="ScreenFade" type="ColorRect" parent="UIRoot"]
|
[node name="ScreenFade" type="ColorRect" parent="UIRoot"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ signal reset_progress_requested
|
||||||
signal quit_requested
|
signal quit_requested
|
||||||
signal menu_visibility_changed(is_open: bool)
|
signal menu_visibility_changed(is_open: bool)
|
||||||
signal join_game_requested(endpoint: String)
|
signal join_game_requested(endpoint: String)
|
||||||
|
signal chat_requested
|
||||||
|
|
||||||
enum ConfirmationAction {
|
enum ConfirmationAction {
|
||||||
NONE,
|
NONE,
|
||||||
|
|
@ -87,6 +88,7 @@ func _ready() -> void:
|
||||||
_save_button.pressed.connect(_save_now)
|
_save_button.pressed.connect(_save_now)
|
||||||
%SettingsButton.pressed.connect(_open_settings)
|
%SettingsButton.pressed.connect(_open_settings)
|
||||||
%JoinGameButton.pressed.connect(_open_join_game)
|
%JoinGameButton.pressed.connect(_open_join_game)
|
||||||
|
%ChatButton.pressed.connect(_request_chat)
|
||||||
%ReturnToTitleButton.pressed.connect(_confirm_return_to_title)
|
%ReturnToTitleButton.pressed.connect(_confirm_return_to_title)
|
||||||
%ResetProgressButton.pressed.connect(_confirm_reset_progress)
|
%ResetProgressButton.pressed.connect(_confirm_reset_progress)
|
||||||
%QuitButton.pressed.connect(_request_quit)
|
%QuitButton.pressed.connect(_request_quit)
|
||||||
|
|
@ -105,6 +107,13 @@ func _ready() -> void:
|
||||||
call_deferred("_update_responsive_pause_stage")
|
call_deferred("_update_responsive_pause_stage")
|
||||||
|
|
||||||
|
|
||||||
|
func _request_chat() -> void:
|
||||||
|
if not visible or _action_in_progress or _root_transition_active:
|
||||||
|
return
|
||||||
|
close_menu(CloseReason.USER_RETURN, true)
|
||||||
|
chat_requested.emit()
|
||||||
|
|
||||||
|
|
||||||
func setup(
|
func setup(
|
||||||
player: PlayerType,
|
player: PlayerType,
|
||||||
save_manager: SaveManagerType,
|
save_manager: SaveManagerType,
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,8 @@ grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
script = ExtResource("4_page")
|
script = ExtResource("4_page")
|
||||||
page_id = &"pause"
|
page_id = &"pause"
|
||||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/ResumeButton"), NodePath("BubbleCluster/SaveButton"), NodePath("BubbleCluster/JoinGameButton"), NodePath("BubbleCluster/SettingsButton"), NodePath("BubbleCluster/ReturnToTitleButton"), NodePath("BubbleCluster/ResetProgressButton"), NodePath("BubbleCluster/QuitButton")])
|
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/ResumeButton"), NodePath("BubbleCluster/SaveButton"), NodePath("BubbleCluster/JoinGameButton"), NodePath("BubbleCluster/ChatButton"), NodePath("BubbleCluster/SettingsButton"), NodePath("BubbleCluster/ReturnToTitleButton"), NodePath("BubbleCluster/ResetProgressButton"), NodePath("BubbleCluster/QuitButton")])
|
||||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/ResumeButton"), NodePath("BubbleCluster/SaveButton"), NodePath("BubbleCluster/JoinGameButton"), NodePath("BubbleCluster/SettingsButton"), NodePath("BubbleCluster/ReturnToTitleButton"), NodePath("BubbleCluster/ResetProgressButton"), NodePath("BubbleCluster/QuitButton")])
|
focus_paths = Array[NodePath]([NodePath("BubbleCluster/ResumeButton"), NodePath("BubbleCluster/SaveButton"), NodePath("BubbleCluster/JoinGameButton"), NodePath("BubbleCluster/ChatButton"), NodePath("BubbleCluster/SettingsButton"), NodePath("BubbleCluster/ReturnToTitleButton"), NodePath("BubbleCluster/ResetProgressButton"), NodePath("BubbleCluster/QuitButton")])
|
||||||
initial_focus_path = NodePath("BubbleCluster/ResumeButton")
|
initial_focus_path = NodePath("BubbleCluster/ResumeButton")
|
||||||
back_focus_path = NodePath("BubbleCluster/ResumeButton")
|
back_focus_path = NodePath("BubbleCluster/ResumeButton")
|
||||||
maximum_layout_size = Vector2(720, 520)
|
maximum_layout_size = Vector2(720, 520)
|
||||||
|
|
@ -135,6 +135,18 @@ minimum_font_size = 16
|
||||||
maximum_font_size = 23
|
maximum_font_size = 23
|
||||||
motion_phase = 2.05
|
motion_phase = 2.05
|
||||||
|
|
||||||
|
[node name="ChatButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(0, 0)
|
||||||
|
text = "chat"
|
||||||
|
neutral_size = Vector2(112, 106)
|
||||||
|
desktop_anchor = Vector2(620, 255)
|
||||||
|
compact_anchor = Vector2(595, 270)
|
||||||
|
compact_minimum_size = Vector2(92, 88)
|
||||||
|
minimum_font_size = 15
|
||||||
|
maximum_font_size = 20
|
||||||
|
motion_phase = 2.3
|
||||||
|
|
||||||
[node name="JoinGameButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")]
|
[node name="JoinGameButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(0, 0)
|
custom_minimum_size = Vector2(0, 0)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ enum PresentationMode {
|
||||||
@onready var _controls_page: SettingsBubblePage = %ControlsPage
|
@onready var _controls_page: SettingsBubblePage = %ControlsPage
|
||||||
@onready var _accessibility_page: SettingsBubblePage = %AccessibilityPage
|
@onready var _accessibility_page: SettingsBubblePage = %AccessibilityPage
|
||||||
@onready var _feedback: Label = %SettingsFeedback
|
@onready var _feedback: Label = %SettingsFeedback
|
||||||
|
@onready var _display_name_edit: LineEdit = %DisplayNameEdit
|
||||||
|
|
||||||
@onready var _world_value: BubbleButton = %WorldValue
|
@onready var _world_value: BubbleButton = %WorldValue
|
||||||
@onready var _ui_value: BubbleButton = %UIValue
|
@onready var _ui_value: BubbleButton = %UIValue
|
||||||
|
|
@ -75,6 +76,8 @@ var _auto_click_interval_value: float = 0.20
|
||||||
var _mouse_sensitivity: float = 0.005
|
var _mouse_sensitivity: float = 0.005
|
||||||
var _controller_sensitivity: float = 2.5
|
var _controller_sensitivity: float = 2.5
|
||||||
var _invert_camera_y: bool = false
|
var _invert_camera_y: bool = false
|
||||||
|
var _network_profile: NetworkProfilePreferences
|
||||||
|
var _network_session: NetworkSession
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -98,6 +101,10 @@ func _ready() -> void:
|
||||||
%DisplayBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
%DisplayBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
||||||
%ControlsBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
%ControlsBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
||||||
%AccessibilityBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
%AccessibilityBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
||||||
|
_display_name_edit.text_submitted.connect(
|
||||||
|
func(_value: String) -> void: _apply_display_name()
|
||||||
|
)
|
||||||
|
_display_name_edit.focus_exited.connect(_apply_display_name)
|
||||||
for index: int in _world_options.size():
|
for index: int in _world_options.size():
|
||||||
_world_options[index].pressed.connect(
|
_world_options[index].pressed.connect(
|
||||||
_set_world_pixelation.bind(index + 1)
|
_set_world_pixelation.bind(index + 1)
|
||||||
|
|
@ -143,6 +150,33 @@ func _ready() -> void:
|
||||||
call_deferred("_refresh_panel_size")
|
call_deferred("_refresh_panel_size")
|
||||||
|
|
||||||
|
|
||||||
|
func setup_network_profile(
|
||||||
|
profile: NetworkProfilePreferences,
|
||||||
|
session: NetworkSession,
|
||||||
|
) -> void:
|
||||||
|
_network_profile = profile
|
||||||
|
_network_session = session
|
||||||
|
if _network_profile != null:
|
||||||
|
_display_name_edit.text = _network_profile.display_name
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_display_name() -> void:
|
||||||
|
if _network_profile == null:
|
||||||
|
return
|
||||||
|
var value := _display_name_edit.text.strip_edges()
|
||||||
|
var accepted := (
|
||||||
|
_network_session.update_local_display_name(value)
|
||||||
|
if _network_session != null
|
||||||
|
else _network_profile.set_display_name(value)
|
||||||
|
)
|
||||||
|
if accepted:
|
||||||
|
_display_name_edit.text = _network_profile.display_name
|
||||||
|
_feedback.text = "display name saved."
|
||||||
|
else:
|
||||||
|
_display_name_edit.text = _network_profile.display_name
|
||||||
|
_feedback.text = "display name must be 1–24 plain-text characters."
|
||||||
|
|
||||||
|
|
||||||
func open_panel(
|
func open_panel(
|
||||||
settings_manager: SettingsManagerType,
|
settings_manager: SettingsManagerType,
|
||||||
presentation_mode: PresentationMode = PresentationMode.GAMEPLAY_MODAL,
|
presentation_mode: PresentationMode = PresentationMode.GAMEPLAY_MODAL,
|
||||||
|
|
@ -158,6 +192,8 @@ func open_panel(
|
||||||
)
|
)
|
||||||
_settings_manager = settings_manager
|
_settings_manager = settings_manager
|
||||||
_load_controls()
|
_load_controls()
|
||||||
|
if _network_profile != null:
|
||||||
|
_display_name_edit.text = _network_profile.display_name
|
||||||
_feedback.text = ""
|
_feedback.text = ""
|
||||||
show()
|
show()
|
||||||
_page_stack.clear()
|
_page_stack.clear()
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,31 @@ minimum_font_size = 14
|
||||||
maximum_font_size = 17
|
maximum_font_size = 17
|
||||||
motion_phase = 4.8
|
motion_phase = 4.8
|
||||||
|
|
||||||
|
[node name="DisplayNamePanel" type="PanelContainer" parent="RootPage"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 318.0
|
||||||
|
offset_top = 430.0
|
||||||
|
offset_right = 618.0
|
||||||
|
offset_bottom = 510.0
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="RootPage/DisplayNamePanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="RootPage/DisplayNamePanel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "display name"
|
||||||
|
|
||||||
|
[node name="DisplayNameEdit" type="LineEdit" parent="RootPage/DisplayNamePanel/VBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
max_length = 24
|
||||||
|
placeholder_text = "Player"
|
||||||
|
|
||||||
|
[node name="Help" type="Label" parent="RootPage/DisplayNamePanel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Shown to other players in multiplayer."
|
||||||
|
theme_override_font_sizes/font_size = 12
|
||||||
|
|
||||||
[node name="DisplayPage" type="Control" parent="."]
|
[node name="DisplayPage" type="Control" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue