Add gathering, unified inventory, and real-time world
This commit is contained in:
parent
173371e6fc
commit
fa57edca83
113 changed files with 6700 additions and 1307 deletions
|
|
@ -5,7 +5,6 @@ const BURST_COUNT: int = 3
|
|||
const WINDOW_COUNT: int = 5
|
||||
const WINDOW_SECONDS: float = 10.0
|
||||
const CALL_COOLDOWN_MILLISECONDS: int = 180
|
||||
const WORLD_COMMAND_COOLDOWN_MILLISECONDS: int = 500
|
||||
const CALL_PITCH_VARIANTS: Array[float] = [
|
||||
0.96,
|
||||
1.03,
|
||||
|
|
@ -23,7 +22,6 @@ signal message_received(message: Dictionary)
|
|||
signal local_message_confirmed(message: Dictionary)
|
||||
signal send_rejected(message: String)
|
||||
signal history_replaced(messages: Array[Dictionary])
|
||||
signal world_command_finished(success: bool, message: String)
|
||||
signal character_call_received(
|
||||
peer_id: int,
|
||||
call_id: String,
|
||||
|
|
@ -37,22 +35,13 @@ var _request_ledgers: Dictionary[int, Dictionary] = {}
|
|||
var _rate_times: Dictionary[int, Array] = {}
|
||||
var _last_call_msec: Dictionary[int, int] = {}
|
||||
var _call_variant_indices: Dictionary[int, int] = {}
|
||||
var _last_world_command_msec: Dictionary[int, int] = {}
|
||||
var _sequence: int = 0
|
||||
var _peer_names: Dictionary[int, String] = {}
|
||||
var _relationships: PlayerRelationshipStore
|
||||
var _world_time: WorldTimeService
|
||||
var _world_weather: WorldWeatherService
|
||||
|
||||
|
||||
func setup(
|
||||
session: NetworkSession,
|
||||
world_time: WorldTimeService,
|
||||
world_weather: WorldWeatherService,
|
||||
) -> void:
|
||||
func setup(session: NetworkSession) -> void:
|
||||
_session = session
|
||||
_world_time = world_time
|
||||
_world_weather = world_weather
|
||||
_session.peer_authenticated.connect(_on_peer_authenticated)
|
||||
_session.peer_removed.connect(_on_peer_removed)
|
||||
_session.state_changed.connect(_on_session_state_changed)
|
||||
|
|
@ -261,188 +250,6 @@ func broadcast_system_message(body: String) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func request_world_time_change(phase_name: String) -> bool:
|
||||
var normalized: String = phase_name.strip_edges().to_lower()
|
||||
if not _valid_time_phase(normalized) or _session == null:
|
||||
return false
|
||||
if _session.is_host():
|
||||
return _apply_world_time_change(normalized)
|
||||
if (
|
||||
not _session.is_joined_client()
|
||||
or not _session.is_local_operator()
|
||||
or not _session.supports_server_capability(
|
||||
NetworkProtocol.WORLD_TIME_CAPABILITY
|
||||
)
|
||||
):
|
||||
return false
|
||||
submit_world_time_command.rpc_id(1, normalized)
|
||||
return true
|
||||
|
||||
|
||||
func request_world_weather_change(weather_name: String) -> bool:
|
||||
var normalized: String = _normalized_weather_name(weather_name)
|
||||
if normalized.is_empty() or _session == null:
|
||||
return false
|
||||
if _session.is_host():
|
||||
return _apply_world_weather_change(normalized)
|
||||
if (
|
||||
not _session.is_joined_client()
|
||||
or not _session.is_local_operator()
|
||||
or not _session.supports_server_capability(
|
||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||
)
|
||||
):
|
||||
return false
|
||||
submit_world_weather_command.rpc_id(1, normalized)
|
||||
return true
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
||||
func submit_world_time_command(phase_name: String) -> void:
|
||||
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||
if not _valid_world_command_sender(sender_id):
|
||||
_send_world_command_result(
|
||||
sender_id, false, "Only the host or an operator can change world time."
|
||||
)
|
||||
return
|
||||
if not _consume_world_command_rate(sender_id):
|
||||
_send_world_command_result(sender_id, false, "Slow down.")
|
||||
return
|
||||
var success: bool = _apply_world_time_change(
|
||||
phase_name.strip_edges().to_lower()
|
||||
)
|
||||
_send_world_command_result(
|
||||
sender_id,
|
||||
success,
|
||||
"" if success else "World time could not be changed.",
|
||||
)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
||||
func submit_world_weather_command(weather_name: String) -> void:
|
||||
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||
if not _valid_world_command_sender(sender_id):
|
||||
_send_world_command_result(
|
||||
sender_id,
|
||||
false,
|
||||
"Only the host or an operator can change world weather.",
|
||||
)
|
||||
return
|
||||
if not _consume_world_command_rate(sender_id):
|
||||
_send_world_command_result(sender_id, false, "Slow down.")
|
||||
return
|
||||
var success: bool = _apply_world_weather_change(
|
||||
_normalized_weather_name(weather_name)
|
||||
)
|
||||
_send_world_command_result(
|
||||
sender_id,
|
||||
success,
|
||||
"" if success else "World weather could not be changed.",
|
||||
)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
||||
func receive_world_command_result(success: bool, message: String) -> void:
|
||||
if _session == null or not _session.is_joined_client():
|
||||
return
|
||||
world_command_finished.emit(success, message.left(120))
|
||||
|
||||
|
||||
func _valid_world_command_sender(peer_id: int) -> bool:
|
||||
return (
|
||||
_session != null
|
||||
and _session.is_host()
|
||||
and peer_id > 1
|
||||
and _session.is_authenticated_peer(peer_id)
|
||||
and _session.is_peer_operator(peer_id)
|
||||
)
|
||||
|
||||
|
||||
func _apply_world_time_change(phase_name: String) -> bool:
|
||||
if _world_time == null or not _valid_time_phase(phase_name):
|
||||
return false
|
||||
var target_hour: float = _time_phase_hour(phase_name)
|
||||
if not _world_time.set_authoritative_time(target_hour):
|
||||
return false
|
||||
broadcast_system_message(
|
||||
"World time set to %s (%s)."
|
||||
% [phase_name, _world_time.get_clock_text()]
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
func _apply_world_weather_change(weather_name: String) -> bool:
|
||||
if _world_weather == null:
|
||||
return false
|
||||
var normalized: String = _normalized_weather_name(weather_name)
|
||||
if normalized.is_empty():
|
||||
return false
|
||||
var target_weather: WorldWeatherService.Weather = (
|
||||
_weather_for_name(normalized)
|
||||
)
|
||||
if not _world_weather.set_authoritative_weather(target_weather):
|
||||
return false
|
||||
broadcast_system_message("World weather set to %s." % normalized)
|
||||
return true
|
||||
|
||||
|
||||
func _send_world_command_result(
|
||||
peer_id: int,
|
||||
success: bool,
|
||||
message: String,
|
||||
) -> void:
|
||||
if peer_id > 1 and _session.is_authenticated_peer(peer_id):
|
||||
receive_world_command_result.rpc_id(peer_id, success, message.left(120))
|
||||
|
||||
|
||||
func _consume_world_command_rate(peer_id: int) -> bool:
|
||||
var now_msec: int = Time.get_ticks_msec()
|
||||
var last_msec: int = _last_world_command_msec.get(
|
||||
peer_id, now_msec - WORLD_COMMAND_COOLDOWN_MILLISECONDS
|
||||
)
|
||||
if now_msec - last_msec < WORLD_COMMAND_COOLDOWN_MILLISECONDS:
|
||||
return false
|
||||
_last_world_command_msec[peer_id] = now_msec
|
||||
return true
|
||||
|
||||
|
||||
static func _valid_time_phase(phase_name: String) -> bool:
|
||||
return phase_name in ["dawn", "day", "dusk", "night"]
|
||||
|
||||
|
||||
static func _time_phase_hour(phase_name: String) -> float:
|
||||
match phase_name:
|
||||
"dawn":
|
||||
return WorldTimeService.DAWN_START_HOUR
|
||||
"day":
|
||||
return WorldTimeService.DAWN_END_HOUR
|
||||
"dusk":
|
||||
return WorldTimeService.DUSK_START_HOUR
|
||||
"night":
|
||||
return WorldTimeService.DUSK_END_HOUR
|
||||
return -1.0
|
||||
|
||||
|
||||
static func _normalized_weather_name(weather_name: String) -> String:
|
||||
var normalized: String = weather_name.strip_edges().to_lower()
|
||||
return "clear" if normalized == "sunny" else normalized if normalized in [
|
||||
"clear", "cloudy", "rainy", "foggy"
|
||||
] else ""
|
||||
|
||||
|
||||
static func _weather_for_name(
|
||||
weather_name: String,
|
||||
) -> WorldWeatherService.Weather:
|
||||
match weather_name:
|
||||
"cloudy":
|
||||
return WorldWeatherService.Weather.CLOUDY
|
||||
"rainy":
|
||||
return WorldWeatherService.Weather.RAINY
|
||||
"foggy":
|
||||
return WorldWeatherService.Weather.FOGGY
|
||||
return WorldWeatherService.Weather.SUNNY
|
||||
|
||||
|
||||
func get_history() -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
for message: Dictionary in _history:
|
||||
|
|
@ -638,7 +445,6 @@ func _on_peer_removed(peer_id: int) -> void:
|
|||
_rate_times.erase(peer_id)
|
||||
_last_call_msec.erase(peer_id)
|
||||
_call_variant_indices.erase(peer_id)
|
||||
_last_world_command_msec.erase(peer_id)
|
||||
if not _session.is_host():
|
||||
return
|
||||
var display_name: String = _peer_names.get(peer_id, "Player")
|
||||
|
|
@ -701,7 +507,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|||
_rate_times.clear()
|
||||
_last_call_msec.clear()
|
||||
_call_variant_indices.clear()
|
||||
_last_world_command_msec.clear()
|
||||
_peer_names.clear()
|
||||
_sequence = 0
|
||||
history_replaced.emit([])
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
|||
_record_and_reject(peer_id, request_id, "Already fishing.")
|
||||
return
|
||||
if not bool(data["capacity_available"]):
|
||||
_record_and_reject(peer_id, request_id, "Cooler is full.")
|
||||
_record_and_reject(peer_id, request_id, "Inventory is full.")
|
||||
return
|
||||
var rod := _item_catalog.get_item_by_id(
|
||||
StringName(str(data["rod_id"]))
|
||||
|
|
@ -812,11 +812,8 @@ func _handle_local_capacity_probe(data: Dictionary) -> void:
|
|||
return
|
||||
var can_accept: bool = (
|
||||
_local_inventory != null
|
||||
and _local_capacity != null
|
||||
and (
|
||||
_local_inventory.contains_catch_id(StringName(data["catch_id"]))
|
||||
or _local_inventory.get_all_catches().size()
|
||||
< _local_capacity.get_capacity()
|
||||
and _local_inventory.can_accept_catch(
|
||||
StringName(data["catch_id"])
|
||||
)
|
||||
)
|
||||
if _session.is_host():
|
||||
|
|
@ -864,7 +861,7 @@ func _handle_capacity_response(
|
|||
):
|
||||
return
|
||||
if not can_accept:
|
||||
_cancel_attempt(peer_id, "Cooler is full.")
|
||||
_cancel_attempt(peer_id, "Inventory is full.")
|
||||
return
|
||||
_finalize_catch(attempt)
|
||||
|
||||
|
|
@ -915,11 +912,7 @@ func _apply_target_outcome(data: Dictionary) -> void:
|
|||
if fish_catch == null:
|
||||
return
|
||||
var already_owned: bool = _local_inventory.contains_catch_id(catch_id)
|
||||
if (
|
||||
not already_owned
|
||||
and _local_inventory.get_all_catches().size()
|
||||
>= _local_capacity.get_capacity()
|
||||
):
|
||||
if not already_owned and not _local_inventory.can_accept_catch(catch_id):
|
||||
return
|
||||
if not already_owned:
|
||||
var experience_award: int = (
|
||||
|
|
|
|||
|
|
@ -389,6 +389,10 @@ func _apply_equipped(data: Dictionary) -> void:
|
|||
item_id == FishingShopStockType.CRAB_NET_ID
|
||||
and bool(data["owns_item"]),
|
||||
)
|
||||
avatar.set_active_shovel(
|
||||
item_id == FishingShopStockType.STANDARD_SHOVEL_ID
|
||||
and bool(data["owns_item"]),
|
||||
)
|
||||
equipped_state_changed.emit(
|
||||
peer_id, StringName(str(data["item_id"])), int(data["category"])
|
||||
)
|
||||
|
|
|
|||
|
|
@ -928,7 +928,9 @@ func _can_receive(attachment: Dictionary) -> bool:
|
|||
return _wallet.can_credit(int(attachment["amount"]))
|
||||
PlayerAssetReservationService.AttachmentType.FISH:
|
||||
return (
|
||||
_inventory.get_all_catches().size() < _cooler_capacity.get_capacity()
|
||||
_inventory.can_accept_catch(
|
||||
StringName(str(attachment["catch_id"]))
|
||||
)
|
||||
and not _inventory.contains_catch_id(
|
||||
StringName(str(attachment["catch_id"]))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const MAIL_RELIABLE_CHANNEL: int = 9
|
|||
const ENET_CHANNEL_COUNT: int = 10
|
||||
const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2"
|
||||
const ART_SHOP_CAPABILITY: String = "art_shop_v1"
|
||||
const BACKPACK_SHOP_CAPABILITY: String = "backpack_shop_v1"
|
||||
const WORLD_TIME_CAPABILITY: String = "world_time_v1"
|
||||
const WORLD_WEATHER_CAPABILITY: String = "world_weather_v1"
|
||||
const FISH_QUALITY_CAPABILITY: String = "fish_quality_v1"
|
||||
|
|
@ -176,6 +177,7 @@ static func make_client_hello(
|
|||
JOBS_CAPABILITY,
|
||||
WORLD_SPAWN_CAPABILITY,
|
||||
APPEARANCE_PREVIEW_CAPABILITY,
|
||||
BACKPACK_SHOP_CAPABILITY,
|
||||
]),
|
||||
"cosmetic_snapshot": cosmetic_snapshot,
|
||||
"identity_fingerprint": identity_fingerprint,
|
||||
|
|
@ -296,6 +298,7 @@ static func make_server_hello(
|
|||
"sale_v1",
|
||||
"shop_v1",
|
||||
ART_SHOP_CAPABILITY,
|
||||
BACKPACK_SHOP_CAPABILITY,
|
||||
"item_use_v1",
|
||||
"equipment_v1",
|
||||
"fish_showcase_v1",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ const RELIABLE_CHANNEL: int = NetworkProtocol.SALE_RELIABLE_CHANNEL
|
|||
const MAX_ID_LENGTH: int = 96
|
||||
const MAX_CATCH_ID_LENGTH: int = 160
|
||||
const MAX_CATCHES_PER_REQUEST: int = 64
|
||||
const MAX_ITEM_STACKS_PER_REQUEST: int = 20
|
||||
const MAX_ITEM_QUANTITY: int = 999
|
||||
const MAX_MESSAGE_LENGTH: int = 160
|
||||
|
||||
enum Rejection {
|
||||
|
|
@ -40,6 +42,7 @@ static func validate_request(data: Variant) -> String:
|
|||
var session_id: String = payload["session_id"]
|
||||
var buyer_id: String = str(payload["buyer_id"])
|
||||
var catches: Array = payload["catches"]
|
||||
var items: Array = payload.get("items", [])
|
||||
if (
|
||||
request_id.is_empty()
|
||||
or request_id.length() > MAX_ID_LENGTH
|
||||
|
|
@ -47,8 +50,10 @@ static func validate_request(data: Variant) -> String:
|
|||
or session_id.length() > MAX_ID_LENGTH
|
||||
or buyer_id.is_empty()
|
||||
or buyer_id.length() > MAX_ID_LENGTH
|
||||
or catches.is_empty()
|
||||
or typeof(items) != TYPE_ARRAY
|
||||
or (catches.is_empty() and items.is_empty())
|
||||
or catches.size() > MAX_CATCHES_PER_REQUEST
|
||||
or items.size() > MAX_ITEM_STACKS_PER_REQUEST
|
||||
):
|
||||
return "Sale could not be completed."
|
||||
var seen: Dictionary[String, bool] = {}
|
||||
|
|
@ -74,6 +79,23 @@ static func validate_request(data: Variant) -> String:
|
|||
):
|
||||
return "Sale could not be completed."
|
||||
seen[catch_id] = true
|
||||
var seen_items: Dictionary[String, bool] = {}
|
||||
for value: Variant in items:
|
||||
if typeof(value) != TYPE_DICTIONARY:
|
||||
return "Sale could not be completed."
|
||||
var item: Dictionary = value
|
||||
var item_id := str(item.get("item_id", ""))
|
||||
var quantity := int(item.get("quantity", 0))
|
||||
if (
|
||||
item_id.is_empty()
|
||||
or item_id.length() > MAX_ID_LENGTH
|
||||
or seen_items.has(item_id)
|
||||
or typeof(item.get("quantity")) != TYPE_INT
|
||||
or quantity < 1
|
||||
or quantity > MAX_ITEM_QUANTITY
|
||||
):
|
||||
return "Sale could not be completed."
|
||||
seen_items[item_id] = true
|
||||
return ""
|
||||
|
||||
|
||||
|
|
@ -93,6 +115,8 @@ static func validate_result(data: Variant) -> bool:
|
|||
and typeof(payload.get("accepted")) == TYPE_BOOL
|
||||
and typeof(payload.get("catch_ids")) == TYPE_ARRAY
|
||||
and payload["catch_ids"].size() <= MAX_CATCHES_PER_REQUEST
|
||||
and typeof(payload.get("items", [])) == TYPE_ARRAY
|
||||
and payload.get("items", []).size() <= MAX_ITEM_STACKS_PER_REQUEST
|
||||
and typeof(payload.get("payout")) == TYPE_INT
|
||||
and int(payload["payout"]) >= 0
|
||||
and typeof(payload.get("base_value")) == TYPE_INT
|
||||
|
|
@ -119,4 +143,20 @@ static func validate_result(data: Variant) -> bool:
|
|||
):
|
||||
return false
|
||||
seen[catch_id] = true
|
||||
var seen_items: Dictionary[String, bool] = {}
|
||||
for value: Variant in payload.get("items", []):
|
||||
if typeof(value) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var item := value as Dictionary
|
||||
var item_id := str(item.get("item_id", ""))
|
||||
if (
|
||||
item_id.is_empty()
|
||||
or item_id.length() > MAX_ID_LENGTH
|
||||
or seen_items.has(item_id)
|
||||
or typeof(item.get("quantity")) != TYPE_INT
|
||||
or int(item["quantity"]) < 1
|
||||
or int(item["quantity"]) > MAX_ITEM_QUANTITY
|
||||
):
|
||||
return false
|
||||
seen_items[item_id] = true
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ const FishPoolType = preload("res://fish/fish_pool.gd")
|
|||
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
|
||||
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
|
||||
const FishSaleResultType = preload("res://economy/fish_sale_result.gd")
|
||||
const ItemDataType = preload("res://items/item_data.gd")
|
||||
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||
const ItemResalePolicyType = preload("res://economy/item_resale_policy.gd")
|
||||
|
||||
const MAX_LEDGER_ENTRIES_PER_PEER: int = 64
|
||||
const PELICAN_BUYER_ID: StringName = &"pelicans"
|
||||
|
|
@ -27,6 +31,8 @@ var _spawn_service: PlayerSpawnService
|
|||
var _network_fishing: NetworkFishingService
|
||||
var _shop_interaction: FishingShopInteraction
|
||||
var _inventory: FishInventory
|
||||
var _bag: PlayerBagType
|
||||
var _item_catalog: ItemCatalogType
|
||||
var _wallet: PlayerWallet
|
||||
var _sale_service: FishSaleServiceType
|
||||
var _save_manager: PlayerSaveManager
|
||||
|
|
@ -40,8 +46,10 @@ var _applied_results: Dictionary[String, bool] = {}
|
|||
var _received_results: Dictionary[String, bool] = {}
|
||||
var _pending_local_request_id: String = ""
|
||||
var _pending_local_catch_ids: Array[StringName] = []
|
||||
var _pending_local_items: Array[Dictionary] = []
|
||||
var _pending_local_buyer_id: StringName
|
||||
var _reservations: PlayerAssetReservationService
|
||||
var _inventory_layout: PlayerInventoryLayout
|
||||
|
||||
|
||||
func setup(
|
||||
|
|
@ -50,18 +58,23 @@ func setup(
|
|||
network_fishing: NetworkFishingService,
|
||||
shop_interaction: FishingShopInteraction,
|
||||
inventory: FishInventory,
|
||||
bag: PlayerBagType,
|
||||
item_catalog: ItemCatalogType,
|
||||
wallet: PlayerWallet,
|
||||
sale_service: FishSaleServiceType,
|
||||
save_manager: PlayerSaveManager,
|
||||
fish_catalog: FishPoolType,
|
||||
buyers: Array[FishBuyerProfileType],
|
||||
reservations: PlayerAssetReservationService,
|
||||
inventory_layout: PlayerInventoryLayout = null,
|
||||
) -> void:
|
||||
_session = session
|
||||
_spawn_service = spawn_service
|
||||
_network_fishing = network_fishing
|
||||
_shop_interaction = shop_interaction
|
||||
_inventory = inventory
|
||||
_bag = bag
|
||||
_item_catalog = item_catalog
|
||||
_wallet = wallet
|
||||
_sale_service = sale_service
|
||||
_save_manager = save_manager
|
||||
|
|
@ -71,6 +84,7 @@ func setup(
|
|||
if buyer != null and buyer.is_valid():
|
||||
_buyers[buyer.id] = buyer
|
||||
_reservations = reservations
|
||||
_inventory_layout = inventory_layout
|
||||
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):
|
||||
|
|
@ -102,6 +116,15 @@ func can_request_sale(
|
|||
func request_local_sale(
|
||||
catch_ids: Array[StringName],
|
||||
buyer_id: StringName = PELICAN_BUYER_ID,
|
||||
) -> String:
|
||||
var no_items: Dictionary[StringName, int] = {}
|
||||
return request_local_mixed_sale(catch_ids, no_items, buyer_id)
|
||||
|
||||
|
||||
func request_local_mixed_sale(
|
||||
catch_ids: Array[StringName],
|
||||
item_quantities: Dictionary[StringName, int],
|
||||
buyer_id: StringName = MAIN_SHOP_BUYER_ID,
|
||||
) -> String:
|
||||
for catch_id: StringName in catch_ids:
|
||||
if _reservations != null and _reservations.is_fish_reserved(catch_id):
|
||||
|
|
@ -123,7 +146,12 @@ func request_local_sale(
|
|||
0
|
||||
)
|
||||
return ""
|
||||
if catch_ids.is_empty() or _inventory == null:
|
||||
if (
|
||||
(catch_ids.is_empty() and item_quantities.is_empty())
|
||||
or _inventory == null
|
||||
or _bag == null
|
||||
or _item_catalog == null
|
||||
):
|
||||
local_sale_finished.emit(
|
||||
"", false, "Sale could not be completed.", _empty_catch_ids(), 0
|
||||
)
|
||||
|
|
@ -143,15 +171,41 @@ func request_local_sale(
|
|||
)
|
||||
return ""
|
||||
evidence.append(fish_catch.to_network_dict())
|
||||
var item_evidence: Array[Dictionary] = []
|
||||
var item_ids: Array[StringName] = []
|
||||
item_ids.assign(item_quantities.keys())
|
||||
item_ids.sort_custom(
|
||||
func(left: StringName, right: StringName) -> bool:
|
||||
return str(left) < str(right)
|
||||
)
|
||||
for item_id: StringName in item_ids:
|
||||
var quantity := int(item_quantities.get(item_id, 0))
|
||||
var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
|
||||
if (
|
||||
quantity < 1
|
||||
or not ItemResalePolicyType.is_sellable(item)
|
||||
or quantity > _bag.get_quantity(item_id)
|
||||
or (
|
||||
_reservations != null
|
||||
and quantity > _reservations.get_available_item_quantity(item_id)
|
||||
)
|
||||
):
|
||||
local_sale_finished.emit(
|
||||
"", false, "That item is no longer available.", _empty_catch_ids(), 0
|
||||
)
|
||||
return ""
|
||||
item_evidence.append({"item_id": str(item_id), "quantity": quantity})
|
||||
var request_id: String = _new_id("sale")
|
||||
var request: Dictionary = {
|
||||
"request_id": request_id,
|
||||
"session_id": _session.get_session_id(),
|
||||
"buyer_id": str(buyer.id),
|
||||
"catches": evidence,
|
||||
"items": item_evidence,
|
||||
}
|
||||
_pending_local_request_id = request_id
|
||||
_pending_local_catch_ids = catch_ids.duplicate()
|
||||
_pending_local_items = item_evidence.duplicate(true)
|
||||
_pending_local_buyer_id = buyer.id
|
||||
local_sale_pending.emit(request_id)
|
||||
if _session.is_host():
|
||||
|
|
@ -219,7 +273,11 @@ func _handle_sale_request(peer_id: int, data: Dictionary) -> void:
|
|||
))
|
||||
return
|
||||
var result: Dictionary = _build_authoritative_result(
|
||||
peer_id, request_id, data["catches"], buyer
|
||||
peer_id,
|
||||
request_id,
|
||||
data["catches"],
|
||||
data.get("items", []),
|
||||
buyer,
|
||||
)
|
||||
_pending_by_peer[peer_id] = request_id
|
||||
_record_and_send(peer_id, result)
|
||||
|
|
@ -229,9 +287,14 @@ func _build_authoritative_result(
|
|||
peer_id: int,
|
||||
request_id: String,
|
||||
evidence_values: Array,
|
||||
item_values: Array,
|
||||
buyer: FishBuyerProfileType,
|
||||
) -> Dictionary:
|
||||
if _fish_catalog == null or buyer == null:
|
||||
if (
|
||||
buyer == null
|
||||
or (not evidence_values.is_empty() and _fish_catalog == null)
|
||||
or (not item_values.is_empty() and _item_catalog == null)
|
||||
):
|
||||
return _rejected_result(
|
||||
request_id, "The buyer is unavailable."
|
||||
)
|
||||
|
|
@ -283,6 +346,27 @@ func _build_authoritative_result(
|
|||
base_value += decoded.sale_value
|
||||
payout += offer
|
||||
catch_ids.append(str(decoded.catch_id))
|
||||
var items: Array[Dictionary] = []
|
||||
if not item_values.is_empty() and buyer.id != MAIN_SHOP_BUYER_ID:
|
||||
return _rejected_result(request_id, "This buyer only accepts catches.")
|
||||
for value: Variant in item_values:
|
||||
var evidence := value as Dictionary
|
||||
var item_id := StringName(str(evidence.get("item_id", "")))
|
||||
var quantity := int(evidence.get("quantity", 0))
|
||||
var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
|
||||
var unit_value := ItemResalePolicyType.get_unit_value(item)
|
||||
if quantity < 1 or unit_value < 0:
|
||||
return _rejected_result(request_id, "That item cannot be sold.")
|
||||
var item_value := unit_value * quantity
|
||||
if (
|
||||
item_value < 0
|
||||
or base_value > 9223372036854775807 - item_value
|
||||
or payout > 9223372036854775807 - item_value
|
||||
):
|
||||
return _rejected_result(request_id, "Sale could not be completed.")
|
||||
base_value += item_value
|
||||
payout += item_value
|
||||
items.append({"item_id": str(item_id), "quantity": quantity})
|
||||
return {
|
||||
"result_id": _new_id("sale_result"),
|
||||
"request_id": request_id,
|
||||
|
|
@ -291,6 +375,7 @@ func _build_authoritative_result(
|
|||
"buyer_id": str(buyer.id),
|
||||
"accepted": true,
|
||||
"catch_ids": catch_ids,
|
||||
"items": items,
|
||||
"payout": payout,
|
||||
"base_value": base_value,
|
||||
"message": "Sale complete.",
|
||||
|
|
@ -313,6 +398,7 @@ func _rejected_result(request_id: String, message: String) -> Dictionary:
|
|||
"target_peer_id": 0,
|
||||
"accepted": false,
|
||||
"catch_ids": [],
|
||||
"items": [],
|
||||
"payout": 0,
|
||||
"base_value": 0,
|
||||
"message": message.left(NetworkSaleProtocol.MAX_MESSAGE_LENGTH),
|
||||
|
|
@ -388,6 +474,12 @@ func _apply_sale_result(data: Dictionary) -> void:
|
|||
if catch_ids != _pending_local_catch_ids:
|
||||
_fail_local_apply(data, "Sale could not be completed.")
|
||||
return
|
||||
var items: Array[Dictionary] = []
|
||||
for value: Variant in data.get("items", []):
|
||||
items.append((value as Dictionary).duplicate(true))
|
||||
if items != _pending_local_items:
|
||||
_fail_local_apply(data, "Sale could not be completed.")
|
||||
return
|
||||
for catch_id: StringName in catch_ids:
|
||||
if _reservations != null and _reservations.is_fish_reserved(catch_id):
|
||||
_fail_local_apply(data, "Reserved in a letter.")
|
||||
|
|
@ -396,10 +488,12 @@ func _apply_sale_result(data: Dictionary) -> void:
|
|||
if buyer == null or not buyer.is_valid():
|
||||
_fail_local_apply(data, "The buyer is unavailable.")
|
||||
return
|
||||
var preview: FishSaleResultType = _sale_service.preview_batch(
|
||||
catch_ids, buyer
|
||||
)
|
||||
if not preview.is_success():
|
||||
var catch_payout: int = 0
|
||||
var catch_base_value: int = 0
|
||||
var preview: FishSaleResultType
|
||||
if not catch_ids.is_empty():
|
||||
preview = _sale_service.preview_batch(catch_ids, buyer)
|
||||
if preview != null and not preview.is_success():
|
||||
var message: String = (
|
||||
"Favorite catches cannot be sold."
|
||||
if preview.status == FishSaleResultType.Status.FAVORITED
|
||||
|
|
@ -407,22 +501,63 @@ func _apply_sale_result(data: Dictionary) -> void:
|
|||
)
|
||||
_fail_local_apply(data, message)
|
||||
return
|
||||
if preview != null:
|
||||
catch_payout = preview.payout
|
||||
catch_base_value = preview.base_value
|
||||
var item_payout: int = 0
|
||||
for record: Dictionary in items:
|
||||
var item_id := StringName(str(record.get("item_id", "")))
|
||||
var quantity := int(record.get("quantity", 0))
|
||||
var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
|
||||
var unit_value := ItemResalePolicyType.get_unit_value(item)
|
||||
if (
|
||||
quantity < 1
|
||||
or unit_value < 0
|
||||
or quantity > _bag.get_quantity(item_id)
|
||||
or (
|
||||
_reservations != null
|
||||
and quantity > _reservations.get_available_item_quantity(item_id)
|
||||
)
|
||||
):
|
||||
_fail_local_apply(data, "That item is no longer available.")
|
||||
return
|
||||
item_payout += unit_value * quantity
|
||||
if (
|
||||
preview.payout != int(data["payout"])
|
||||
or preview.base_value != int(data["base_value"])
|
||||
catch_payout + item_payout != int(data["payout"])
|
||||
or catch_base_value + item_payout != int(data["base_value"])
|
||||
):
|
||||
_fail_local_apply(data, "Sale could not be completed.")
|
||||
return
|
||||
var inventory_snapshot: Array[FishCatch] = _inventory.get_all_catches()
|
||||
var sequence_snapshot: int = _inventory.get_next_catch_sequence()
|
||||
var wallet_snapshot: int = _wallet.get_balance()
|
||||
var local_result: FishSaleResultType = _sale_service.sell_batch(
|
||||
catch_ids, buyer
|
||||
var bag_snapshot := _bag.get_all_items()
|
||||
var layout_snapshot: Dictionary = (
|
||||
_inventory_layout.to_save_data()
|
||||
if _inventory_layout != null else {}
|
||||
)
|
||||
if not local_result.is_success() or not _save_manager.save_if_dirty():
|
||||
var wallet_snapshot: int = _wallet.get_balance()
|
||||
var applied: bool = true
|
||||
if not catch_ids.is_empty():
|
||||
applied = (
|
||||
_inventory.remove_catches_by_ids(catch_ids).size()
|
||||
== catch_ids.size()
|
||||
)
|
||||
if applied:
|
||||
for record: Dictionary in items:
|
||||
if not _bag.remove_item(
|
||||
StringName(str(record["item_id"])), int(record["quantity"])
|
||||
):
|
||||
applied = false
|
||||
break
|
||||
if applied:
|
||||
applied = _wallet.credit(int(data["payout"]))
|
||||
if not applied or not _save_manager.save_if_dirty():
|
||||
_inventory.replace_all_catches(
|
||||
inventory_snapshot, sequence_snapshot
|
||||
)
|
||||
_bag.replace_all_items(bag_snapshot)
|
||||
if _inventory_layout != null:
|
||||
_inventory_layout.restore_from_save_data(layout_snapshot)
|
||||
_wallet.restore_balance(wallet_snapshot)
|
||||
_save_manager.save_if_dirty()
|
||||
_fail_local_apply(data, "Sale could not be completed.")
|
||||
|
|
@ -457,6 +592,7 @@ func _finish_local_sale(
|
|||
) -> void:
|
||||
_pending_local_request_id = ""
|
||||
_pending_local_catch_ids.clear()
|
||||
_pending_local_items.clear()
|
||||
_pending_local_buyer_id = StringName()
|
||||
local_sale_finished.emit(
|
||||
request_id, accepted, message, catch_ids, payout
|
||||
|
|
@ -591,6 +727,7 @@ func _clear_session_state() -> void:
|
|||
_received_results.clear()
|
||||
_pending_local_request_id = ""
|
||||
_pending_local_catch_ids.clear()
|
||||
_pending_local_items.clear()
|
||||
_pending_local_buyer_id = StringName()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -271,6 +271,7 @@ func _register_player_host() -> void:
|
|||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||
NetworkProtocol.JOBS_CAPABILITY,
|
||||
NetworkProtocol.WORLD_SPAWN_CAPABILITY,
|
||||
NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
|
||||
]),
|
||||
)
|
||||
_registry.update_appearance(1, _local_appearance_snapshot)
|
||||
|
|
@ -554,6 +555,7 @@ func supports_server_capability(capability: StringName) -> bool:
|
|||
return str(capability) in PackedStringArray([
|
||||
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
|
||||
NetworkProtocol.ART_SHOP_CAPABILITY,
|
||||
NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
|
||||
"item_use_v1", "equipment_v1", "fish_showcase_v1",
|
||||
NetworkProtocol.FISH_QUALITY_CAPABILITY,
|
||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||
|
|
@ -1365,6 +1367,7 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||
NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
|
||||
]),
|
||||
)
|
||||
_registry.update_appearance(local_peer_id, _local_appearance_snapshot)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ extends RefCounted
|
|||
|
||||
const CAPABILITY: StringName = &"shop_v1"
|
||||
const ART_CAPABILITY: StringName = &"art_shop_v1"
|
||||
const BACKPACK_CAPABILITY: StringName = &"backpack_shop_v1"
|
||||
const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL
|
||||
const MAX_ID_LENGTH: int = 96
|
||||
const MAX_MESSAGE_LENGTH: int = 160
|
||||
|
|
@ -16,6 +17,7 @@ enum ProductCategory {
|
|||
COOLER_CAPACITY_UPGRADE,
|
||||
ART_KIT,
|
||||
ART_UPGRADE,
|
||||
BACKPACK_CAPACITY_UPGRADE,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const SHOP_ID: StringName = &"main_fishing_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"
|
||||
const BACKPACK_PRODUCT_ID: StringName = &"backpack_capacity_upgrade"
|
||||
const MAX_LEDGER_ENTRIES_PER_PEER: int = 64
|
||||
|
||||
signal local_purchase_pending(request_id: String)
|
||||
|
|
@ -35,6 +36,7 @@ var _bag: PlayerBag
|
|||
var _item_catalog: ItemCatalog
|
||||
var _upgrades: PlayerFishingUpgrades
|
||||
var _cooler_capacity: PlayerCoolerCapacity
|
||||
var _inventory_layout: PlayerInventoryLayout
|
||||
var _art_unlocks: PlayerArtUnlocks
|
||||
var _save_manager: PlayerSaveManager
|
||||
var _request_ledgers: Dictionary[int, Dictionary] = {}
|
||||
|
|
@ -60,6 +62,7 @@ func setup(
|
|||
art_unlocks: PlayerArtUnlocks,
|
||||
save_manager: PlayerSaveManager,
|
||||
reservations: PlayerAssetReservationService,
|
||||
inventory_layout: PlayerInventoryLayout = null,
|
||||
) -> void:
|
||||
_session = session
|
||||
_spawn_service = spawn_service
|
||||
|
|
@ -73,6 +76,7 @@ func setup(
|
|||
_art_unlocks = art_unlocks
|
||||
_save_manager = save_manager
|
||||
_reservations = reservations
|
||||
_inventory_layout = inventory_layout
|
||||
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):
|
||||
|
|
@ -104,6 +108,18 @@ func can_request_art_purchase() -> bool:
|
|||
)
|
||||
|
||||
|
||||
func can_request_backpack_purchase() -> bool:
|
||||
return (
|
||||
can_request_purchase()
|
||||
and (
|
||||
_session.is_host()
|
||||
or _session.supports_server_capability(
|
||||
NetworkShopProtocol.BACKPACK_CAPABILITY
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func is_local_purchase_pending() -> bool:
|
||||
return not _pending_local_request.is_empty()
|
||||
|
||||
|
|
@ -163,6 +179,16 @@ func request_cooler_capacity_upgrade() -> String:
|
|||
)
|
||||
|
||||
|
||||
func request_backpack_capacity_upgrade() -> String:
|
||||
return _request_purchase(
|
||||
BACKPACK_PRODUCT_ID,
|
||||
NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE,
|
||||
1,
|
||||
_inventory_layout.get_backpack_level()
|
||||
if _inventory_layout != null else 0,
|
||||
)
|
||||
|
||||
|
||||
func request_art_kit() -> String:
|
||||
return _request_purchase(
|
||||
ArtShopStockType.ART_KIT_ITEM_ID,
|
||||
|
|
@ -230,6 +256,15 @@ func _request_purchase(
|
|||
product_id, category, 0, 0,
|
||||
)
|
||||
return ""
|
||||
if (
|
||||
category == NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE
|
||||
and not can_request_backpack_purchase()
|
||||
):
|
||||
local_purchase_finished.emit(
|
||||
"", false, "Backpack upgrades require a newer server.",
|
||||
product_id, category, 0, 0,
|
||||
)
|
||||
return ""
|
||||
var request_id: String = _new_id("shop")
|
||||
var request: Dictionary = {
|
||||
"request_id": request_id,
|
||||
|
|
@ -372,7 +407,7 @@ func _build_authoritative_result(
|
|||
or current_state + quantity > item.max_stack
|
||||
):
|
||||
rejection = (
|
||||
"Your Bag is full."
|
||||
"Your inventory is full."
|
||||
if item != null and current_state >= item.max_stack
|
||||
else "Purchase could not be completed."
|
||||
)
|
||||
|
|
@ -458,6 +493,17 @@ func _build_authoritative_result(
|
|||
]
|
||||
)
|
||||
resulting_state = current_state + 1
|
||||
NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE:
|
||||
if (
|
||||
product_id != BACKPACK_PRODUCT_ID
|
||||
or current_state >= PlayerInventoryLayout.MAX_BACKPACK_LEVEL
|
||||
):
|
||||
rejection = "Upgrade is already at maximum."
|
||||
else:
|
||||
cost = PlayerInventoryLayout.BACKPACK_EXPANSION_COSTS[
|
||||
current_state
|
||||
]
|
||||
resulting_state = current_state + 1
|
||||
NetworkShopProtocol.ProductCategory.ART_KIT:
|
||||
var art_item: ItemDataType = _item_catalog.get_item_by_id(
|
||||
product_id
|
||||
|
|
@ -612,6 +658,10 @@ func _apply_purchase_result(data: Dictionary) -> void:
|
|||
var reel_snapshot: int = _upgrades.get_reel_speed_level()
|
||||
var barrier_snapshot: int = _upgrades.get_barrier_power_level()
|
||||
var cooler_snapshot: int = _cooler_capacity.get_level()
|
||||
var backpack_snapshot: int = (
|
||||
_inventory_layout.get_backpack_level()
|
||||
if _inventory_layout != null else 0
|
||||
)
|
||||
var art_snapshot: int = _art_unlocks.get_unlock_mask()
|
||||
var applied: bool = _apply_local_product(data)
|
||||
if not applied or not _save_manager.save_if_dirty():
|
||||
|
|
@ -619,6 +669,8 @@ func _apply_purchase_result(data: Dictionary) -> void:
|
|||
_bag.replace_unlocked_bait_ids(bait_unlock_snapshot)
|
||||
_upgrades.restore_levels(reel_snapshot, barrier_snapshot)
|
||||
_cooler_capacity.restore_level(cooler_snapshot)
|
||||
if _inventory_layout != null:
|
||||
_inventory_layout.restore_backpack_level(backpack_snapshot)
|
||||
_art_unlocks.restore_mask(art_snapshot)
|
||||
_wallet.restore_balance(wallet_snapshot)
|
||||
_save_manager.save_if_dirty()
|
||||
|
|
@ -687,7 +739,7 @@ func _validate_local_result(data: Dictionary) -> String:
|
|||
):
|
||||
return "Purchase could not be completed."
|
||||
if not _bag.can_add_item(product_id, data["quantity"]):
|
||||
return "Your Bag is full."
|
||||
return "Your inventory is full."
|
||||
NetworkShopProtocol.ProductCategory.ROD:
|
||||
var rod := (
|
||||
_item_catalog.get_item_by_id(product_id)
|
||||
|
|
@ -719,6 +771,13 @@ func _validate_local_result(data: Dictionary) -> String:
|
|||
return "Purchase could not be completed."
|
||||
if _cooler_capacity.get_next_cost() != cost:
|
||||
return "Purchase could not be completed."
|
||||
NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE:
|
||||
if (
|
||||
_inventory_layout == null
|
||||
or _inventory_layout.get_backpack_level() != expected_state
|
||||
or _inventory_layout.get_next_backpack_cost() != cost
|
||||
):
|
||||
return "Purchase could not be completed."
|
||||
NetworkShopProtocol.ProductCategory.ART_KIT:
|
||||
var art_item: ItemDataType = (
|
||||
_item_catalog.get_item_by_id(product_id)
|
||||
|
|
@ -768,6 +827,11 @@ func _apply_local_product(data: Dictionary) -> bool:
|
|||
return _upgrades.purchase_barrier_power(_wallet)
|
||||
NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE:
|
||||
return _cooler_capacity.purchase(_wallet)
|
||||
NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE:
|
||||
return (
|
||||
_inventory_layout != null
|
||||
and _inventory_layout.purchase_backpack(_wallet)
|
||||
)
|
||||
NetworkShopProtocol.ProductCategory.ART_KIT:
|
||||
return (
|
||||
_wallet.debit(int(data["total_cost"]))
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ var _entity_revisions: Dictionary = {}
|
|||
var _surface_triangles: Dictionary = {}
|
||||
var _surface_areas: Dictionary = {}
|
||||
var _surface_total_areas: Dictionary = {}
|
||||
var _spawn_anchor_positions: Dictionary = {}
|
||||
var _respawns: Array[Dictionary] = []
|
||||
var _next_respawn_by_type: Dictionary[StringName, float] = {}
|
||||
var _charge_requests: Dictionary = {}
|
||||
|
|
@ -151,10 +152,9 @@ func find_entity_near(
|
|||
if bool(state.get("locked", false)):
|
||||
continue
|
||||
var entity_position: Vector3 = state.get("position", Vector3.ZERO)
|
||||
var distance_squared: float = Vector2(
|
||||
entity_position.x - position.x,
|
||||
entity_position.z - position.z,
|
||||
).length_squared()
|
||||
var distance_squared: float = entity_position.distance_squared_to(
|
||||
position
|
||||
)
|
||||
if distance_squared <= best_distance_squared:
|
||||
best_distance_squared = distance_squared
|
||||
best_id = entity_id
|
||||
|
|
@ -177,10 +177,9 @@ func find_capture_target(
|
|||
if entry == null or entry.required_tool_id != tool_id:
|
||||
continue
|
||||
var entity_position: Vector3 = state.get("position", Vector3.ZERO)
|
||||
var distance_squared: float = Vector2(
|
||||
entity_position.x - position.x,
|
||||
entity_position.z - position.z,
|
||||
).length_squared()
|
||||
var distance_squared: float = entity_position.distance_squared_to(
|
||||
position
|
||||
)
|
||||
if (
|
||||
distance_squared <= entry.capture_radius * entry.capture_radius
|
||||
and distance_squared <= best_distance_squared
|
||||
|
|
@ -248,14 +247,30 @@ func _begin_population_if_ready() -> void:
|
|||
|
||||
|
||||
func _cache_spawn_surface(entry: GatherableDataType) -> void:
|
||||
if entry == null or _surface_triangles.has(entry.type_id):
|
||||
if (
|
||||
entry == null
|
||||
or _surface_triangles.has(entry.type_id)
|
||||
or _spawn_anchor_positions.has(entry.type_id)
|
||||
):
|
||||
return
|
||||
var triangles: Array[PackedVector3Array] = (
|
||||
_world.get_spawn_surface_triangles(
|
||||
if not entry.spawn_anchor_set_id.is_empty():
|
||||
var anchor_positions: PackedVector3Array = (
|
||||
_world.get_gatherable_spawn_positions(entry.spawn_anchor_set_id)
|
||||
)
|
||||
_spawn_anchor_positions[entry.type_id] = anchor_positions
|
||||
if anchor_positions.is_empty():
|
||||
push_warning(
|
||||
"No gatherable spawn anchors were found for %s." % entry.type_id
|
||||
)
|
||||
return
|
||||
var triangles: Array[PackedVector3Array]
|
||||
if not entry.diggable_area_id.is_empty():
|
||||
triangles = _world.get_diggable_area_triangles(entry.diggable_area_id)
|
||||
else:
|
||||
triangles = _world.get_spawn_surface_triangles(
|
||||
entry.surface_materials,
|
||||
entry.minimum_surface_y,
|
||||
)
|
||||
)
|
||||
var areas := PackedFloat32Array()
|
||||
var total_area: float = 0.0
|
||||
for triangle: PackedVector3Array in triangles:
|
||||
|
|
@ -279,6 +294,11 @@ func _spawn_entity(entry: GatherableDataType) -> void:
|
|||
var position: Vector3 = _sample_surface_position(entry)
|
||||
if not position.is_finite():
|
||||
return
|
||||
var target: Vector3 = (
|
||||
position
|
||||
if entry.is_stationary_spawn()
|
||||
else _sample_surface_position(entry, position, entry.roam_radius)
|
||||
)
|
||||
var quality: int = FishQualityType.roll(_rng)
|
||||
var entity_id: String = _new_id("world")
|
||||
var state: Dictionary = {
|
||||
|
|
@ -286,11 +306,16 @@ func _spawn_entity(entry: GatherableDataType) -> void:
|
|||
"type_id": entry.type_id,
|
||||
"data": entry,
|
||||
"position": position,
|
||||
"target": _sample_surface_position(entry, position, entry.roam_radius),
|
||||
"target": target,
|
||||
"yaw": _rng.randf_range(-PI, PI),
|
||||
"quality": quality,
|
||||
"revision": 1,
|
||||
"locked": false,
|
||||
"expires_at": (
|
||||
_now() + entry.active_lifetime_seconds
|
||||
if entry.active_lifetime_seconds > 0.0
|
||||
else INF
|
||||
),
|
||||
}
|
||||
if not (state["target"] as Vector3).is_finite():
|
||||
state["target"] = position
|
||||
|
|
@ -311,6 +336,11 @@ func _update_host_entities(delta: float) -> void:
|
|||
var entry := state.get("data") as GatherableDataType
|
||||
if entry == null:
|
||||
continue
|
||||
if _now() >= float(state.get("expires_at", INF)):
|
||||
_despawn_entity(entity_id, &"expired", false, true)
|
||||
continue
|
||||
if entry.is_stationary_spawn():
|
||||
continue
|
||||
var quality: int = _get_state_quality(state)
|
||||
var position: Vector3 = state["position"]
|
||||
var target: Vector3 = state["target"]
|
||||
|
|
@ -349,7 +379,7 @@ func _update_host_entities(delta: float) -> void:
|
|||
state["yaw"] = atan2(-direction.x, -direction.z)
|
||||
state["position"] = position
|
||||
_entities[entity_id] = state
|
||||
if _should_scare(entry, position, quality):
|
||||
if entry.can_be_scared() and _should_scare(entry, position, quality):
|
||||
_despawn_entity(entity_id, &"scared", true, true)
|
||||
|
||||
|
||||
|
|
@ -383,6 +413,8 @@ func _sample_surface_position(
|
|||
maximum_distance: float = INF,
|
||||
) -> Vector3:
|
||||
_cache_spawn_surface(entry)
|
||||
if not entry.spawn_anchor_set_id.is_empty():
|
||||
return _sample_anchor_position(entry, origin, maximum_distance)
|
||||
var triangles: Array = _surface_triangles.get(entry.type_id, [])
|
||||
var cumulative_areas: PackedFloat32Array = _surface_areas.get(
|
||||
entry.type_id,
|
||||
|
|
@ -420,6 +452,44 @@ func _sample_surface_position(
|
|||
return fallback
|
||||
|
||||
|
||||
func _sample_anchor_position(
|
||||
entry: GatherableDataType,
|
||||
origin: Vector3,
|
||||
maximum_distance: float,
|
||||
) -> Vector3:
|
||||
var anchors: PackedVector3Array = _spawn_anchor_positions.get(
|
||||
entry.type_id,
|
||||
PackedVector3Array(),
|
||||
)
|
||||
var candidates := PackedVector3Array()
|
||||
for anchor: Vector3 in anchors:
|
||||
if not anchor.is_finite() or _anchor_is_occupied(entry.type_id, anchor):
|
||||
continue
|
||||
if (
|
||||
origin.is_finite()
|
||||
and is_finite(maximum_distance)
|
||||
and anchor.distance_to(origin) > maximum_distance
|
||||
):
|
||||
continue
|
||||
candidates.append(anchor)
|
||||
if candidates.is_empty():
|
||||
return Vector3(INF, INF, INF)
|
||||
return candidates[_rng.randi_range(0, candidates.size() - 1)]
|
||||
|
||||
|
||||
func _anchor_is_occupied(type_id: StringName, anchor: Vector3) -> bool:
|
||||
for state: Dictionary in _entities.values():
|
||||
if StringName(state.get("type_id", StringName())) != type_id:
|
||||
continue
|
||||
var position: Variant = state.get("position")
|
||||
if (
|
||||
typeof(position) == TYPE_VECTOR3
|
||||
and (position as Vector3).distance_squared_to(anchor) <= 0.0001
|
||||
):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _broadcast_entity_snapshots() -> void:
|
||||
if _entities.is_empty():
|
||||
return
|
||||
|
|
@ -515,7 +585,9 @@ func _handle_interaction_begin(peer_id: int, data: Dictionary) -> void:
|
|||
or request_id.length() > MAX_REQUEST_ID_LENGTH
|
||||
or _pending_captures.has(peer_id)
|
||||
):
|
||||
_send_interaction_result(peer_id, request_id, false, "Cannot use the net now.")
|
||||
_send_interaction_result(
|
||||
peer_id, request_id, false, "Cannot use that gathering tool now."
|
||||
)
|
||||
return
|
||||
_charge_requests[peer_id] = {
|
||||
"request_id": request_id,
|
||||
|
|
@ -556,27 +628,26 @@ func _handle_interaction_finish(peer_id: int, data: Dictionary) -> void:
|
|||
):
|
||||
error = "The catch attempt was invalid."
|
||||
elif state.is_empty() or entry == null or bool(state.get("locked", false)):
|
||||
error = "That animal is no longer there."
|
||||
error = "That gathering spot is no longer there."
|
||||
elif _now() - float(charge.get("started", _now())) + 0.05 < entry.charge_duration:
|
||||
error = "Pull the net all the way back first."
|
||||
error = "Finish readying the tool first."
|
||||
elif _item_use.get_equipped_item_id(peer_id) != entry.required_tool_id:
|
||||
error = "Equip the correct gathering tool."
|
||||
elif avatar == null or not avatar.is_sneaking():
|
||||
error = "Sneak closer before swinging the net."
|
||||
elif avatar == null:
|
||||
error = "The player is unavailable."
|
||||
elif entry.requires_sneaking and not avatar.is_sneaking():
|
||||
error = "Sneak closer before using the tool."
|
||||
else:
|
||||
var entity_position: Vector3 = state["position"]
|
||||
var target_distance: float = Vector2(
|
||||
entity_position.x - target_position.x,
|
||||
entity_position.z - target_position.z,
|
||||
).length()
|
||||
var target_distance: float = entity_position.distance_to(target_position)
|
||||
var player_distance: float = Vector2(
|
||||
avatar.global_position.x - entity_position.x,
|
||||
avatar.global_position.z - entity_position.z,
|
||||
).length()
|
||||
if target_distance > entry.capture_radius:
|
||||
error = "The net missed."
|
||||
error = "The gathering tool missed."
|
||||
elif player_distance > entry.interaction_range:
|
||||
error = "Move closer before swinging the net."
|
||||
error = "Move closer before using the tool."
|
||||
if not error.is_empty():
|
||||
_send_interaction_result(peer_id, request_id, false, error)
|
||||
return
|
||||
|
|
@ -670,12 +741,7 @@ func _handle_local_capacity_probe(data: Dictionary) -> void:
|
|||
var catch_id := StringName(str(data.get("catch_id", "")))
|
||||
var can_accept: bool = (
|
||||
_local_inventory != null
|
||||
and _local_capacity != null
|
||||
and (
|
||||
_local_inventory.contains_catch_id(catch_id)
|
||||
or _local_inventory.get_all_catches().size()
|
||||
< _local_capacity.get_capacity()
|
||||
)
|
||||
and _local_inventory.can_accept_catch(catch_id)
|
||||
)
|
||||
if _session.is_host():
|
||||
_handle_capacity_response(
|
||||
|
|
@ -728,7 +794,7 @@ func _handle_capacity_response(
|
|||
):
|
||||
return
|
||||
if not can_accept:
|
||||
_reject_pending_capture(peer_id, "Cooler is full.")
|
||||
_reject_pending_capture(peer_id, "Inventory is full.")
|
||||
return
|
||||
_pending_captures.erase(peer_id)
|
||||
var entity_id: String = str(pending["entity_id"])
|
||||
|
|
@ -813,8 +879,7 @@ func _apply_capture_result(data: Dictionary) -> void:
|
|||
)
|
||||
if (
|
||||
not already_owned
|
||||
and _local_inventory.get_all_catches().size()
|
||||
>= _local_capacity.get_capacity()
|
||||
and not _local_inventory.can_accept_catch(fish_catch.catch_id)
|
||||
):
|
||||
return
|
||||
if not already_owned:
|
||||
|
|
@ -1138,6 +1203,7 @@ func _clear_world() -> void:
|
|||
_surface_triangles.clear()
|
||||
_surface_areas.clear()
|
||||
_surface_total_areas.clear()
|
||||
_spawn_anchor_positions.clear()
|
||||
_respawns.clear()
|
||||
_next_respawn_by_type.clear()
|
||||
_charge_requests.clear()
|
||||
|
|
|
|||
|
|
@ -45,20 +45,16 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|||
_sequence = 0
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
_world_time.set_persistence_tracking_enabled(true)
|
||||
_world_time.begin_session(
|
||||
_world_time.get_persistent_time_hours()
|
||||
)
|
||||
_world_time.begin_authoritative_session()
|
||||
return
|
||||
if state == NetworkSession.State.JOINED_CLIENT:
|
||||
_active_session_id = _session.get_session_id()
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
_world_time.set_persistence_tracking_enabled(false)
|
||||
if _session.supports_server_capability(
|
||||
NetworkProtocol.WORLD_TIME_CAPABILITY
|
||||
):
|
||||
_world_time.begin_session()
|
||||
_world_time.begin_remote_session()
|
||||
else:
|
||||
_world_time.end_session()
|
||||
return
|
||||
|
|
@ -72,7 +68,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|||
_sequence = 0
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
_world_time.set_persistence_tracking_enabled(false)
|
||||
_world_time.end_session()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ extends Node
|
|||
|
||||
const SYNC_INTERVAL_SECONDS: float = 15.0
|
||||
const MAX_SESSION_ID_LENGTH: int = 96
|
||||
const MAX_REMAINING_SECONDS: float = 1800.0
|
||||
const MAX_REMAINING_SECONDS: float = WorldWeatherService.WEATHER_PERIOD_SECONDS
|
||||
|
||||
var _session: NetworkSession
|
||||
var _world_weather: WorldWeatherService
|
||||
|
|
@ -44,7 +44,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|||
_sequence = 0
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
_world_weather.set_persistence_tracking_enabled(true)
|
||||
_world_weather.begin_authoritative_session(
|
||||
_active_session_id.hash()
|
||||
)
|
||||
|
|
@ -53,7 +52,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|||
_active_session_id = _session.get_session_id()
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
_world_weather.set_persistence_tracking_enabled(false)
|
||||
if _session.supports_server_capability(
|
||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||
):
|
||||
|
|
@ -71,7 +69,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|||
_sequence = 0
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
_world_weather.set_persistence_tracking_enabled(false)
|
||||
_world_weather.end_session()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue